From 68b73822499e6d43641490ee7af949483ddc2f0a Mon Sep 17 00:00:00 2001 From: John Lin Date: Thu, 4 Jun 2020 21:18:43 +0000 Subject: [PATCH] Bug 1628792 - p1: merge exoplayer r2.11.4 codebase. r=agi,geckoview-reviewers Reference bugs: - bug 1341990 (original import) - bug 1523544 (package renaming) - bug 1457255 (warning fixes) - bug 1459420 (Firefox ProxySelector support) Differential Revision: https://phabricator.services.mozilla.com/D78389 --- mobile/android/geckoview/build.gradle | 7 + .../exoplayer2/AudioBecomingNoisyManager.java | 81 + .../android/exoplayer2/AudioFocusManager.java | 397 +++ .../google/android/exoplayer2/BasePlayer.java | 209 ++ .../android/exoplayer2/BaseRenderer.java | 170 +- .../com/google/android/exoplayer2/C.java | 902 ++++-- .../android/exoplayer2/ControlDispatcher.java | 75 + .../exoplayer2/DefaultControlDispatcher.java | 55 + .../exoplayer2/DefaultLoadControl.java | 452 ++- .../android/exoplayer2/DefaultMediaClock.java | 197 ++ .../exoplayer2/DefaultRenderersFactory.java | 461 ++- .../exoplayer2/ExoPlaybackException.java | 145 +- .../google/android/exoplayer2/ExoPlayer.java | 691 ++-- .../android/exoplayer2/ExoPlayerFactory.java | 414 ++- .../android/exoplayer2/ExoPlayerImpl.java | 888 +++-- .../exoplayer2/ExoPlayerImplInternal.java | 2516 +++++++++------ .../exoplayer2/ExoPlayerLibraryInfo.java | 53 +- .../com/google/android/exoplayer2/Format.java | 1807 ++++++++--- .../android/exoplayer2/FormatHolder.java | 23 +- .../android/exoplayer2/LoadControl.java | 53 +- .../android/exoplayer2/MediaPeriodHolder.java | 432 +++ .../android/exoplayer2/MediaPeriodInfo.java | 141 + .../android/exoplayer2/MediaPeriodQueue.java | 743 +++++ .../android/exoplayer2/NoSampleRenderer.java | 306 ++ .../android/exoplayer2/PlaybackInfo.java | 358 +++ .../exoplayer2/PlaybackParameters.java | 64 +- .../android/exoplayer2/PlaybackPreparer.java | 23 + .../com/google/android/exoplayer2/Player.java | 1040 ++++++ .../android/exoplayer2/PlayerMessage.java | 301 ++ .../google/android/exoplayer2/Renderer.java | 87 +- .../exoplayer2/RendererCapabilities.java | 235 +- .../exoplayer2/RendererConfiguration.java | 4 +- .../android/exoplayer2/RenderersFactory.java | 18 +- .../android/exoplayer2/SeekParameters.java | 91 + .../android/exoplayer2/SimpleExoPlayer.java | 1681 ++++++++-- .../google/android/exoplayer2/Timeline.java | 838 +++-- .../android/exoplayer2/WakeLockManager.java | 101 + .../android/exoplayer2/WifiLockManager.java | 94 + .../analytics/AnalyticsCollector.java | 881 +++++ .../analytics/AnalyticsListener.java | 514 +++ .../analytics/DefaultAnalyticsListener.java | 23 + .../DefaultPlaybackSessionManager.java | 355 ++ .../analytics/PlaybackSessionManager.java | 120 + .../exoplayer2/analytics/PlaybackStats.java | 980 ++++++ .../analytics/PlaybackStatsListener.java | 1059 ++++++ .../exoplayer2/analytics/package-info.java | 19 + .../android/exoplayer2/audio/Ac3Util.java | 449 ++- .../android/exoplayer2/audio/Ac4Util.java | 250 ++ .../exoplayer2/audio/AudioAttributes.java | 162 + .../exoplayer2/audio/AudioCapabilities.java | 69 +- .../audio/AudioCapabilitiesReceiver.java | 88 +- .../audio/AudioDecoderException.java | 26 +- .../exoplayer2/audio/AudioListener.java | 41 + .../exoplayer2/audio/AudioProcessor.java | 111 +- .../audio/AudioRendererEventListener.java | 108 +- .../android/exoplayer2/audio/AudioSink.java | 329 ++ .../audio/AudioTimestampPoller.java | 309 ++ .../android/exoplayer2/audio/AudioTrack.java | 1732 ---------- .../audio/AudioTrackPositionTracker.java | 545 ++++ .../exoplayer2/audio/AuxEffectInfo.java | 85 + .../exoplayer2/audio/BaseAudioProcessor.java | 143 + .../audio/ChannelMappingAudioProcessor.java | 133 +- .../exoplayer2/audio/DefaultAudioSink.java | 1474 +++++++++ .../android/exoplayer2/audio/DtsUtil.java | 125 +- .../audio/FloatResamplingAudioProcessor.java | 109 + .../exoplayer2/audio/ForwardingAudioSink.java | 151 + .../audio/MediaCodecAudioRenderer.java | 825 ++++- .../audio/ResamplingAudioProcessor.java | 149 +- .../audio/SilenceSkippingAudioProcessor.java | 352 ++ .../audio/SimpleDecoderAudioRenderer.java | 435 ++- .../android/exoplayer2/audio/Sonic.java | 508 ++- .../exoplayer2/audio/SonicAudioProcessor.java | 161 +- .../exoplayer2/audio/TeeAudioProcessor.java | 235 ++ .../audio/TrimmingAudioProcessor.java | 178 + .../android/exoplayer2/audio/WavUtil.java | 91 + .../exoplayer2/audio/package-info.java | 19 + .../database/DatabaseIOException.java | 31 + .../exoplayer2/database/DatabaseProvider.java | 56 + .../database/DefaultDatabaseProvider.java | 42 + .../database/ExoDatabaseProvider.java | 95 + .../exoplayer2/database/VersionTable.java | 170 + .../exoplayer2/database/package-info.java | 19 + .../android/exoplayer2/decoder/Buffer.java | 5 + .../exoplayer2/decoder/CryptoInfo.java | 101 +- .../android/exoplayer2/decoder/Decoder.java | 4 + .../exoplayer2/decoder/DecoderCounters.java | 32 +- .../decoder/DecoderInputBuffer.java | 68 +- .../exoplayer2/decoder/SimpleDecoder.java | 72 +- .../decoder/SimpleOutputBuffer.java | 6 +- .../exoplayer2/decoder/package-info.java | 19 + .../android/exoplayer2/drm/ClearKeyUtil.java | 97 + .../exoplayer2/drm/DecryptionException.java | 33 +- .../exoplayer2/drm/DefaultDrmSession.java | 607 ++++ .../drm/DefaultDrmSessionEventListener.java | 51 + .../drm/DefaultDrmSessionManager.java | 1080 +++---- .../android/exoplayer2/drm/DrmInitData.java | 235 +- .../android/exoplayer2/drm/DrmSession.java | 135 +- .../exoplayer2/drm/DrmSessionManager.java | 95 +- .../exoplayer2/drm/DummyExoMediaDrm.java | 146 + .../exoplayer2/drm/ErrorStateDrmSession.java | 74 + .../exoplayer2/drm/ExoMediaCrypto.java | 13 +- .../android/exoplayer2/drm/ExoMediaDrm.java | 255 +- .../exoplayer2/drm/FrameworkMediaCrypto.java | 50 +- .../exoplayer2/drm/FrameworkMediaDrm.java | 369 ++- .../exoplayer2/drm/HttpMediaDrmCallback.java | 118 +- .../exoplayer2/drm/LocalMediaDrmCallback.java | 51 + .../exoplayer2/drm/MediaDrmCallback.java | 1 - .../exoplayer2/drm/OfflineLicenseHelper.java | 183 +- .../drm/UnsupportedDrmException.java | 7 +- .../android/exoplayer2/drm/WidevineUtil.java | 16 +- .../android/exoplayer2/drm/package-info.java | 19 + .../extractor/BinarySearchSeeker.java | 538 ++++ .../exoplayer2/extractor/ChunkIndex.java | 27 +- .../extractor/ConstantBitrateSeekMap.java | 123 + .../extractor/DefaultExtractorInput.java | 46 +- .../extractor/DefaultExtractorsFactory.java | 176 +- .../extractor/DefaultTrackOutput.java | 997 ------ .../extractor/DummyExtractorOutput.java | 35 + .../extractor/DummyTrackOutput.java | 10 +- .../exoplayer2/extractor/Extractor.java | 33 +- .../exoplayer2/extractor/ExtractorInput.java | 124 +- .../exoplayer2/extractor/ExtractorOutput.java | 2 +- .../exoplayer2/extractor/ExtractorUtil.java | 52 + .../extractor/ExtractorsFactory.java | 9 +- .../exoplayer2/extractor/FlacFrameReader.java | 336 ++ .../extractor/FlacMetadataReader.java | 312 ++ .../extractor/FlacSeekTableSeekMap.java | 84 + .../extractor/GaplessInfoHolder.java | 33 +- .../exoplayer2/extractor/Id3Peeker.java | 87 + .../exoplayer2/extractor/MpegAudioHeader.java | 121 +- .../android/exoplayer2/extractor/SeekMap.java | 96 +- .../exoplayer2/extractor/SeekPoint.java | 64 + .../exoplayer2/extractor/TrackOutput.java | 95 +- .../extractor/{ogg => }/VorbisBitArray.java | 91 +- .../extractor/{ogg => }/VorbisUtil.java | 287 +- .../extractor/amr/AmrExtractor.java | 383 +++ .../flac/FlacBinarySearchSeeker.java | 131 + .../extractor/flac/FlacExtractor.java | 411 +++ .../extractor/flv/AudioTagPayloadReader.java | 28 +- .../extractor/flv/FlvExtractor.java | 129 +- .../extractor/flv/ScriptTagPayloadReader.java | 35 +- .../extractor/flv/TagPayloadReader.java | 16 +- .../extractor/flv/VideoTagPayloadReader.java | 21 +- .../extractor/mkv/DefaultEbmlReader.java | 77 +- ...mlReaderOutput.java => EbmlProcessor.java} | 50 +- .../exoplayer2/extractor/mkv/EbmlReader.java | 41 +- .../extractor/mkv/MatroskaExtractor.java | 959 ++++-- .../exoplayer2/extractor/mkv/Sniffer.java | 7 +- .../extractor/mp3/ConstantBitrateSeeker.java | 41 +- .../exoplayer2/extractor/mp3/MlltSeeker.java | 125 + .../extractor/mp3/Mp3Extractor.java | 320 +- .../exoplayer2/extractor/mp3/Seeker.java | 60 + .../exoplayer2/extractor/mp3/VbriSeeker.java | 62 +- .../exoplayer2/extractor/mp3/XingSeeker.java | 172 +- .../exoplayer2/extractor/mp4/Atom.java | 453 ++- .../exoplayer2/extractor/mp4/AtomParsers.java | 727 +++-- .../mp4/FixedSampleSizeRechunker.java | 16 +- .../extractor/mp4/FragmentedMp4Extractor.java | 757 +++-- .../extractor/mp4/MdtaMetadataEntry.java | 115 + .../extractor/mp4/MetadataUtil.java | 431 ++- .../extractor/mp4/Mp4Extractor.java | 550 +++- .../extractor/mp4/PsshAtomUtil.java | 132 +- .../exoplayer2/extractor/mp4/Sniffer.java | 80 +- .../exoplayer2/extractor/mp4/Track.java | 52 +- .../extractor/mp4/TrackEncryptionBox.java | 76 +- .../extractor/mp4/TrackFragment.java | 4 + .../extractor/mp4/TrackSampleTable.java | 46 +- .../extractor/ogg/DefaultOggSeeker.java | 312 +- .../exoplayer2/extractor/ogg/FlacReader.java | 126 +- .../extractor/ogg/OggExtractor.java | 81 +- .../exoplayer2/extractor/ogg/OggPacket.java | 34 +- .../extractor/ogg/OggPageHeader.java | 23 +- .../exoplayer2/extractor/ogg/OggSeeker.java | 10 +- .../exoplayer2/extractor/ogg/OpusReader.java | 9 +- .../extractor/ogg/StreamReader.java | 40 +- .../extractor/ogg/VorbisReader.java | 22 +- .../extractor/rawcc/RawCcExtractor.java | 5 +- .../exoplayer2/extractor/ts/Ac3Extractor.java | 47 +- .../exoplayer2/extractor/ts/Ac3Reader.java | 14 +- .../exoplayer2/extractor/ts/Ac4Extractor.java | 156 + .../exoplayer2/extractor/ts/Ac4Reader.java | 216 ++ .../extractor/ts/AdtsExtractor.java | 252 +- .../exoplayer2/extractor/ts/AdtsReader.java | 224 +- .../ts/DefaultTsPayloadReaderFactory.java | 145 +- .../exoplayer2/extractor/ts/DtsReader.java | 20 +- .../extractor/ts/DvbSubtitleReader.java | 19 +- .../extractor/ts/ElementaryStreamReader.java | 8 +- .../exoplayer2/extractor/ts/H262Reader.java | 126 +- .../exoplayer2/extractor/ts/H264Reader.java | 114 +- .../exoplayer2/extractor/ts/H265Reader.java | 19 +- .../exoplayer2/extractor/ts/Id3Reader.java | 21 +- .../exoplayer2/extractor/ts/LatmReader.java | 310 ++ .../extractor/ts/MpegAudioReader.java | 4 +- .../exoplayer2/extractor/ts/PesReader.java | 14 +- .../extractor/ts/PsBinarySearchSeeker.java | 209 ++ .../extractor/ts/PsDurationReader.java | 259 ++ .../exoplayer2/extractor/ts/PsExtractor.java | 107 +- .../extractor/ts/SectionReader.java | 5 +- .../exoplayer2/extractor/ts/SeiReader.java | 22 +- .../extractor/ts/TsBinarySearchSeeker.java | 140 + .../extractor/ts/TsDurationReader.java | 197 ++ .../exoplayer2/extractor/ts/TsExtractor.java | 384 ++- .../extractor/ts/TsPayloadReader.java | 50 +- .../exoplayer2/extractor/ts/TsUtil.java | 96 + .../extractor/ts/UserDataReader.java | 81 + .../extractor/wav/WavExtractor.java | 542 +++- .../exoplayer2/extractor/wav/WavHeader.java | 113 +- .../extractor/wav/WavHeaderReader.java | 98 +- .../exoplayer2/extractor/wav/WavSeekMap.java | 74 + .../exoplayer2/mediacodec/MediaCodecInfo.java | 426 ++- .../mediacodec/MediaCodecRenderer.java | 1793 ++++++++--- .../mediacodec/MediaCodecSelector.java | 49 +- .../exoplayer2/mediacodec/MediaCodecUtil.java | 946 +++++- .../mediacodec/MediaFormatUtil.java | 109 + .../exoplayer2/mediacodec/package-info.java | 19 + .../android/exoplayer2/metadata/Metadata.java | 97 +- .../exoplayer2/metadata/MetadataDecoder.java | 9 +- .../metadata/MetadataDecoderFactory.java | 69 +- ...oderException.java => MetadataOutput.java} | 22 +- .../exoplayer2/metadata/MetadataRenderer.java | 130 +- .../metadata/emsg/EventMessage.java | 87 +- .../metadata/emsg/EventMessageDecoder.java | 30 +- .../metadata/emsg/EventMessageEncoder.java | 73 + .../metadata/emsg/package-info.java | 19 + .../metadata/flac/PictureFrame.java | 144 + .../metadata/flac/VorbisComment.java | 99 + .../metadata/flac/package-info.java | 19 + .../exoplayer2/metadata/icy/IcyDecoder.java | 101 + .../exoplayer2/metadata/icy/IcyHeaders.java | 243 ++ .../exoplayer2/metadata/icy/IcyInfo.java | 107 + .../exoplayer2/metadata/icy/package-info.java | 19 + .../exoplayer2/metadata/id3/ApicFrame.java | 21 +- .../exoplayer2/metadata/id3/BinaryFrame.java | 9 +- .../exoplayer2/metadata/id3/ChapterFrame.java | 7 +- .../metadata/id3/ChapterTocFrame.java | 16 +- .../exoplayer2/metadata/id3/CommentFrame.java | 18 +- .../exoplayer2/metadata/id3/GeobFrame.java | 26 +- .../exoplayer2/metadata/id3/Id3Decoder.java | 192 +- .../exoplayer2/metadata/id3/Id3Frame.java | 8 +- .../metadata/id3/InternalFrame.java | 97 + .../exoplayer2/metadata/id3/MlltFrame.java | 114 + .../exoplayer2/metadata/id3/PrivFrame.java | 15 +- .../metadata/id3/TextInformationFrame.java | 20 +- .../exoplayer2/metadata/id3/UrlLinkFrame.java | 20 +- .../exoplayer2/metadata/id3/package-info.java | 19 + .../exoplayer2/metadata/package-info.java | 19 + .../metadata/scte35/PrivateCommand.java | 14 +- .../metadata/scte35/SpliceCommand.java | 7 + .../metadata/scte35/SpliceInfoDecoder.java | 14 +- .../metadata/scte35/SpliceInsertCommand.java | 65 +- .../scte35/SpliceScheduleCommand.java | 62 +- .../metadata/scte35/TimeSignalCommand.java | 6 + .../metadata/scte35/package-info.java | 19 + .../exoplayer2/offline/ActionFile.java | 164 + .../offline/ActionFileUpgradeUtil.java | 120 + .../offline/DefaultDownloadIndex.java | 452 +++ .../offline/DefaultDownloaderFactory.java | 119 + .../android/exoplayer2/offline/Download.java | 164 + .../exoplayer2/offline/DownloadCursor.java | 129 + .../exoplayer2/offline/DownloadException.java | 33 + .../exoplayer2/offline/DownloadHelper.java | 1174 +++++++ .../exoplayer2/offline/DownloadIndex.java | 49 + .../exoplayer2/offline/DownloadManager.java | 1346 ++++++++ .../exoplayer2/offline/DownloadProgress.java | 28 + .../exoplayer2/offline/DownloadRequest.java | 212 ++ .../exoplayer2/offline/DownloadService.java | 1049 ++++++ .../exoplayer2/offline/Downloader.java | 60 + .../offline/DownloaderConstructorHelper.java | 170 + .../exoplayer2/offline/DownloaderFactory.java | 28 + .../offline/FilterableManifest.java | 36 + .../offline/FilteringManifestParser.java | 49 + .../offline/ProgressiveDownloader.java | 120 + .../exoplayer2/offline/SegmentDownloader.java | 279 ++ .../android/exoplayer2/offline/StreamKey.java | 132 + .../offline/WritableDownloadIndex.java | 87 + .../exoplayer2/offline/package-info.java | 19 + .../android/exoplayer2/package-info.java | 19 + .../scheduler/PlatformScheduler.java | 150 + .../exoplayer2/scheduler/Requirements.java | 223 ++ .../scheduler/RequirementsWatcher.java | 197 ++ .../exoplayer2/scheduler/Scheduler.java | 48 + .../exoplayer2/scheduler/package-info.java | 19 + .../source/AbstractConcatenatedTimeline.java | 327 ++ .../AdaptiveMediaSourceEventListener.java | 301 +- .../exoplayer2/source/BaseMediaSource.java | 191 ++ .../source/ClippingMediaPeriod.java | 256 +- .../source/ClippingMediaSource.java | 348 +- .../source/CompositeMediaSource.java | 354 ++ .../source/CompositeSequenceableLoader.java | 40 +- .../CompositeSequenceableLoaderFactory.java | 31 + .../source/ConcatenatingMediaSource.java | 1098 ++++++- ...ultCompositeSequenceableLoaderFactory.java | 29 + .../DefaultMediaSourceEventListener.java | 23 + .../exoplayer2/source/EmptySampleStream.java | 4 +- .../source/ExtractorMediaPeriod.java | 737 ----- .../source/ExtractorMediaSource.java | 383 ++- .../exoplayer2/source/ForwardingTimeline.java | 83 + .../exoplayer2/source/IcyDataSource.java | 149 + .../exoplayer2/source/LoopingMediaSource.java | 188 +- .../exoplayer2/source/MaskingMediaPeriod.java | 236 ++ .../exoplayer2/source/MaskingMediaSource.java | 353 ++ .../exoplayer2/source/MediaPeriod.java | 187 +- .../exoplayer2/source/MediaSource.java | 294 +- .../source/MediaSourceEventListener.java | 740 +++++ .../exoplayer2/source/MediaSourceFactory.java | 62 + .../exoplayer2/source/MergingMediaPeriod.java | 114 +- .../exoplayer2/source/MergingMediaSource.java | 126 +- .../source/ProgressiveMediaPeriod.java | 1162 +++++++ .../source/ProgressiveMediaSource.java | 327 ++ .../exoplayer2/source/SampleDataQueue.java | 472 +++ .../exoplayer2/source/SampleQueue.java | 926 ++++++ .../exoplayer2/source/SampleStream.java | 23 +- .../exoplayer2/source/SequenceableLoader.java | 26 +- .../exoplayer2/source/ShuffleOrder.java | 283 ++ .../exoplayer2/source/SilenceMediaSource.java | 253 ++ .../source/SinglePeriodTimeline.java | 161 +- .../source/SingleSampleMediaPeriod.java | 277 +- .../source/SingleSampleMediaSource.java | 323 +- .../android/exoplayer2/source/TrackGroup.java | 58 +- .../exoplayer2/source/TrackGroupArray.java | 57 +- .../source/ads/AdPlaybackState.java | 486 +++ .../exoplayer2/source/ads/AdsLoader.java | 150 + .../exoplayer2/source/ads/AdsMediaSource.java | 439 +++ .../source/ads/SinglePeriodAdTimeline.java | 66 + .../source/chunk/BaseMediaChunk.java | 35 +- .../source/chunk/BaseMediaChunkIterator.java | 75 + .../source/chunk/BaseMediaChunkOutput.java | 37 +- .../exoplayer2/source/chunk/Chunk.java | 49 +- .../source/chunk/ChunkExtractorWrapper.java | 71 +- .../exoplayer2/source/chunk/ChunkHolder.java | 8 +- .../source/chunk/ChunkSampleStream.java | 618 +++- .../exoplayer2/source/chunk/ChunkSource.java | 52 +- .../chunk/ChunkedTrackBlacklistUtil.java | 99 - .../source/chunk/ContainerMediaChunk.java | 93 +- .../exoplayer2/source/chunk/DataChunk.java | 29 +- .../source/chunk/InitializationChunk.java | 53 +- .../exoplayer2/source/chunk/MediaChunk.java | 29 +- .../source/chunk/MediaChunkIterator.java | 104 + .../source/chunk/MediaChunkListIterator.java | 61 + .../source/chunk/SingleSampleMediaChunk.java | 66 +- .../source/hls/Aes128DataSource.java | 55 +- .../hls/DefaultHlsExtractorFactory.java | 338 ++ .../hls/FullSegmentEncryptionKeyCache.java | 85 + .../exoplayer2/source/hls/HlsChunkSource.java | 594 ++-- .../source/hls/HlsExtractorFactory.java | 92 + .../exoplayer2/source/hls/HlsMediaChunk.java | 587 ++-- .../exoplayer2/source/hls/HlsMediaPeriod.java | 817 ++++- .../exoplayer2/source/hls/HlsMediaSource.java | 510 ++- .../source/hls/HlsSampleStream.java | 60 +- .../source/hls/HlsSampleStreamWrapper.java | 1494 +++++++-- .../source/hls/HlsTrackMetadataEntry.java | 245 ++ .../hls/SampleQueueMappingException.java | 30 + .../source/hls/WebvttExtractor.java | 60 +- .../source/hls/offline/HlsDownloader.java | 148 + .../source/hls/offline/package-info.java | 19 + .../exoplayer2/source/hls/package-info.java | 19 + .../DefaultHlsPlaylistParserFactory.java | 33 + .../playlist/DefaultHlsPlaylistTracker.java | 678 ++++ .../FilteringHlsPlaylistParserFactory.java | 55 + .../hls/playlist/HlsMasterPlaylist.java | 313 +- .../source/hls/playlist/HlsMediaPlaylist.java | 275 +- .../source/hls/playlist/HlsPlaylist.java | 31 +- .../hls/playlist/HlsPlaylistParser.java | 792 ++++- .../playlist/HlsPlaylistParserFactory.java | 38 + .../hls/playlist/HlsPlaylistTracker.java | 592 +--- .../source/hls/playlist/package-info.java | 19 + .../exoplayer2/text/CaptionStyleCompat.java | 47 +- .../google/android/exoplayer2/text/Cue.java | 306 +- .../text/SimpleSubtitleDecoder.java | 20 +- .../text/SubtitleDecoderException.java | 5 + .../text/SubtitleDecoderFactory.java | 118 +- .../exoplayer2/text/SubtitleInputBuffer.java | 17 +- .../exoplayer2/text/SubtitleOutputBuffer.java | 12 +- .../CeaOutputBuffer.java => TextOutput.java} | 27 +- .../android/exoplayer2/text/TextRenderer.java | 133 +- .../exoplayer2/text/cea/Cea608Decoder.java | 728 +++-- .../exoplayer2/text/cea/Cea708Cue.java | 8 +- .../exoplayer2/text/cea/Cea708Decoder.java | 43 +- .../text/cea/Cea708InitializationData.java | 54 + .../exoplayer2/text/cea/CeaDecoder.java | 68 +- .../exoplayer2/text/cea/CeaSubtitle.java | 2 +- .../android/exoplayer2/text/cea/CeaUtil.java | 119 +- .../exoplayer2/text/cea/package-info.java | 19 + .../exoplayer2/text/dvb/DvbDecoder.java | 7 +- .../exoplayer2/text/dvb/DvbParser.java | 152 +- .../exoplayer2/text/dvb/package-info.java | 19 + .../android/exoplayer2/text/package-info.java | 19 + .../exoplayer2/text/pgs/PgsDecoder.java | 259 ++ .../exoplayer2/text/pgs/PgsSubtitle.java | 51 + .../exoplayer2/text/pgs/package-info.java | 19 + .../exoplayer2/text/ssa/SsaDecoder.java | 446 +++ .../text/ssa/SsaDialogueFormat.java | 83 + .../android/exoplayer2/text/ssa/SsaStyle.java | 301 ++ .../exoplayer2/text/ssa/SsaSubtitle.java | 71 + .../exoplayer2/text/ssa/package-info.java | 19 + .../exoplayer2/text/subrip/SubripDecoder.java | 183 +- .../text/subrip/SubripSubtitle.java | 4 +- .../exoplayer2/text/subrip/package-info.java | 19 + .../exoplayer2/text/ttml/TtmlDecoder.java | 327 +- .../exoplayer2/text/ttml/TtmlNode.java | 197 +- .../exoplayer2/text/ttml/TtmlRegion.java | 37 +- .../exoplayer2/text/ttml/TtmlStyle.java | 14 +- .../exoplayer2/text/ttml/TtmlSubtitle.java | 20 +- .../exoplayer2/text/ttml/package-info.java | 19 + .../exoplayer2/text/tx3g/Tx3gDecoder.java | 27 +- .../exoplayer2/text/tx3g/Tx3gSubtitle.java | 2 +- .../exoplayer2/text/tx3g/package-info.java | 19 + .../exoplayer2/text/webvtt/CssParser.java | 84 +- .../text/webvtt/Mp4WebvttDecoder.java | 25 +- .../text/webvtt/Mp4WebvttSubtitle.java | 2 +- .../text/webvtt/WebvttCssStyle.java | 65 +- .../exoplayer2/text/webvtt/WebvttCue.java | 266 +- .../text/webvtt/WebvttCueParser.java | 140 +- .../exoplayer2/text/webvtt/WebvttDecoder.java | 17 +- .../text/webvtt/WebvttParserUtil.java | 47 +- .../text/webvtt/WebvttSubtitle.java | 25 +- .../exoplayer2/text/webvtt/package-info.java | 20 + .../AdaptiveTrackSelection.java | 664 +++- .../trackselection/BaseTrackSelection.java | 27 +- .../BufferSizeAdaptationBuilder.java | 494 +++ .../trackselection/DefaultTrackSelector.java | 2862 ++++++++++++++--- .../trackselection/FixedTrackSelection.java | 39 +- .../trackselection/MappingTrackSelector.java | 980 +++--- .../trackselection/RandomTrackSelection.java | 22 +- .../trackselection/TrackSelection.java | 160 +- .../trackselection/TrackSelectionArray.java | 27 +- .../TrackSelectionParameters.java | 336 ++ .../trackselection/TrackSelectionUtil.java | 100 + .../trackselection/TrackSelector.java | 108 +- .../trackselection/TrackSelectorResult.java | 59 +- .../trackselection/package-info.java | 19 + .../exoplayer2/upstream/Allocation.java | 19 +- .../exoplayer2/upstream/AssetDataSource.java | 47 +- .../exoplayer2/upstream/BandwidthMeter.java | 55 +- .../exoplayer2/upstream/BaseDataSource.java | 102 + .../upstream/ByteArrayDataSink.java | 15 +- .../upstream/ByteArrayDataSource.java | 24 +- .../upstream/ContentDataSource.java | 80 +- .../upstream/DataSchemeDataSource.java | 110 + .../android/exoplayer2/upstream/DataSink.java | 7 +- .../exoplayer2/upstream/DataSource.java | 31 +- .../upstream/DataSourceInputStream.java | 2 +- .../android/exoplayer2/upstream/DataSpec.java | 374 ++- .../exoplayer2/upstream/DefaultAllocator.java | 3 - .../upstream/DefaultBandwidthMeter.java | 692 +++- .../upstream/DefaultDataSource.java | 222 +- .../upstream/DefaultDataSourceFactory.java | 33 +- .../upstream/DefaultHttpDataSource.java | 386 ++- .../DefaultHttpDataSourceFactory.java | 54 +- .../DefaultLoadErrorHandlingPolicy.java | 110 + .../exoplayer2/upstream/DummyDataSource.java | 21 +- .../exoplayer2/upstream/FileDataSource.java | 100 +- .../upstream/FileDataSourceFactory.java | 20 +- .../exoplayer2/upstream/HttpDataSource.java | 67 +- .../upstream/LoadErrorHandlingPolicy.java | 87 + .../android/exoplayer2/upstream/Loader.java | 296 +- .../exoplayer2/upstream/ParsingLoadable.java | 107 +- .../upstream/PriorityDataSource.java | 14 + .../upstream/RawResourceDataSource.java | 63 +- .../upstream/ResolvingDataSource.java | 132 + .../exoplayer2/upstream/StatsDataSource.java | 113 + .../exoplayer2/upstream/TeeDataSource.java | 53 +- .../exoplayer2/upstream/TransferListener.java | 44 +- .../exoplayer2/upstream/UdpDataSource.java | 61 +- .../exoplayer2/upstream/cache/Cache.java | 143 +- .../upstream/cache/CacheDataSink.java | 83 +- .../upstream/cache/CacheDataSinkFactory.java | 21 +- .../upstream/cache/CacheDataSource.java | 455 ++- .../cache/CacheDataSourceFactory.java | 89 +- .../upstream/cache/CacheEvictor.java | 14 +- .../upstream/cache/CacheFileMetadata.java | 28 + .../cache/CacheFileMetadataIndex.java | 252 ++ .../upstream/cache/CacheKeyFactory.java | 29 + .../exoplayer2/upstream/cache/CacheSpan.java | 27 +- .../exoplayer2/upstream/cache/CacheUtil.java | 449 ++- .../upstream/cache/CachedContent.java | 139 +- .../upstream/cache/CachedContentIndex.java | 1036 ++++-- .../upstream/cache/CachedRegionTracker.java | 22 +- .../upstream/cache/ContentMetadata.java | 87 + .../cache/ContentMetadataMutations.java | 145 + .../cache/DefaultContentMetadata.java | 173 + .../cache/LeastRecentlyUsedCacheEvictor.java | 41 +- .../upstream/cache/NoOpCacheEvictor.java | 5 + .../upstream/cache/SimpleCache.java | 735 ++++- .../upstream/cache/SimpleCacheSpan.java | 146 +- .../upstream/crypto/AesCipherDataSink.java | 22 +- .../upstream/crypto/AesCipherDataSource.java | 32 +- .../upstream/crypto/AesFlushingCipher.java | 5 +- .../upstream/crypto/CryptoUtil.java | 8 +- .../android/exoplayer2/util/Assertions.java | 54 +- .../android/exoplayer2/util/AtomicFile.java | 21 +- .../google/android/exoplayer2/util/Clock.java | 28 +- .../util/CodecSpecificDataUtil.java | 136 +- .../android/exoplayer2/util/ColorParser.java | 5 +- .../exoplayer2/util/ConditionVariable.java | 25 +- .../exoplayer2/util/EGLSurfaceTexture.java | 312 ++ .../exoplayer2/util/ErrorMessageProvider.java | 31 + .../exoplayer2/util/EventDispatcher.java | 100 + .../android/exoplayer2/util/EventLogger.java | 651 ++++ .../exoplayer2/util/FlacConstants.java | 42 + .../exoplayer2/util/FlacStreamInfo.java | 78 - .../exoplayer2/util/FlacStreamMetadata.java | 384 +++ .../android/exoplayer2/util/GlUtil.java | 404 +++ .../exoplayer2/util/HandlerWrapper.java | 61 + .../exoplayer2/util/LibraryLoader.java | 8 +- .../google/android/exoplayer2/util/Log.java | 177 + .../android/exoplayer2/util/MediaClock.java | 9 +- .../android/exoplayer2/util/MimeTypes.java | 308 +- .../android/exoplayer2/util/NalUnitUtil.java | 54 +- .../android/exoplayer2/util/NonNullApi.java | 34 + .../exoplayer2/util/NotificationUtil.java | 134 + .../exoplayer2/util/ParsableBitArray.java | 179 +- .../exoplayer2/util/ParsableByteArray.java | 67 +- .../util/ParsableNalUnitBitArray.java | 86 +- .../android/exoplayer2/util/Predicate.java | 2 +- .../exoplayer2/util/PriorityTaskManager.java | 2 +- .../exoplayer2/util/RepeatModeUtil.java | 95 + .../exoplayer2/util/SlidingPercentile.java | 24 +- .../exoplayer2/util/StandaloneMediaClock.java | 41 +- .../android/exoplayer2/util/SystemClock.java | 21 +- .../exoplayer2/util/SystemHandlerWrapper.java | 85 + .../exoplayer2/util/TimedValueQueue.java | 161 + .../exoplayer2/util/TimestampAdjuster.java | 80 +- .../android/exoplayer2/util/UriUtil.java | 29 +- .../google/android/exoplayer2/util/Util.java | 1555 +++++++-- .../exoplayer2/util/XmlPullParserUtil.java | 48 +- .../android/exoplayer2/util/package-info.java | 17 + .../android/exoplayer2/video/ColorInfo.java | 55 +- .../exoplayer2/video/DolbyVisionConfig.java | 64 + .../exoplayer2/video/DummySurface.java | 228 ++ .../android/exoplayer2/video/HevcConfig.java | 5 +- .../video/MediaCodecVideoRenderer.java | 1568 +++++++-- .../video/SimpleDecoderVideoRenderer.java | 975 ++++++ .../video/VideoDecoderException.java | 40 + .../video/VideoDecoderGLSurfaceView.java | 57 + .../video/VideoDecoderInputBuffer.java | 30 + .../video/VideoDecoderOutputBuffer.java | 185 ++ .../VideoDecoderOutputBufferRenderer.java | 27 + .../video/VideoDecoderRenderer.java | 241 ++ .../video/VideoFrameMetadataListener.java | 40 + .../video/VideoFrameReleaseTimeHelper.java | 132 +- .../exoplayer2/video/VideoListener.java | 58 + .../video/VideoRendererEventListener.java | 170 +- .../exoplayer2/video/package-info.java | 19 + .../video/spherical/CameraMotionListener.java | 32 + .../video/spherical/CameraMotionRenderer.java | 134 + .../video/spherical/FrameRotationQueue.java | 124 + .../video/spherical/Projection.java | 236 ++ .../video/spherical/ProjectionDecoder.java | 238 ++ .../video/spherical/package-info.java | 19 + 550 files changed, 90410 insertions(+), 20731 deletions(-) create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioBecomingNoisyManager.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioFocusManager.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/BasePlayer.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ControlDispatcher.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultControlDispatcher.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultMediaClock.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodHolder.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodInfo.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodQueue.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/NoSampleRenderer.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackInfo.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackPreparer.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Player.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlayerMessage.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SeekParameters.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/WakeLockManager.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/WifiLockManager.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsCollector.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsListener.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStats.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Ac4Util.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioAttributes.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioListener.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioSink.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTimestampPoller.java delete mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTrack.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AuxEffectInfo.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/BaseAudioProcessor.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DefaultAudioSink.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ForwardingAudioSink.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/TeeAudioProcessor.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/WavUtil.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DatabaseIOException.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DatabaseProvider.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DefaultDatabaseProvider.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/ExoDatabaseProvider.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/VersionTable.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ClearKeyUtil.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSession.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java delete mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorUtil.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacFrameReader.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Id3Peeker.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekPoint.java rename mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/{ogg => }/VorbisBitArray.java (56%) rename mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/{ogg => }/VorbisUtil.java (78%) create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java rename mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/{EbmlReaderOutput.java => EbmlProcessor.java} (71%) create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Seeker.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/LatmReader.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsUtil.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/UserDataReader.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/package-info.java rename mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/{MetadataDecoderException.java => MetadataOutput.java} (56%) create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/PictureFrame.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/VorbisComment.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyInfo.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/InternalFrame.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/MlltFrame.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ActionFile.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/Download.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadCursor.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadException.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadHelper.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadIndex.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadManager.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadProgress.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadRequest.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadService.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/Downloader.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloaderFactory.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/FilterableManifest.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/FilteringManifestParser.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ProgressiveDownloader.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/SegmentDownloader.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/StreamKey.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/WritableDownloadIndex.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/PlatformScheduler.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Requirements.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Scheduler.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BaseMediaSource.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeMediaSource.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java delete mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ForwardingTimeline.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/IcyDataSource.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaPeriod.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaSource.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceEventListener.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceFactory.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaSource.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleDataQueue.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleQueue.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ShuffleOrder.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SilenceMediaSource.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdPlaybackState.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsLoader.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsMediaSource.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java delete mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/FullSegmentEncryptionKeyCache.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParserFactory.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/package-info.java rename mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/{cea/CeaOutputBuffer.java => TextOutput.java} (55%) create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/PgsDecoder.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaDecoder.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaStyle.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BaseDataSource.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ResolvingDataSource.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/StatsDataSource.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EGLSurfaceTexture.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ErrorMessageProvider.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventDispatcher.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventLogger.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacConstants.java delete mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacStreamInfo.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacStreamMetadata.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/GlUtil.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/HandlerWrapper.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Log.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NonNullApi.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NotificationUtil.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/RepeatModeUtil.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemHandlerWrapper.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimedValueQueue.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/DolbyVisionConfig.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/DummySurface.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderException.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderOutputBufferRenderer.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderRenderer.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoListener.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/package-info.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/CameraMotionListener.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/FrameRotationQueue.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/Projection.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java create mode 100644 mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/package-info.java diff --git a/mobile/android/geckoview/build.gradle b/mobile/android/geckoview/build.gradle index ef8bfec1ddd5..e295f33e208c 100644 --- a/mobile/android/geckoview/build.gradle +++ b/mobile/android/geckoview/build.gradle @@ -212,6 +212,13 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { } dependencies { + // For exoplayer. + implementation "androidx.annotation:annotation:1.1.0" + compileOnly "com.google.code.findbugs:jsr305:3.0.2" + compileOnly "org.checkerframework:checker-compat-qual:2.5.0" + compileOnly "org.checkerframework:checker-qual:2.5.0" + compileOnly "org.jetbrains.kotlin:kotlin-annotations-jvm:1.3.70" + implementation "com.android.support:support-v4:$support_library_version" implementation "com.android.support:palette-v7:$support_library_version" implementation "org.yaml:snakeyaml:1.24:android" diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioBecomingNoisyManager.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioBecomingNoisyManager.java new file mode 100644 index 000000000000..c833c448e46e --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioBecomingNoisyManager.java @@ -0,0 +1,81 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.os.Handler; + +/* package */ final class AudioBecomingNoisyManager { + + private final Context context; + private final AudioBecomingNoisyReceiver receiver; + private boolean receiverRegistered; + + public interface EventListener { + void onAudioBecomingNoisy(); + } + + public AudioBecomingNoisyManager(Context context, Handler eventHandler, EventListener listener) { + this.context = context.getApplicationContext(); + this.receiver = new AudioBecomingNoisyReceiver(eventHandler, listener); + } + + /** + * Enables the {@link AudioBecomingNoisyManager} which calls {@link + * EventListener#onAudioBecomingNoisy()} upon receiving an intent of {@link + * AudioManager#ACTION_AUDIO_BECOMING_NOISY}. + * + * @param enabled True if the listener should be notified when audio is becoming noisy. + */ + public void setEnabled(boolean enabled) { + if (enabled && !receiverRegistered) { + context.registerReceiver( + receiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + receiverRegistered = true; + } else if (!enabled && receiverRegistered) { + context.unregisterReceiver(receiver); + receiverRegistered = false; + } + } + + private final class AudioBecomingNoisyReceiver extends BroadcastReceiver implements Runnable { + private final EventListener listener; + private final Handler eventHandler; + + public AudioBecomingNoisyReceiver(Handler eventHandler, EventListener listener) { + this.eventHandler = eventHandler; + this.listener = listener; + } + + @Override + public void onReceive(Context context, Intent intent) { + if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) { + eventHandler.post(this); + } + } + + @Override + public void run() { + if (receiverRegistered) { + listener.onAudioBecomingNoisy(); + } + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioFocusManager.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioFocusManager.java new file mode 100644 index 000000000000..5806f57a0851 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioFocusManager.java @@ -0,0 +1,397 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import android.content.Context; +import android.media.AudioFocusRequest; +import android.media.AudioManager; +import android.os.Handler; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioAttributes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** Manages requesting and responding to changes in audio focus. */ +/* package */ final class AudioFocusManager { + + /** Interface to allow AudioFocusManager to give commands to a player. */ + public interface PlayerControl { + /** + * Called when the volume multiplier on the player should be changed. + * + * @param volumeMultiplier The new volume multiplier. + */ + void setVolumeMultiplier(float volumeMultiplier); + + /** + * Called when a command must be executed on the player. + * + * @param playerCommand The command that must be executed. + */ + void executePlayerCommand(@PlayerCommand int playerCommand); + } + + /** + * Player commands. One of {@link #PLAYER_COMMAND_DO_NOT_PLAY}, {@link + * #PLAYER_COMMAND_WAIT_FOR_CALLBACK} or {@link #PLAYER_COMMAND_PLAY_WHEN_READY}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + PLAYER_COMMAND_DO_NOT_PLAY, + PLAYER_COMMAND_WAIT_FOR_CALLBACK, + PLAYER_COMMAND_PLAY_WHEN_READY, + }) + public @interface PlayerCommand {} + /** Do not play. */ + public static final int PLAYER_COMMAND_DO_NOT_PLAY = -1; + /** Do not play now. Wait for callback to play. */ + public static final int PLAYER_COMMAND_WAIT_FOR_CALLBACK = 0; + /** Play freely. */ + public static final int PLAYER_COMMAND_PLAY_WHEN_READY = 1; + + /** Audio focus state. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + AUDIO_FOCUS_STATE_NO_FOCUS, + AUDIO_FOCUS_STATE_HAVE_FOCUS, + AUDIO_FOCUS_STATE_LOSS_TRANSIENT, + AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK + }) + private @interface AudioFocusState {} + /** No audio focus is currently being held. */ + private static final int AUDIO_FOCUS_STATE_NO_FOCUS = 0; + /** The requested audio focus is currently held. */ + private static final int AUDIO_FOCUS_STATE_HAVE_FOCUS = 1; + /** Audio focus has been temporarily lost. */ + private static final int AUDIO_FOCUS_STATE_LOSS_TRANSIENT = 2; + /** Audio focus has been temporarily lost, but playback may continue with reduced volume. */ + private static final int AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK = 3; + + private static final String TAG = "AudioFocusManager"; + + private static final float VOLUME_MULTIPLIER_DUCK = 0.2f; + private static final float VOLUME_MULTIPLIER_DEFAULT = 1.0f; + + private final AudioManager audioManager; + private final AudioFocusListener focusListener; + @Nullable private PlayerControl playerControl; + @Nullable private AudioAttributes audioAttributes; + + @AudioFocusState private int audioFocusState; + @C.AudioFocusGain private int focusGain; + private float volumeMultiplier = VOLUME_MULTIPLIER_DEFAULT; + + private @MonotonicNonNull AudioFocusRequest audioFocusRequest; + private boolean rebuildAudioFocusRequest; + + /** + * Constructs an AudioFocusManager to automatically handle audio focus for a player. + * + * @param context The current context. + * @param eventHandler A {@link Handler} to for the thread on which the player is used. + * @param playerControl A {@link PlayerControl} to handle commands from this instance. + */ + public AudioFocusManager(Context context, Handler eventHandler, PlayerControl playerControl) { + this.audioManager = + (AudioManager) context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE); + this.playerControl = playerControl; + this.focusListener = new AudioFocusListener(eventHandler); + this.audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS; + } + + /** Gets the current player volume multiplier. */ + public float getVolumeMultiplier() { + return volumeMultiplier; + } + + /** + * Sets audio attributes that should be used to manage audio focus. + * + *

Call {@link #updateAudioFocus(boolean, int)} to update the audio focus based on these + * attributes. + * + * @param audioAttributes The audio attributes or {@code null} if audio focus should not be + * managed automatically. + */ + public void setAudioAttributes(@Nullable AudioAttributes audioAttributes) { + if (!Util.areEqual(this.audioAttributes, audioAttributes)) { + this.audioAttributes = audioAttributes; + focusGain = convertAudioAttributesToFocusGain(audioAttributes); + Assertions.checkArgument( + focusGain == C.AUDIOFOCUS_GAIN || focusGain == C.AUDIOFOCUS_NONE, + "Automatic handling of audio focus is only available for USAGE_MEDIA and USAGE_GAME."); + } + } + + /** + * Called by the player to abandon or request audio focus based on the desired player state. + * + * @param playWhenReady The desired value of playWhenReady. + * @param playbackState The desired playback state. + * @return A {@link PlayerCommand} to execute on the player. + */ + @PlayerCommand + public int updateAudioFocus(boolean playWhenReady, @Player.State int playbackState) { + if (shouldAbandonAudioFocus(playbackState)) { + abandonAudioFocus(); + return playWhenReady ? PLAYER_COMMAND_PLAY_WHEN_READY : PLAYER_COMMAND_DO_NOT_PLAY; + } + return playWhenReady ? requestAudioFocus() : PLAYER_COMMAND_DO_NOT_PLAY; + } + + /** + * Called when the manager is no longer required. Audio focus will be released without making any + * calls to the {@link PlayerControl}. + */ + public void release() { + playerControl = null; + abandonAudioFocus(); + } + + // Internal methods. + + @VisibleForTesting + /* package */ AudioManager.OnAudioFocusChangeListener getFocusListener() { + return focusListener; + } + + private boolean shouldAbandonAudioFocus(@Player.State int playbackState) { + return playbackState == Player.STATE_IDLE || focusGain != C.AUDIOFOCUS_GAIN; + } + + @PlayerCommand + private int requestAudioFocus() { + if (audioFocusState == AUDIO_FOCUS_STATE_HAVE_FOCUS) { + return PLAYER_COMMAND_PLAY_WHEN_READY; + } + int requestResult = Util.SDK_INT >= 26 ? requestAudioFocusV26() : requestAudioFocusDefault(); + if (requestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + setAudioFocusState(AUDIO_FOCUS_STATE_HAVE_FOCUS); + return PLAYER_COMMAND_PLAY_WHEN_READY; + } else { + setAudioFocusState(AUDIO_FOCUS_STATE_NO_FOCUS); + return PLAYER_COMMAND_DO_NOT_PLAY; + } + } + + private void abandonAudioFocus() { + if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { + return; + } + if (Util.SDK_INT >= 26) { + abandonAudioFocusV26(); + } else { + abandonAudioFocusDefault(); + } + setAudioFocusState(AUDIO_FOCUS_STATE_NO_FOCUS); + } + + private int requestAudioFocusDefault() { + return audioManager.requestAudioFocus( + focusListener, + Util.getStreamTypeForAudioUsage(Assertions.checkNotNull(audioAttributes).usage), + focusGain); + } + + @RequiresApi(26) + private int requestAudioFocusV26() { + if (audioFocusRequest == null || rebuildAudioFocusRequest) { + AudioFocusRequest.Builder builder = + audioFocusRequest == null + ? new AudioFocusRequest.Builder(focusGain) + : new AudioFocusRequest.Builder(audioFocusRequest); + + boolean willPauseWhenDucked = willPauseWhenDucked(); + audioFocusRequest = + builder + .setAudioAttributes(Assertions.checkNotNull(audioAttributes).getAudioAttributesV21()) + .setWillPauseWhenDucked(willPauseWhenDucked) + .setOnAudioFocusChangeListener(focusListener) + .build(); + + rebuildAudioFocusRequest = false; + } + return audioManager.requestAudioFocus(audioFocusRequest); + } + + private void abandonAudioFocusDefault() { + audioManager.abandonAudioFocus(focusListener); + } + + @RequiresApi(26) + private void abandonAudioFocusV26() { + if (audioFocusRequest != null) { + audioManager.abandonAudioFocusRequest(audioFocusRequest); + } + } + + private boolean willPauseWhenDucked() { + return audioAttributes != null && audioAttributes.contentType == C.CONTENT_TYPE_SPEECH; + } + + /** + * Converts {@link AudioAttributes} to one of the audio focus request. + * + *

This follows the class Javadoc of {@link AudioFocusRequest}. + * + * @param audioAttributes The audio attributes associated with this focus request. + * @return The type of audio focus gain that should be requested. + */ + @C.AudioFocusGain + private static int convertAudioAttributesToFocusGain(@Nullable AudioAttributes audioAttributes) { + if (audioAttributes == null) { + // Don't handle audio focus. It may be either video only contents or developers + // want to have more finer grained control. (e.g. adding audio focus listener) + return C.AUDIOFOCUS_NONE; + } + + switch (audioAttributes.usage) { + // USAGE_VOICE_COMMUNICATION_SIGNALLING is for DTMF that may happen multiple times + // during the phone call when AUDIOFOCUS_GAIN_TRANSIENT is requested for that. + // Don't request audio focus here. + case C.USAGE_VOICE_COMMUNICATION_SIGNALLING: + return C.AUDIOFOCUS_NONE; + + // Javadoc says 'AUDIOFOCUS_GAIN: Examples of uses of this focus gain are for music + // playback, for a game or a video player' + case C.USAGE_GAME: + case C.USAGE_MEDIA: + return C.AUDIOFOCUS_GAIN; + + // Special usages: USAGE_UNKNOWN shouldn't be used. Request audio focus to prevent + // multiple media playback happen at the same time. + case C.USAGE_UNKNOWN: + Log.w( + TAG, + "Specify a proper usage in the audio attributes for audio focus" + + " handling. Using AUDIOFOCUS_GAIN by default."); + return C.AUDIOFOCUS_GAIN; + + // Javadoc says 'AUDIOFOCUS_GAIN_TRANSIENT: An example is for playing an alarm, or + // during a VoIP call' + case C.USAGE_ALARM: + case C.USAGE_VOICE_COMMUNICATION: + return C.AUDIOFOCUS_GAIN_TRANSIENT; + + // Javadoc says 'AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: Examples are when playing + // driving directions or notifications' + case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: + case C.USAGE_ASSISTANCE_SONIFICATION: + case C.USAGE_NOTIFICATION: + case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED: + case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT: + case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST: + case C.USAGE_NOTIFICATION_EVENT: + case C.USAGE_NOTIFICATION_RINGTONE: + return C.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK; + + // Javadoc says 'AUDIOFOCUS_GAIN_EXCLUSIVE: This is typically used if you are doing + // audio recording or speech recognition'. + // Assistant is considered as both recording and notifying developer + case C.USAGE_ASSISTANT: + if (Util.SDK_INT >= 19) { + return C.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE; + } else { + return C.AUDIOFOCUS_GAIN_TRANSIENT; + } + + // Special usages: + case C.USAGE_ASSISTANCE_ACCESSIBILITY: + if (audioAttributes.contentType == C.CONTENT_TYPE_SPEECH) { + // Voice shouldn't be interrupted by other playback. + return C.AUDIOFOCUS_GAIN_TRANSIENT; + } + return C.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK; + default: + Log.w(TAG, "Unidentified audio usage: " + audioAttributes.usage); + return C.AUDIOFOCUS_NONE; + } + } + + private void setAudioFocusState(@AudioFocusState int audioFocusState) { + if (this.audioFocusState == audioFocusState) { + return; + } + this.audioFocusState = audioFocusState; + + float volumeMultiplier = + (audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK) + ? AudioFocusManager.VOLUME_MULTIPLIER_DUCK + : AudioFocusManager.VOLUME_MULTIPLIER_DEFAULT; + if (this.volumeMultiplier == volumeMultiplier) { + return; + } + this.volumeMultiplier = volumeMultiplier; + if (playerControl != null) { + playerControl.setVolumeMultiplier(volumeMultiplier); + } + } + + private void handlePlatformAudioFocusChange(int focusChange) { + switch (focusChange) { + case AudioManager.AUDIOFOCUS_GAIN: + setAudioFocusState(AUDIO_FOCUS_STATE_HAVE_FOCUS); + executePlayerCommand(PLAYER_COMMAND_PLAY_WHEN_READY); + return; + case AudioManager.AUDIOFOCUS_LOSS: + executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY); + abandonAudioFocus(); + return; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || willPauseWhenDucked()) { + executePlayerCommand(PLAYER_COMMAND_WAIT_FOR_CALLBACK); + setAudioFocusState(AUDIO_FOCUS_STATE_LOSS_TRANSIENT); + } else { + setAudioFocusState(AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK); + } + return; + default: + Log.w(TAG, "Unknown focus change type: " + focusChange); + } + } + + private void executePlayerCommand(@PlayerCommand int playerCommand) { + if (playerControl != null) { + playerControl.executePlayerCommand(playerCommand); + } + } + + // Internal audio focus listener. + + private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener { + private final Handler eventHandler; + + public AudioFocusListener(Handler eventHandler) { + this.eventHandler = eventHandler; + } + + @Override + public void onAudioFocusChange(int focusChange) { + eventHandler.post(() -> handlePlatformAudioFocusChange(focusChange)); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/BasePlayer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/BasePlayer.java new file mode 100644 index 000000000000..c06361e69b85 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/BasePlayer.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** Abstract base {@link Player} which implements common implementation independent methods. */ +public abstract class BasePlayer implements Player { + + protected final Timeline.Window window; + + public BasePlayer() { + window = new Timeline.Window(); + } + + @Override + public final boolean isPlaying() { + return getPlaybackState() == Player.STATE_READY + && getPlayWhenReady() + && getPlaybackSuppressionReason() == PLAYBACK_SUPPRESSION_REASON_NONE; + } + + @Override + public final void seekToDefaultPosition() { + seekToDefaultPosition(getCurrentWindowIndex()); + } + + @Override + public final void seekToDefaultPosition(int windowIndex) { + seekTo(windowIndex, /* positionMs= */ C.TIME_UNSET); + } + + @Override + public final void seekTo(long positionMs) { + seekTo(getCurrentWindowIndex(), positionMs); + } + + @Override + public final boolean hasPrevious() { + return getPreviousWindowIndex() != C.INDEX_UNSET; + } + + @Override + public final void previous() { + int previousWindowIndex = getPreviousWindowIndex(); + if (previousWindowIndex != C.INDEX_UNSET) { + seekToDefaultPosition(previousWindowIndex); + } + } + + @Override + public final boolean hasNext() { + return getNextWindowIndex() != C.INDEX_UNSET; + } + + @Override + public final void next() { + int nextWindowIndex = getNextWindowIndex(); + if (nextWindowIndex != C.INDEX_UNSET) { + seekToDefaultPosition(nextWindowIndex); + } + } + + @Override + public final void stop() { + stop(/* reset= */ false); + } + + @Override + public final int getNextWindowIndex() { + Timeline timeline = getCurrentTimeline(); + return timeline.isEmpty() + ? C.INDEX_UNSET + : timeline.getNextWindowIndex( + getCurrentWindowIndex(), getRepeatModeForNavigation(), getShuffleModeEnabled()); + } + + @Override + public final int getPreviousWindowIndex() { + Timeline timeline = getCurrentTimeline(); + return timeline.isEmpty() + ? C.INDEX_UNSET + : timeline.getPreviousWindowIndex( + getCurrentWindowIndex(), getRepeatModeForNavigation(), getShuffleModeEnabled()); + } + + @Override + @Nullable + public final Object getCurrentTag() { + Timeline timeline = getCurrentTimeline(); + return timeline.isEmpty() ? null : timeline.getWindow(getCurrentWindowIndex(), window).tag; + } + + @Override + @Nullable + public final Object getCurrentManifest() { + Timeline timeline = getCurrentTimeline(); + return timeline.isEmpty() ? null : timeline.getWindow(getCurrentWindowIndex(), window).manifest; + } + + @Override + public final int getBufferedPercentage() { + long position = getBufferedPosition(); + long duration = getDuration(); + return position == C.TIME_UNSET || duration == C.TIME_UNSET + ? 0 + : duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100); + } + + @Override + public final boolean isCurrentWindowDynamic() { + Timeline timeline = getCurrentTimeline(); + return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isDynamic; + } + + @Override + public final boolean isCurrentWindowLive() { + Timeline timeline = getCurrentTimeline(); + return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isLive; + } + + @Override + public final boolean isCurrentWindowSeekable() { + Timeline timeline = getCurrentTimeline(); + return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isSeekable; + } + + @Override + public final long getContentDuration() { + Timeline timeline = getCurrentTimeline(); + return timeline.isEmpty() + ? C.TIME_UNSET + : timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); + } + + @RepeatMode + private int getRepeatModeForNavigation() { + @RepeatMode int repeatMode = getRepeatMode(); + return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode; + } + + /** Holds a listener reference. */ + protected static final class ListenerHolder { + + /** + * The listener on which {link #invoke} will execute {@link ListenerInvocation listener + * invocations}. + */ + public final Player.EventListener listener; + + private boolean released; + + public ListenerHolder(Player.EventListener listener) { + this.listener = listener; + } + + /** Prevents any further {@link ListenerInvocation} to be executed on {@link #listener}. */ + public void release() { + released = true; + } + + /** + * Executes the given {@link ListenerInvocation} on {@link #listener}. Does nothing if {@link + * #release} has been called on this instance. + */ + public void invoke(ListenerInvocation listenerInvocation) { + if (!released) { + listenerInvocation.invokeListener(listener); + } + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + return listener.equals(((ListenerHolder) other).listener); + } + + @Override + public int hashCode() { + return listener.hashCode(); + } + } + + /** Parameterized invocation of a {@link Player.EventListener} method. */ + protected interface ListenerInvocation { + + /** Executes the invocation on the given {@link Player.EventListener}. */ + void invokeListener(Player.EventListener listener); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/BaseRenderer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/BaseRenderer.java index 730d04d5a2c7..9c2c2440534f 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/BaseRenderer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/BaseRenderer.java @@ -15,10 +15,17 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2; +import android.os.Looper; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaCrypto; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleStream; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MediaClock; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; /** @@ -27,14 +34,17 @@ import java.io.IOException; public abstract class BaseRenderer implements Renderer, RendererCapabilities { private final int trackType; + private final FormatHolder formatHolder; private RendererConfiguration configuration; private int index; private int state; private SampleStream stream; + private Format[] streamFormats; private long streamOffsetUs; - private boolean readEndOfStream; + private long readingPositionUs; private boolean streamIsFinal; + private boolean throwRendererExceptionIsExecuting; /** * @param trackType The track type that the renderer handles. One of the {@link C} @@ -42,7 +52,8 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { */ public BaseRenderer(int trackType) { this.trackType = trackType; - readEndOfStream = true; + formatHolder = new FormatHolder(); + readingPositionUs = C.TIME_END_OF_SOURCE; } @Override @@ -61,6 +72,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { } @Override + @Nullable public MediaClock getMediaClock() { return null; } @@ -94,19 +106,26 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { throws ExoPlaybackException { Assertions.checkState(!streamIsFinal); this.stream = stream; - readEndOfStream = false; + readingPositionUs = offsetUs; + streamFormats = formats; streamOffsetUs = offsetUs; - onStreamChanged(formats); + onStreamChanged(formats, offsetUs); } @Override + @Nullable public final SampleStream getStream() { return stream; } @Override public final boolean hasReadStreamToEnd() { - return readEndOfStream; + return readingPositionUs == C.TIME_END_OF_SOURCE; + } + + @Override + public final long getReadingPositionUs() { + return readingPositionUs; } @Override @@ -127,7 +146,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { @Override public final void resetPosition(long positionUs) throws ExoPlaybackException { streamIsFinal = false; - readEndOfStream = false; + readingPositionUs = positionUs; onPositionReset(positionUs, false); } @@ -141,23 +160,33 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { @Override public final void disable() { Assertions.checkState(state == STATE_ENABLED); + formatHolder.clear(); state = STATE_DISABLED; - onDisabled(); stream = null; + streamFormats = null; streamIsFinal = false; + onDisabled(); + } + + @Override + public final void reset() { + Assertions.checkState(state == STATE_DISABLED); + formatHolder.clear(); + onReset(); } // RendererCapabilities implementation. @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_NOT_SUPPORTED; } - // ExoPlayerComponent implementation. + // PlayerMessage.Target implementation. @Override - public void handleMessage(int what, Object object) throws ExoPlaybackException { + public void handleMessage(int what, @Nullable Object object) throws ExoPlaybackException { // Do nothing. } @@ -183,16 +212,19 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * The default implementation is a no-op. * * @param formats The enabled formats. + * @param offsetUs The offset that will be added to the timestamps of buffers read via + * {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)} so that decoder input + * buffers have monotonically increasing timestamps. * @throws ExoPlaybackException If an error occurs. */ - protected void onStreamChanged(Format[] formats) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { // Do nothing. } /** * Called when the position is reset. This occurs when the renderer is enabled after - * {@link #onStreamChanged(Format[])} has been called, and also when a position discontinuity - * is encountered. + * {@link #onStreamChanged(Format[], long)} has been called, and also when a position + * discontinuity is encountered. *

* After a position reset, the renderer's {@link SampleStream} is guaranteed to provide samples * starting from a key frame. @@ -238,8 +270,28 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { // Do nothing. } + /** + * Called when the renderer is reset. + * + *

The default implementation is a no-op. + */ + protected void onReset() { + // Do nothing. + } + // Methods to be called by subclasses. + /** Returns a clear {@link FormatHolder}. */ + protected final FormatHolder getFormatHolder() { + formatHolder.clear(); + return formatHolder; + } + + /** Returns the formats of the currently enabled stream. */ + protected final Format[] getStreamFormats() { + return streamFormats; + } + /** * Returns the configuration set when the renderer was most recently enabled. */ @@ -247,6 +299,35 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return configuration; } + /** Returns a {@link DrmSession} ready for assignment, handling resource management. */ + @Nullable + protected final DrmSession getUpdatedSourceDrmSession( + @Nullable Format oldFormat, + Format newFormat, + @Nullable DrmSessionManager drmSessionManager, + @Nullable DrmSession existingSourceSession) + throws ExoPlaybackException { + boolean drmInitDataChanged = + !Util.areEqual(newFormat.drmInitData, oldFormat == null ? null : oldFormat.drmInitData); + if (!drmInitDataChanged) { + return existingSourceSession; + } + @Nullable DrmSession newSourceDrmSession = null; + if (newFormat.drmInitData != null) { + if (drmSessionManager == null) { + throw createRendererException( + new IllegalStateException("Media requires a DrmSessionManager"), newFormat); + } + newSourceDrmSession = + drmSessionManager.acquireSession( + Assertions.checkNotNull(Looper.myLooper()), newFormat.drmInitData); + } + if (existingSourceSession != null) { + existingSourceSession.release(); + } + return newSourceDrmSession; + } + /** * Returns the index of the renderer within the player. */ @@ -254,6 +335,30 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return index; } + /** + * Creates an {@link ExoPlaybackException} of type {@link ExoPlaybackException#TYPE_RENDERER} for + * this renderer. + * + * @param cause The cause of the exception. + * @param format The current format used by the renderer. May be null. + */ + protected final ExoPlaybackException createRendererException( + Exception cause, @Nullable Format format) { + @FormatSupport int formatSupport = RendererCapabilities.FORMAT_HANDLED; + if (format != null && !throwRendererExceptionIsExecuting) { + // Prevent recursive re-entry from subclass supportsFormat implementations. + throwRendererExceptionIsExecuting = true; + try { + formatSupport = RendererCapabilities.getFormatSupport(supportsFormat(format)); + } catch (ExoPlaybackException e) { + // Ignore, we are already failing. + } finally { + throwRendererExceptionIsExecuting = false; + } + } + return ExoPlaybackException.createForRenderer(cause, getIndex(), format, formatSupport); + } + /** * Reads from the enabled upstream source. If the upstream source has been read to the end then * {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been @@ -261,23 +366,24 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the - * end of the stream. If the end of the stream has been reached, the - * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. + * end of the stream. If the end of the stream has been reached, the {@link + * C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. * @param formatRequired Whether the caller requires that the format of the stream be read even if * it's not changing. A sample will never be read if set to true, however it is still possible * for the end of stream or nothing to be read. * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * {@link C#RESULT_BUFFER_READ}. */ - protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer, - boolean formatRequired) { + protected final int readSource( + FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) { int result = stream.readData(formatHolder, buffer, formatRequired); if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { - readEndOfStream = true; + readingPositionUs = C.TIME_END_OF_SOURCE; return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ; } buffer.timeUs += streamOffsetUs; + readingPositionUs = Math.max(readingPositionUs, buffer.timeUs); } else if (result == C.RESULT_FORMAT_READ) { Format format = formatHolder.format; if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) { @@ -293,18 +399,38 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * {@code positionUs} is beyond it. * * @param positionUs The position in microseconds. + * @return The number of samples that were skipped. */ - protected void skipSource(long positionUs) { - stream.skipData(positionUs - streamOffsetUs); + protected int skipSource(long positionUs) { + return stream.skipData(positionUs - streamOffsetUs); } /** * Returns whether the upstream source is ready. - * - * @return Whether the source is ready. */ protected final boolean isSourceReady() { - return readEndOfStream ? streamIsFinal : stream.isReady(); + return hasReadStreamToEnd() ? streamIsFinal : stream.isReady(); + } + + /** + * Returns whether {@code drmSessionManager} supports the specified {@code drmInitData}, or true + * if {@code drmInitData} is null. + * + * @param drmSessionManager The drm session manager. + * @param drmInitData {@link DrmInitData} of the format to check for support. + * @return Whether {@code drmSessionManager} supports the specified {@code drmInitData}, or + * true if {@code drmInitData} is null. + */ + protected static boolean supportsFormatDrm(@Nullable DrmSessionManager drmSessionManager, + @Nullable DrmInitData drmInitData) { + if (drmInitData == null) { + // Content is unencrypted. + return true; + } else if (drmSessionManager == null) { + // Content is encrypted, but no drm session manager is available. + return false; + } + return drmSessionManager.canAcquireSession(drmInitData); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/C.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/C.java index b268a43c8a3d..673c3d90a858 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/C.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/C.java @@ -17,13 +17,21 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2; import android.annotation.TargetApi; import android.content.Context; +import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioManager; import android.media.MediaCodec; import android.media.MediaFormat; -import android.support.annotation.IntDef; import android.view.Surface; +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.PlayerMessage.Target; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AuxEffectInfo; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoFrameMetadataListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.spherical.CameraMotionListener; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.UUID; @@ -31,6 +39,7 @@ import java.util.UUID; /** * Defines constants used by the library. */ +@SuppressWarnings("InlinedApi") public final class C { private C() {} @@ -62,9 +71,13 @@ public final class C { */ public static final int LENGTH_UNSET = -1; - /** - * The number of microseconds in one second. - */ + /** Represents an unset or unknown percentage. */ + public static final int PERCENTAGE_UNSET = -1; + + /** The number of milliseconds in one second. */ + public static final long MILLIS_PER_SECOND = 1000L; + + /** The number of microseconds in one second. */ public static final long MICROS_PER_SECOND = 1000000L; /** @@ -72,130 +85,175 @@ public final class C { */ public static final long NANOS_PER_SECOND = 1000000000L; + /** The number of bits per byte. */ + public static final int BITS_PER_BYTE = 8; + + /** The number of bytes per float. */ + public static final int BYTES_PER_FLOAT = 4; + + /** + * The name of the ASCII charset. + */ + public static final String ASCII_NAME = "US-ASCII"; + /** * The name of the UTF-8 charset. */ public static final String UTF8_NAME = "UTF-8"; - /** - * The name of the UTF-16 charset. - */ + /** The name of the ISO-8859-1 charset. */ + public static final String ISO88591_NAME = "ISO-8859-1"; + + /** The name of the UTF-16 charset. */ public static final String UTF16_NAME = "UTF-16"; + /** The name of the UTF-16 little-endian charset. */ + public static final String UTF16LE_NAME = "UTF-16LE"; + /** - * * The name of the serif font family. + * The name of the serif font family. */ public static final String SERIF_NAME = "serif"; /** - * * The name of the sans-serif font family. + * The name of the sans-serif font family. */ public static final String SANS_SERIF_NAME = "sans-serif"; /** - * Crypto modes for a codec. + * Crypto modes for a codec. One of {@link #CRYPTO_MODE_UNENCRYPTED}, {@link #CRYPTO_MODE_AES_CTR} + * or {@link #CRYPTO_MODE_AES_CBC}. */ + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({CRYPTO_MODE_UNENCRYPTED, CRYPTO_MODE_AES_CTR, CRYPTO_MODE_AES_CBC}) public @interface CryptoMode {} /** * @see MediaCodec#CRYPTO_MODE_UNENCRYPTED */ - @SuppressWarnings("InlinedApi") public static final int CRYPTO_MODE_UNENCRYPTED = MediaCodec.CRYPTO_MODE_UNENCRYPTED; /** * @see MediaCodec#CRYPTO_MODE_AES_CTR */ - @SuppressWarnings("InlinedApi") public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR; /** * @see MediaCodec#CRYPTO_MODE_AES_CBC */ - @SuppressWarnings("InlinedApi") - public static final int CRYPTO_MODE_AES_CBC = 0x2; + public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC; /** * Represents an unset {@link android.media.AudioTrack} session identifier. Equal to * {@link AudioManager#AUDIO_SESSION_ID_GENERATE}. */ - @SuppressWarnings("InlinedApi") public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE; /** - * Represents an audio encoding, or an invalid or unset value. + * Represents an audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, + * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link + * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, + * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_MP3}, {@link #ENCODING_AC3}, {@link + * #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, + * {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, - ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_AC3, ENCODING_E_AC3, ENCODING_DTS, - ENCODING_DTS_HD}) + @IntDef({ + Format.NO_VALUE, + ENCODING_INVALID, + ENCODING_PCM_8BIT, + ENCODING_PCM_16BIT, + ENCODING_PCM_16BIT_BIG_ENDIAN, + ENCODING_PCM_24BIT, + ENCODING_PCM_32BIT, + ENCODING_PCM_FLOAT, + ENCODING_MP3, + ENCODING_AC3, + ENCODING_E_AC3, + ENCODING_E_AC3_JOC, + ENCODING_AC4, + ENCODING_DTS, + ENCODING_DTS_HD, + ENCODING_DOLBY_TRUEHD + }) public @interface Encoding {} /** - * Represents a PCM audio encoding, or an invalid or unset value. + * Represents a PCM audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, + * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link + * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, + * {@link #ENCODING_PCM_FLOAT}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, - ENCODING_PCM_24BIT, ENCODING_PCM_32BIT}) + @IntDef({ + Format.NO_VALUE, + ENCODING_INVALID, + ENCODING_PCM_8BIT, + ENCODING_PCM_16BIT, + ENCODING_PCM_16BIT_BIG_ENDIAN, + ENCODING_PCM_24BIT, + ENCODING_PCM_32BIT, + ENCODING_PCM_FLOAT + }) public @interface PcmEncoding {} - /** - * @see AudioFormat#ENCODING_INVALID - */ + /** @see AudioFormat#ENCODING_INVALID */ public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID; - /** - * @see AudioFormat#ENCODING_PCM_8BIT - */ + /** @see AudioFormat#ENCODING_PCM_8BIT */ public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT; - /** - * @see AudioFormat#ENCODING_PCM_16BIT - */ + /** @see AudioFormat#ENCODING_PCM_16BIT */ public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT; - /** - * PCM encoding with 24 bits per sample. - */ - public static final int ENCODING_PCM_24BIT = 0x80000000; - /** - * PCM encoding with 32 bits per sample. - */ - public static final int ENCODING_PCM_32BIT = 0x40000000; - /** - * @see AudioFormat#ENCODING_AC3 - */ - @SuppressWarnings("InlinedApi") + /** Like {@link #ENCODING_PCM_16BIT}, but with the bytes in big endian order. */ + public static final int ENCODING_PCM_16BIT_BIG_ENDIAN = 0x10000000; + /** PCM encoding with 24 bits per sample. */ + public static final int ENCODING_PCM_24BIT = 0x20000000; + /** PCM encoding with 32 bits per sample. */ + public static final int ENCODING_PCM_32BIT = 0x30000000; + /** @see AudioFormat#ENCODING_PCM_FLOAT */ + public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT; + /** @see AudioFormat#ENCODING_MP3 */ + public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3; + /** @see AudioFormat#ENCODING_AC3 */ public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; - /** - * @see AudioFormat#ENCODING_E_AC3 - */ - @SuppressWarnings("InlinedApi") + /** @see AudioFormat#ENCODING_E_AC3 */ public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3; - /** - * @see AudioFormat#ENCODING_DTS - */ - @SuppressWarnings("InlinedApi") + /** @see AudioFormat#ENCODING_E_AC3_JOC */ + public static final int ENCODING_E_AC3_JOC = AudioFormat.ENCODING_E_AC3_JOC; + /** @see AudioFormat#ENCODING_AC4 */ + public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4; + /** @see AudioFormat#ENCODING_DTS */ public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS; - /** - * @see AudioFormat#ENCODING_DTS_HD - */ - @SuppressWarnings("InlinedApi") + /** @see AudioFormat#ENCODING_DTS_HD */ public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD; + /** @see AudioFormat#ENCODING_DOLBY_TRUEHD */ + public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD; /** - * @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND - */ - @SuppressWarnings({"InlinedApi", "deprecation"}) - public static final int CHANNEL_OUT_7POINT1_SURROUND = Util.SDK_INT < 23 - ? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; - - /** - * Stream types for an {@link android.media.AudioTrack}. + * Stream types for an {@link android.media.AudioTrack}. One of {@link #STREAM_TYPE_ALARM}, {@link + * #STREAM_TYPE_DTMF}, {@link #STREAM_TYPE_MUSIC}, {@link #STREAM_TYPE_NOTIFICATION}, {@link + * #STREAM_TYPE_RING}, {@link #STREAM_TYPE_SYSTEM}, {@link #STREAM_TYPE_VOICE_CALL} or {@link + * #STREAM_TYPE_USE_DEFAULT}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({STREAM_TYPE_ALARM, STREAM_TYPE_MUSIC, STREAM_TYPE_NOTIFICATION, STREAM_TYPE_RING, - STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL}) + @IntDef({ + STREAM_TYPE_ALARM, + STREAM_TYPE_DTMF, + STREAM_TYPE_MUSIC, + STREAM_TYPE_NOTIFICATION, + STREAM_TYPE_RING, + STREAM_TYPE_SYSTEM, + STREAM_TYPE_VOICE_CALL, + STREAM_TYPE_USE_DEFAULT + }) public @interface StreamType {} /** * @see AudioManager#STREAM_ALARM */ public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM; + /** + * @see AudioManager#STREAM_DTMF + */ + public static final int STREAM_TYPE_DTMF = AudioManager.STREAM_DTMF; /** * @see AudioManager#STREAM_MUSIC */ @@ -216,53 +274,290 @@ public final class C { * @see AudioManager#STREAM_VOICE_CALL */ public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL; + /** + * @see AudioManager#USE_DEFAULT_STREAM_TYPE + */ + public static final int STREAM_TYPE_USE_DEFAULT = AudioManager.USE_DEFAULT_STREAM_TYPE; /** * The default stream type used by audio renderers. */ public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC; /** - * Flags which can apply to a buffer containing a media sample. + * Content types for {@link org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioAttributes}. One of {@link + * #CONTENT_TYPE_MOVIE}, {@link #CONTENT_TYPE_MUSIC}, {@link #CONTENT_TYPE_SONIFICATION}, {@link + * #CONTENT_TYPE_SPEECH} or {@link #CONTENT_TYPE_UNKNOWN}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {BUFFER_FLAG_KEY_FRAME, BUFFER_FLAG_END_OF_STREAM, - BUFFER_FLAG_ENCRYPTED, BUFFER_FLAG_DECODE_ONLY}) + @IntDef({ + CONTENT_TYPE_MOVIE, + CONTENT_TYPE_MUSIC, + CONTENT_TYPE_SONIFICATION, + CONTENT_TYPE_SPEECH, + CONTENT_TYPE_UNKNOWN + }) + public @interface AudioContentType {} + /** + * @see android.media.AudioAttributes#CONTENT_TYPE_MOVIE + */ + public static final int CONTENT_TYPE_MOVIE = android.media.AudioAttributes.CONTENT_TYPE_MOVIE; + /** + * @see android.media.AudioAttributes#CONTENT_TYPE_MUSIC + */ + public static final int CONTENT_TYPE_MUSIC = android.media.AudioAttributes.CONTENT_TYPE_MUSIC; + /** + * @see android.media.AudioAttributes#CONTENT_TYPE_SONIFICATION + */ + public static final int CONTENT_TYPE_SONIFICATION = + android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION; + /** + * @see android.media.AudioAttributes#CONTENT_TYPE_SPEECH + */ + public static final int CONTENT_TYPE_SPEECH = + android.media.AudioAttributes.CONTENT_TYPE_SPEECH; + /** + * @see android.media.AudioAttributes#CONTENT_TYPE_UNKNOWN + */ + public static final int CONTENT_TYPE_UNKNOWN = + android.media.AudioAttributes.CONTENT_TYPE_UNKNOWN; + + /** + * Flags for {@link org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioAttributes}. Possible flag value is + * {@link #FLAG_AUDIBILITY_ENFORCED}. + * + *

Note that {@code FLAG_HW_AV_SYNC} is not available because the player takes care of setting + * the flag when tunneling is enabled via a track selector. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_AUDIBILITY_ENFORCED}) + public @interface AudioFlags {} + /** + * @see android.media.AudioAttributes#FLAG_AUDIBILITY_ENFORCED + */ + public static final int FLAG_AUDIBILITY_ENFORCED = + android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED; + + /** + * Usage types for {@link org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioAttributes}. One of {@link + * #USAGE_ALARM}, {@link #USAGE_ASSISTANCE_ACCESSIBILITY}, {@link + * #USAGE_ASSISTANCE_NAVIGATION_GUIDANCE}, {@link #USAGE_ASSISTANCE_SONIFICATION}, {@link + * #USAGE_ASSISTANT}, {@link #USAGE_GAME}, {@link #USAGE_MEDIA}, {@link #USAGE_NOTIFICATION}, + * {@link #USAGE_NOTIFICATION_COMMUNICATION_DELAYED}, {@link + * #USAGE_NOTIFICATION_COMMUNICATION_INSTANT}, {@link #USAGE_NOTIFICATION_COMMUNICATION_REQUEST}, + * {@link #USAGE_NOTIFICATION_EVENT}, {@link #USAGE_NOTIFICATION_RINGTONE}, {@link + * #USAGE_UNKNOWN}, {@link #USAGE_VOICE_COMMUNICATION} or {@link + * #USAGE_VOICE_COMMUNICATION_SIGNALLING}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + USAGE_ALARM, + USAGE_ASSISTANCE_ACCESSIBILITY, + USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, + USAGE_ASSISTANCE_SONIFICATION, + USAGE_ASSISTANT, + USAGE_GAME, + USAGE_MEDIA, + USAGE_NOTIFICATION, + USAGE_NOTIFICATION_COMMUNICATION_DELAYED, + USAGE_NOTIFICATION_COMMUNICATION_INSTANT, + USAGE_NOTIFICATION_COMMUNICATION_REQUEST, + USAGE_NOTIFICATION_EVENT, + USAGE_NOTIFICATION_RINGTONE, + USAGE_UNKNOWN, + USAGE_VOICE_COMMUNICATION, + USAGE_VOICE_COMMUNICATION_SIGNALLING + }) + public @interface AudioUsage {} + /** + * @see android.media.AudioAttributes#USAGE_ALARM + */ + public static final int USAGE_ALARM = android.media.AudioAttributes.USAGE_ALARM; + /** @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY */ + public static final int USAGE_ASSISTANCE_ACCESSIBILITY = + android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY; + /** + * @see android.media.AudioAttributes#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE + */ + public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE = + android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE; + /** + * @see android.media.AudioAttributes#USAGE_ASSISTANCE_SONIFICATION + */ + public static final int USAGE_ASSISTANCE_SONIFICATION = + android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION; + /** @see android.media.AudioAttributes#USAGE_ASSISTANT */ + public static final int USAGE_ASSISTANT = android.media.AudioAttributes.USAGE_ASSISTANT; + /** + * @see android.media.AudioAttributes#USAGE_GAME + */ + public static final int USAGE_GAME = android.media.AudioAttributes.USAGE_GAME; + /** + * @see android.media.AudioAttributes#USAGE_MEDIA + */ + public static final int USAGE_MEDIA = android.media.AudioAttributes.USAGE_MEDIA; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION + */ + public static final int USAGE_NOTIFICATION = android.media.AudioAttributes.USAGE_NOTIFICATION; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_DELAYED + */ + public static final int USAGE_NOTIFICATION_COMMUNICATION_DELAYED = + android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_INSTANT + */ + public static final int USAGE_NOTIFICATION_COMMUNICATION_INSTANT = + android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_REQUEST + */ + public static final int USAGE_NOTIFICATION_COMMUNICATION_REQUEST = + android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION_EVENT + */ + public static final int USAGE_NOTIFICATION_EVENT = + android.media.AudioAttributes.USAGE_NOTIFICATION_EVENT; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION_RINGTONE + */ + public static final int USAGE_NOTIFICATION_RINGTONE = + android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; + /** + * @see android.media.AudioAttributes#USAGE_UNKNOWN + */ + public static final int USAGE_UNKNOWN = android.media.AudioAttributes.USAGE_UNKNOWN; + /** + * @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION + */ + public static final int USAGE_VOICE_COMMUNICATION = + android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION; + /** + * @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION_SIGNALLING + */ + public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING = + android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING; + + /** + * Capture policies for {@link org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioAttributes}. One of {@link + * #ALLOW_CAPTURE_BY_ALL}, {@link #ALLOW_CAPTURE_BY_NONE} or {@link #ALLOW_CAPTURE_BY_SYSTEM}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ALLOW_CAPTURE_BY_ALL, ALLOW_CAPTURE_BY_NONE, ALLOW_CAPTURE_BY_SYSTEM}) + public @interface AudioAllowedCapturePolicy {} + /** See {@link android.media.AudioAttributes#ALLOW_CAPTURE_BY_ALL}. */ + public static final int ALLOW_CAPTURE_BY_ALL = AudioAttributes.ALLOW_CAPTURE_BY_ALL; + /** See {@link android.media.AudioAttributes#ALLOW_CAPTURE_BY_NONE}. */ + public static final int ALLOW_CAPTURE_BY_NONE = AudioAttributes.ALLOW_CAPTURE_BY_NONE; + /** See {@link android.media.AudioAttributes#ALLOW_CAPTURE_BY_SYSTEM}. */ + public static final int ALLOW_CAPTURE_BY_SYSTEM = AudioAttributes.ALLOW_CAPTURE_BY_SYSTEM; + + /** + * Audio focus types. One of {@link #AUDIOFOCUS_NONE}, {@link #AUDIOFOCUS_GAIN}, {@link + * #AUDIOFOCUS_GAIN_TRANSIENT}, {@link #AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK} or {@link + * #AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + AUDIOFOCUS_NONE, + AUDIOFOCUS_GAIN, + AUDIOFOCUS_GAIN_TRANSIENT, + AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, + AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE + }) + public @interface AudioFocusGain {} + /** @see AudioManager#AUDIOFOCUS_NONE */ + public static final int AUDIOFOCUS_NONE = AudioManager.AUDIOFOCUS_NONE; + /** @see AudioManager#AUDIOFOCUS_GAIN */ + public static final int AUDIOFOCUS_GAIN = AudioManager.AUDIOFOCUS_GAIN; + /** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT */ + public static final int AUDIOFOCUS_GAIN_TRANSIENT = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT; + /** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK */ + public static final int AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK = + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK; + /** @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE */ + public static final int AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE = + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE; + + /** + * Flags which can apply to a buffer containing a media sample. Possible flag values are {@link + * #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_LAST_SAMPLE}, + * {@link #BUFFER_FLAG_ENCRYPTED} and {@link #BUFFER_FLAG_DECODE_ONLY}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + BUFFER_FLAG_KEY_FRAME, + BUFFER_FLAG_END_OF_STREAM, + BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA, + BUFFER_FLAG_LAST_SAMPLE, + BUFFER_FLAG_ENCRYPTED, + BUFFER_FLAG_DECODE_ONLY + }) public @interface BufferFlags {} /** * Indicates that a buffer holds a synchronization sample. */ - @SuppressWarnings("InlinedApi") public static final int BUFFER_FLAG_KEY_FRAME = MediaCodec.BUFFER_FLAG_KEY_FRAME; /** * Flag for empty buffers that signal that the end of the stream was reached. */ - @SuppressWarnings("InlinedApi") public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM; + /** Indicates that a buffer has supplemental data. */ + public static final int BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA = 1 << 28; // 0x10000000 + /** Indicates that a buffer is known to contain the last media sample of the stream. */ + public static final int BUFFER_FLAG_LAST_SAMPLE = 1 << 29; // 0x20000000 + /** Indicates that a buffer is (at least partially) encrypted. */ + public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000 + /** Indicates that a buffer should be decoded but not rendered. */ + public static final int BUFFER_FLAG_DECODE_ONLY = 1 << 31; // 0x80000000 + + // LINT.IfChange /** - * Indicates that a buffer is (at least partially) encrypted. + * Video decoder output modes. Possible modes are {@link #VIDEO_OUTPUT_MODE_NONE}, {@link + * #VIDEO_OUTPUT_MODE_YUV} and {@link #VIDEO_OUTPUT_MODE_SURFACE_YUV}. */ - public static final int BUFFER_FLAG_ENCRYPTED = 0x40000000; - /** - * Indicates that a buffer should be decoded but not rendered. - */ - public static final int BUFFER_FLAG_DECODE_ONLY = 0x80000000; + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {VIDEO_OUTPUT_MODE_NONE, VIDEO_OUTPUT_MODE_YUV, VIDEO_OUTPUT_MODE_SURFACE_YUV}) + public @interface VideoOutputMode {} + /** Video decoder output mode is not set. */ + public static final int VIDEO_OUTPUT_MODE_NONE = -1; + /** Video decoder output mode that outputs raw 4:2:0 YUV planes. */ + public static final int VIDEO_OUTPUT_MODE_YUV = 0; + /** Video decoder output mode that renders 4:2:0 YUV planes directly to a surface. */ + public static final int VIDEO_OUTPUT_MODE_SURFACE_YUV = 1; + // LINT.ThenChange( + // ../../../../../../../../../extensions/av1/src/main/jni/gav1_jni.cc, + // ../../../../../../../../../extensions/vp9/src/main/jni/vpx_jni.cc + // ) /** - * Video scaling modes for {@link MediaCodec}-based {@link Renderer}s. + * Video scaling modes for {@link MediaCodec}-based {@link Renderer}s. One of {@link + * #VIDEO_SCALING_MODE_SCALE_TO_FIT} or {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}. */ + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef(value = {VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}) public @interface VideoScalingMode {} /** * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT */ - @SuppressWarnings("InlinedApi") public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT = MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT; /** * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT */ - @SuppressWarnings("InlinedApi") public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING = MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING; /** @@ -271,29 +566,35 @@ public final class C { public static final int VIDEO_SCALING_MODE_DEFAULT = VIDEO_SCALING_MODE_SCALE_TO_FIT; /** - * Track selection flags. + * Track selection flags. Possible flag values are {@link #SELECTION_FLAG_DEFAULT}, {@link + * #SELECTION_FLAG_FORCED} and {@link #SELECTION_FLAG_AUTOSELECT}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {SELECTION_FLAG_DEFAULT, SELECTION_FLAG_FORCED, - SELECTION_FLAG_AUTOSELECT}) + @IntDef( + flag = true, + value = {SELECTION_FLAG_DEFAULT, SELECTION_FLAG_FORCED, SELECTION_FLAG_AUTOSELECT}) public @interface SelectionFlags {} /** * Indicates that the track should be selected if user preferences do not state otherwise. */ public static final int SELECTION_FLAG_DEFAULT = 1; - /** - * Indicates that the track must be displayed. Only applies to text tracks. - */ - public static final int SELECTION_FLAG_FORCED = 2; + /** Indicates that the track must be displayed. Only applies to text tracks. */ + public static final int SELECTION_FLAG_FORCED = 1 << 1; // 2 /** * Indicates that the player may choose to play the track in absence of an explicit user * preference. */ - public static final int SELECTION_FLAG_AUTOSELECT = 4; + public static final int SELECTION_FLAG_AUTOSELECT = 1 << 2; // 4 + + /** Represents an undetermined language as an ISO 639-2 language code. */ + public static final String LANGUAGE_UNDETERMINED = "und"; /** - * Represents a streaming or other media type. + * Represents a streaming or other media type. One of {@link #TYPE_DASH}, {@link #TYPE_SS}, {@link + * #TYPE_HLS} or {@link #TYPE_OTHER}. */ + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_OTHER}) public @interface ContentType {} @@ -336,60 +637,46 @@ public final class C { */ public static final int RESULT_FORMAT_READ = -5; - /** - * A data type constant for data of unknown or unspecified type. - */ + /** A data type constant for data of unknown or unspecified type. */ public static final int DATA_TYPE_UNKNOWN = 0; - /** - * A data type constant for media, typically containing media samples. - */ + /** A data type constant for media, typically containing media samples. */ public static final int DATA_TYPE_MEDIA = 1; - /** - * A data type constant for media, typically containing only initialization data. - */ + /** A data type constant for media, typically containing only initialization data. */ public static final int DATA_TYPE_MEDIA_INITIALIZATION = 2; - /** - * A data type constant for drm or encryption data. - */ + /** A data type constant for drm or encryption data. */ public static final int DATA_TYPE_DRM = 3; - /** - * A data type constant for a manifest file. - */ + /** A data type constant for a manifest file. */ public static final int DATA_TYPE_MANIFEST = 4; - /** - * A data type constant for time synchronization data. - */ + /** A data type constant for time synchronization data. */ public static final int DATA_TYPE_TIME_SYNCHRONIZATION = 5; + /** A data type constant for ads loader data. */ + public static final int DATA_TYPE_AD = 6; + /** + * A data type constant for live progressive media streams, typically containing media samples. + */ + public static final int DATA_TYPE_MEDIA_PROGRESSIVE_LIVE = 7; /** * Applications or extensions may define custom {@code DATA_TYPE_*} constants greater than or * equal to this value. */ public static final int DATA_TYPE_CUSTOM_BASE = 10000; - /** - * A type constant for tracks of unknown type. - */ + /** A type constant for tracks of unknown type. */ public static final int TRACK_TYPE_UNKNOWN = -1; - /** - * A type constant for tracks of some default type, where the type itself is unknown. - */ + /** A type constant for tracks of some default type, where the type itself is unknown. */ public static final int TRACK_TYPE_DEFAULT = 0; - /** - * A type constant for audio tracks. - */ + /** A type constant for audio tracks. */ public static final int TRACK_TYPE_AUDIO = 1; - /** - * A type constant for video tracks. - */ + /** A type constant for video tracks. */ public static final int TRACK_TYPE_VIDEO = 2; - /** - * A type constant for text tracks. - */ + /** A type constant for text tracks. */ public static final int TRACK_TYPE_TEXT = 3; - /** - * A type constant for metadata tracks. - */ + /** A type constant for metadata tracks. */ public static final int TRACK_TYPE_METADATA = 4; + /** A type constant for camera motion tracks. */ + public static final int TRACK_TYPE_CAMERA_MOTION = 5; + /** A type constant for a dummy or empty track. */ + public static final int TRACK_TYPE_NONE = 6; /** * Applications or extensions may define custom {@code TRACK_TYPE_*} constants greater than or * equal to this value. @@ -422,36 +709,24 @@ public final class C { */ public static final int SELECTION_REASON_CUSTOM_BASE = 10000; - /** - * A default size in bytes for an individual allocation that forms part of a larger buffer. - */ + /** A default size in bytes for an individual allocation that forms part of a larger buffer. */ public static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024; - /** - * A default size in bytes for a video buffer. - */ - public static final int DEFAULT_VIDEO_BUFFER_SIZE = 200 * DEFAULT_BUFFER_SEGMENT_SIZE; + /** "cenc" scheme type name as defined in ISO/IEC 23001-7:2016. */ + @SuppressWarnings("ConstantField") + public static final String CENC_TYPE_cenc = "cenc"; - /** - * A default size in bytes for an audio buffer. - */ - public static final int DEFAULT_AUDIO_BUFFER_SIZE = 54 * DEFAULT_BUFFER_SEGMENT_SIZE; + /** "cbc1" scheme type name as defined in ISO/IEC 23001-7:2016. */ + @SuppressWarnings("ConstantField") + public static final String CENC_TYPE_cbc1 = "cbc1"; - /** - * A default size in bytes for a text buffer. - */ - public static final int DEFAULT_TEXT_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE; + /** "cens" scheme type name as defined in ISO/IEC 23001-7:2016. */ + @SuppressWarnings("ConstantField") + public static final String CENC_TYPE_cens = "cens"; - /** - * A default size in bytes for a metadata buffer. - */ - public static final int DEFAULT_METADATA_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE; - - /** - * A default size in bytes for a muxed buffer (e.g. containing video, audio and text). - */ - public static final int DEFAULT_MUXED_BUFFER_SIZE = DEFAULT_VIDEO_BUFFER_SIZE - + DEFAULT_AUDIO_BUFFER_SIZE + DEFAULT_TEXT_BUFFER_SIZE; + /** "cbcs" scheme type name as defined in ISO/IEC 23001-7:2016. */ + @SuppressWarnings("ConstantField") + public static final String CENC_TYPE_cbcs = "cbcs"; /** * The Nil UUID as defined by @@ -459,12 +734,19 @@ public final class C { */ public static final UUID UUID_NIL = new UUID(0L, 0L); + /** + * UUID for the W3C + * Common PSSH + * box. + */ + public static final UUID COMMON_PSSH_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL); + /** * UUID for the ClearKey DRM scheme. *

* ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up. */ - public static final UUID CLEARKEY_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL); + public static final UUID CLEARKEY_UUID = new UUID(0xE2719D58A985B3C9L, 0x781AB030AF78D30EL); /** * UUID for the Widevine DRM scheme. @@ -482,59 +764,103 @@ public final class C { public static final UUID PLAYREADY_UUID = new UUID(0x9A04F07998404286L, 0xAB92E65BE0885F95L); /** - * The type of a message that can be passed to a video {@link Renderer} via - * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object - * should be the target {@link Surface}, or null. + * The type of a message that can be passed to a video {@link Renderer} via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be the target {@link Surface}, or + * null. */ public static final int MSG_SET_SURFACE = 1; /** - * A type of a message that can be passed to an audio {@link Renderer} via - * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object - * should be a {@link Float} with 0 being silence and 1 being unity gain. + * A type of a message that can be passed to an audio {@link Renderer} via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be a {@link Float} with 0 being + * silence and 1 being unity gain. */ public static final int MSG_SET_VOLUME = 2; /** - * A type of a message that can be passed to an audio {@link Renderer} via - * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object - * should be one of the integer stream types in {@link C.StreamType}, and will specify the stream - * type of the underlying {@link android.media.AudioTrack}. See also - * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}. If the stream type - * is not set, audio renderers use {@link #STREAM_TYPE_DEFAULT}. - *

- * Note that when the stream type changes, the AudioTrack must be reinitialized, which can - * introduce a brief gap in audio output. Note also that tracks in the same audio session must - * share the same routing, so a new audio session id will be generated. + * A type of a message that can be passed to an audio {@link Renderer} via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be an {@link + * org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioAttributes} instance that will configure the + * underlying audio track. If not set, the default audio attributes will be used. They are + * suitable for general media playback. + * + *

Setting the audio attributes during playback may introduce a short gap in audio output as + * the audio track is recreated. A new audio session id will also be generated. + * + *

If tunneling is enabled by the track selector, the specified audio attributes will be + * ignored, but they will take effect if audio is later played without tunneling. + * + *

If the device is running a build before platform API version 21, audio attributes cannot be + * set directly on the underlying audio track. In this case, the usage will be mapped onto an + * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}. + * + *

To get audio attributes that are equivalent to a legacy stream type, pass the stream type to + * {@link Util#getAudioUsageForStreamType(int)} and use the returned {@link C.AudioUsage} to build + * an audio attributes instance. */ - public static final int MSG_SET_STREAM_TYPE = 3; + public static final int MSG_SET_AUDIO_ATTRIBUTES = 3; /** * The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer} - * via {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message - * object should be one of the integer scaling modes in {@link C.VideoScalingMode}. - *

- * Note that the scaling mode only applies if the {@link Surface} targeted by the renderer is + * via {@link ExoPlayer#createMessage(Target)}. The message payload should be one of the integer + * scaling modes in {@link C.VideoScalingMode}. + * + *

Note that the scaling mode only applies if the {@link Surface} targeted by the renderer is * owned by a {@link android.view.SurfaceView}. */ public static final int MSG_SET_SCALING_MODE = 4; /** - * Applications or extensions may define custom {@code MSG_*} constants greater than or equal to - * this value. + * A type of a message that can be passed to an audio {@link Renderer} via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be an {@link AuxEffectInfo} + * instance representing an auxiliary audio effect for the underlying audio track. + */ + public static final int MSG_SET_AUX_EFFECT_INFO = 5; + + /** + * The type of a message that can be passed to a video {@link Renderer} via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be a {@link + * VideoFrameMetadataListener} instance, or null. + */ + public static final int MSG_SET_VIDEO_FRAME_METADATA_LISTENER = 6; + + /** + * The type of a message that can be passed to a camera motion {@link Renderer} via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be a {@link CameraMotionListener} + * instance, or null. + */ + public static final int MSG_SET_CAMERA_MOTION_LISTENER = 7; + + /** + * The type of a message that can be passed to a {@link SimpleDecoderVideoRenderer} via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be the target {@link + * VideoDecoderOutputBufferRenderer}, or null. + * + *

This message is intended only for use with extension renderers that expect a {@link + * VideoDecoderOutputBufferRenderer}. For other use cases, an output surface should be passed via + * {@link #MSG_SET_SURFACE} instead. + */ + public static final int MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER = 8; + + /** + * Applications or extensions may define custom {@code MSG_*} constants that can be passed to + * {@link Renderer}s. These custom constants must be greater than or equal to this value. */ public static final int MSG_CUSTOM_BASE = 10000; /** - * The stereo mode for 360/3D/VR videos. + * The stereo mode for 360/3D/VR videos. One of {@link Format#NO_VALUE}, {@link + * #STEREO_MODE_MONO}, {@link #STEREO_MODE_TOP_BOTTOM}, {@link #STEREO_MODE_LEFT_RIGHT} or {@link + * #STEREO_MODE_STEREO_MESH}. */ + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({ - Format.NO_VALUE, - STEREO_MODE_MONO, - STEREO_MODE_TOP_BOTTOM, - STEREO_MODE_LEFT_RIGHT, - STEREO_MODE_STEREO_MESH + Format.NO_VALUE, + STEREO_MODE_MONO, + STEREO_MODE_TOP_BOTTOM, + STEREO_MODE_LEFT_RIGHT, + STEREO_MODE_STEREO_MESH }) public @interface StereoMode {} /** @@ -556,65 +882,83 @@ public final class C { public static final int STEREO_MODE_STEREO_MESH = 3; /** - * Video colorspaces. + * Video colorspaces. One of {@link Format#NO_VALUE}, {@link #COLOR_SPACE_BT709}, {@link + * #COLOR_SPACE_BT601} or {@link #COLOR_SPACE_BT2020}. */ + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({Format.NO_VALUE, COLOR_SPACE_BT709, COLOR_SPACE_BT601, COLOR_SPACE_BT2020}) public @interface ColorSpace {} /** * @see MediaFormat#COLOR_STANDARD_BT709 */ - @SuppressWarnings("InlinedApi") - public static final int COLOR_SPACE_BT709 = 0x01; + public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709; /** * @see MediaFormat#COLOR_STANDARD_BT601_PAL */ - @SuppressWarnings("InlinedApi") - public static final int COLOR_SPACE_BT601 = 0x02; + public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL; /** * @see MediaFormat#COLOR_STANDARD_BT2020 */ - @SuppressWarnings("InlinedApi") - public static final int COLOR_SPACE_BT2020 = 0x06; + public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020; /** - * Video color transfer characteristics. + * Video color transfer characteristics. One of {@link Format#NO_VALUE}, {@link + * #COLOR_TRANSFER_SDR}, {@link #COLOR_TRANSFER_ST2084} or {@link #COLOR_TRANSFER_HLG}. */ + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({Format.NO_VALUE, COLOR_TRANSFER_SDR, COLOR_TRANSFER_ST2084, COLOR_TRANSFER_HLG}) public @interface ColorTransfer {} /** * @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO */ - @SuppressWarnings("InlinedApi") - public static final int COLOR_TRANSFER_SDR = 0x03; + public static final int COLOR_TRANSFER_SDR = MediaFormat.COLOR_TRANSFER_SDR_VIDEO; /** * @see MediaFormat#COLOR_TRANSFER_ST2084 */ - @SuppressWarnings("InlinedApi") - public static final int COLOR_TRANSFER_ST2084 = 0x06; + public static final int COLOR_TRANSFER_ST2084 = MediaFormat.COLOR_TRANSFER_ST2084; /** * @see MediaFormat#COLOR_TRANSFER_HLG */ - @SuppressWarnings("InlinedApi") - public static final int COLOR_TRANSFER_HLG = 0x07; + public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG; /** - * Video color range. + * Video color range. One of {@link Format#NO_VALUE}, {@link #COLOR_RANGE_LIMITED} or {@link + * #COLOR_RANGE_FULL}. */ + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({Format.NO_VALUE, COLOR_RANGE_LIMITED, COLOR_RANGE_FULL}) public @interface ColorRange {} /** * @see MediaFormat#COLOR_RANGE_LIMITED */ - @SuppressWarnings("InlinedApi") - public static final int COLOR_RANGE_LIMITED = 0x02; + public static final int COLOR_RANGE_LIMITED = MediaFormat.COLOR_RANGE_LIMITED; /** * @see MediaFormat#COLOR_RANGE_FULL */ - @SuppressWarnings("InlinedApi") - public static final int COLOR_RANGE_FULL = 0x01; + public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL; + + /** Video projection types. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + Format.NO_VALUE, + PROJECTION_RECTANGULAR, + PROJECTION_EQUIRECTANGULAR, + PROJECTION_CUBEMAP, + PROJECTION_MESH + }) + public @interface Projection {} + /** Conventional rectangular projection. */ + public static final int PROJECTION_RECTANGULAR = 0; + /** Equirectangular spherical projection. */ + public static final int PROJECTION_EQUIRECTANGULAR = 1; + /** Cube map projection. */ + public static final int PROJECTION_CUBEMAP = 2; + /** 3-D mesh projection. */ + public static final int PROJECTION_MESH = 3; /** * Priority for media playback. @@ -630,30 +974,182 @@ public final class C { */ public static final int PRIORITY_DOWNLOAD = PRIORITY_PLAYBACK - 1000; + /** + * Network connection type. One of {@link #NETWORK_TYPE_UNKNOWN}, {@link #NETWORK_TYPE_OFFLINE}, + * {@link #NETWORK_TYPE_WIFI}, {@link #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, {@link + * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_5G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link + * #NETWORK_TYPE_ETHERNET} or {@link #NETWORK_TYPE_OTHER}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + NETWORK_TYPE_UNKNOWN, + NETWORK_TYPE_OFFLINE, + NETWORK_TYPE_WIFI, + NETWORK_TYPE_2G, + NETWORK_TYPE_3G, + NETWORK_TYPE_4G, + NETWORK_TYPE_5G, + NETWORK_TYPE_CELLULAR_UNKNOWN, + NETWORK_TYPE_ETHERNET, + NETWORK_TYPE_OTHER + }) + public @interface NetworkType {} + /** Unknown network type. */ + public static final int NETWORK_TYPE_UNKNOWN = 0; + /** No network connection. */ + public static final int NETWORK_TYPE_OFFLINE = 1; + /** Network type for a Wifi connection. */ + public static final int NETWORK_TYPE_WIFI = 2; + /** Network type for a 2G cellular connection. */ + public static final int NETWORK_TYPE_2G = 3; + /** Network type for a 3G cellular connection. */ + public static final int NETWORK_TYPE_3G = 4; + /** Network type for a 4G cellular connection. */ + public static final int NETWORK_TYPE_4G = 5; + /** Network type for a 5G cellular connection. */ + public static final int NETWORK_TYPE_5G = 9; + /** + * Network type for cellular connections which cannot be mapped to one of {@link + * #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, or {@link #NETWORK_TYPE_4G}. + */ + public static final int NETWORK_TYPE_CELLULAR_UNKNOWN = 6; + /** Network type for an Ethernet connection. */ + public static final int NETWORK_TYPE_ETHERNET = 7; + /** Network type for other connections which are not Wifi or cellular (e.g. VPN, Bluetooth). */ + public static final int NETWORK_TYPE_OTHER = 8; + + /** + * Mode specifying whether the player should hold a WakeLock and a WifiLock. One of {@link + * #WAKE_MODE_NONE}, {@link #WAKE_MODE_LOCAL} and {@link #WAKE_MODE_NETWORK}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({WAKE_MODE_NONE, WAKE_MODE_LOCAL, WAKE_MODE_NETWORK}) + public @interface WakeMode {} + /** + * A wake mode that will not cause the player to hold any locks. + * + *

This is suitable for applications that do not play media with the screen off. + */ + public static final int WAKE_MODE_NONE = 0; + /** + * A wake mode that will cause the player to hold a {@link android.os.PowerManager.WakeLock} + * during playback. + * + *

This is suitable for applications that play media with the screen off and do not load media + * over wifi. + */ + public static final int WAKE_MODE_LOCAL = 1; + /** + * A wake mode that will cause the player to hold a {@link android.os.PowerManager.WakeLock} and a + * {@link android.net.wifi.WifiManager.WifiLock} during playback. + * + *

This is suitable for applications that play media with the screen off and may load media + * over wifi. + */ + public static final int WAKE_MODE_NETWORK = 2; + + /** + * Track role flags. Possible flag values are {@link #ROLE_FLAG_MAIN}, {@link + * #ROLE_FLAG_ALTERNATE}, {@link #ROLE_FLAG_SUPPLEMENTARY}, {@link #ROLE_FLAG_COMMENTARY}, {@link + * #ROLE_FLAG_DUB}, {@link #ROLE_FLAG_EMERGENCY}, {@link #ROLE_FLAG_CAPTION}, {@link + * #ROLE_FLAG_SUBTITLE}, {@link #ROLE_FLAG_SIGN}, {@link #ROLE_FLAG_DESCRIBES_VIDEO}, {@link + * #ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND}, {@link #ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY}, + * {@link #ROLE_FLAG_TRANSCRIBES_DIALOG} and {@link #ROLE_FLAG_EASY_TO_READ}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + ROLE_FLAG_MAIN, + ROLE_FLAG_ALTERNATE, + ROLE_FLAG_SUPPLEMENTARY, + ROLE_FLAG_COMMENTARY, + ROLE_FLAG_DUB, + ROLE_FLAG_EMERGENCY, + ROLE_FLAG_CAPTION, + ROLE_FLAG_SUBTITLE, + ROLE_FLAG_SIGN, + ROLE_FLAG_DESCRIBES_VIDEO, + ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND, + ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY, + ROLE_FLAG_TRANSCRIBES_DIALOG, + ROLE_FLAG_EASY_TO_READ + }) + public @interface RoleFlags {} + /** Indicates a main track. */ + public static final int ROLE_FLAG_MAIN = 1; + /** + * Indicates an alternate track. For example a video track recorded from an different view point + * than the main track(s). + */ + public static final int ROLE_FLAG_ALTERNATE = 1 << 1; + /** + * Indicates a supplementary track, meaning the track has lower importance than the main track(s). + * For example a video track that provides a visual accompaniment to a main audio track. + */ + public static final int ROLE_FLAG_SUPPLEMENTARY = 1 << 2; + /** Indicates the track contains commentary, for example from the director. */ + public static final int ROLE_FLAG_COMMENTARY = 1 << 3; + /** + * Indicates the track is in a different language from the original, for example dubbed audio or + * translated captions. + */ + public static final int ROLE_FLAG_DUB = 1 << 4; + /** Indicates the track contains information about a current emergency. */ + public static final int ROLE_FLAG_EMERGENCY = 1 << 5; + /** + * Indicates the track contains captions. This flag may be set on video tracks to indicate the + * presence of burned in captions. + */ + public static final int ROLE_FLAG_CAPTION = 1 << 6; + /** + * Indicates the track contains subtitles. This flag may be set on video tracks to indicate the + * presence of burned in subtitles. + */ + public static final int ROLE_FLAG_SUBTITLE = 1 << 7; + /** Indicates the track contains a visual sign-language interpretation of an audio track. */ + public static final int ROLE_FLAG_SIGN = 1 << 8; + /** Indicates the track contains an audio or textual description of a video track. */ + public static final int ROLE_FLAG_DESCRIBES_VIDEO = 1 << 9; + /** Indicates the track contains a textual description of music and sound. */ + public static final int ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND = 1 << 10; + /** Indicates the track is designed for improved intelligibility of dialogue. */ + public static final int ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY = 1 << 11; + /** Indicates the track contains a transcription of spoken dialog. */ + public static final int ROLE_FLAG_TRANSCRIBES_DIALOG = 1 << 12; + /** Indicates the track contains a text that has been edited for ease of reading. */ + public static final int ROLE_FLAG_EASY_TO_READ = 1 << 13; + /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving - * {@link #TIME_UNSET} values. + * {@link #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values. * * @param timeUs The time in microseconds. * @return The corresponding time in milliseconds. */ public static long usToMs(long timeUs) { - return timeUs == TIME_UNSET ? TIME_UNSET : (timeUs / 1000); + return (timeUs == TIME_UNSET || timeUs == TIME_END_OF_SOURCE) ? timeUs : (timeUs / 1000); } /** * Converts a time in milliseconds to the corresponding time in microseconds, preserving - * {@link #TIME_UNSET} values. + * {@link #TIME_UNSET} values and {@link #TIME_END_OF_SOURCE} values. * * @param timeMs The time in milliseconds. * @return The corresponding time in microseconds. */ public static long msToUs(long timeMs) { - return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000); + return (timeMs == TIME_UNSET || timeMs == TIME_END_OF_SOURCE) ? timeMs : (timeMs * 1000); } /** - * Returns a newly generated {@link android.media.AudioTrack} session identifier. + * Returns a newly generated audio session identifier, or {@link AudioManager#ERROR} if an error + * occurred in which case audio playback may fail. + * + * @see AudioManager#generateAudioSessionId() */ @TargetApi(21) public static int generateAudioSessionIdV21(Context context) { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ControlDispatcher.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ControlDispatcher.java new file mode 100644 index 000000000000..a23b44e68587 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ControlDispatcher.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.RepeatMode; + +/** + * Dispatches operations to the {@link Player}. + *

+ * Implementations may choose to suppress (e.g. prevent playback from resuming if audio focus is + * denied) or modify (e.g. change the seek position to prevent a user from seeking past a + * non-skippable advert) operations. + */ +public interface ControlDispatcher { + + /** + * Dispatches a {@link Player#setPlayWhenReady(boolean)} operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @param playWhenReady Whether playback should proceed when ready. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady); + + /** + * Dispatches a {@link Player#seekTo(int, long)} operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @param windowIndex The index of the window. + * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to + * the window's default position. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchSeekTo(Player player, int windowIndex, long positionMs); + + /** + * Dispatches a {@link Player#setRepeatMode(int)} operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @param repeatMode The repeat mode. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode); + + /** + * Dispatches a {@link Player#setShuffleModeEnabled(boolean)} operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @param shuffleModeEnabled Whether shuffling is enabled. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled); + + /** + * Dispatches a {@link Player#stop()} operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @param reset Whether the player should be reset. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchStop(Player player, boolean reset); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultControlDispatcher.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultControlDispatcher.java new file mode 100644 index 000000000000..32fa0edf6e71 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultControlDispatcher.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.RepeatMode; + +/** + * Default {@link ControlDispatcher} that dispatches all operations to the player without + * modification. + */ +public class DefaultControlDispatcher implements ControlDispatcher { + + @Override + public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) { + player.setPlayWhenReady(playWhenReady); + return true; + } + + @Override + public boolean dispatchSeekTo(Player player, int windowIndex, long positionMs) { + player.seekTo(windowIndex, positionMs); + return true; + } + + @Override + public boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode) { + player.setRepeatMode(repeatMode); + return true; + } + + @Override + public boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled) { + player.setShuffleModeEnabled(shuffleModeEnabled); + return true; + } + + @Override + public boolean dispatchStop(Player player, boolean reset) { + player.stop(reset); + return true; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultLoadControl.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultLoadControl.java index 94c17e23a0d6..ad5350a722f0 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultLoadControl.java @@ -19,24 +19,26 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArr import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultAllocator; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.PriorityTaskManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; /** * The default {@link LoadControl} implementation. */ -public final class DefaultLoadControl implements LoadControl { +public class DefaultLoadControl implements LoadControl { /** * The default minimum duration of media that the player will attempt to ensure is buffered at all - * times, in milliseconds. + * times, in milliseconds. This value is only applied to playbacks without video. */ public static final int DEFAULT_MIN_BUFFER_MS = 15000; /** * The default maximum duration of media that the player will attempt to buffer, in milliseconds. + * For playbacks with video, this is also the default minimum duration of media that the player + * will attempt to ensure is buffered. */ - public static final int DEFAULT_MAX_BUFFER_MS = 30000; + public static final int DEFAULT_MAX_BUFFER_MS = 50000; /** * The default duration of media that must be buffered for playback to start or resume following a @@ -45,90 +47,296 @@ public final class DefaultLoadControl implements LoadControl { public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = 2500; /** - * The default duration of media that must be buffered for playback to resume after a rebuffer, - * in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user - * action. + * The default duration of media that must be buffered for playback to resume after a rebuffer, in + * milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action. */ - public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; + public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; - private static final int ABOVE_HIGH_WATERMARK = 0; - private static final int BETWEEN_WATERMARKS = 1; - private static final int BELOW_LOW_WATERMARK = 2; + /** + * The default target buffer size in bytes. The value ({@link C#LENGTH_UNSET}) means that the load + * control will calculate the target buffer size based on the selected tracks. + */ + public static final int DEFAULT_TARGET_BUFFER_BYTES = C.LENGTH_UNSET; + + /** The default prioritization of buffer time constraints over size constraints. */ + public static final boolean DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS = true; + + /** The default back buffer duration in milliseconds. */ + public static final int DEFAULT_BACK_BUFFER_DURATION_MS = 0; + + /** The default for whether the back buffer is retained from the previous keyframe. */ + public static final boolean DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME = false; + + /** A default size in bytes for a video buffer. */ + public static final int DEFAULT_VIDEO_BUFFER_SIZE = 500 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + + /** A default size in bytes for an audio buffer. */ + public static final int DEFAULT_AUDIO_BUFFER_SIZE = 54 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + + /** A default size in bytes for a text buffer. */ + public static final int DEFAULT_TEXT_BUFFER_SIZE = 2 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + + /** A default size in bytes for a metadata buffer. */ + public static final int DEFAULT_METADATA_BUFFER_SIZE = 2 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + + /** A default size in bytes for a camera motion buffer. */ + public static final int DEFAULT_CAMERA_MOTION_BUFFER_SIZE = 2 * C.DEFAULT_BUFFER_SEGMENT_SIZE; + + /** A default size in bytes for a muxed buffer (e.g. containing video, audio and text). */ + public static final int DEFAULT_MUXED_BUFFER_SIZE = + DEFAULT_VIDEO_BUFFER_SIZE + DEFAULT_AUDIO_BUFFER_SIZE + DEFAULT_TEXT_BUFFER_SIZE; + + /** Builder for {@link DefaultLoadControl}. */ + public static final class Builder { + + private DefaultAllocator allocator; + private int minBufferAudioMs; + private int minBufferVideoMs; + private int maxBufferMs; + private int bufferForPlaybackMs; + private int bufferForPlaybackAfterRebufferMs; + private int targetBufferBytes; + private boolean prioritizeTimeOverSizeThresholds; + private int backBufferDurationMs; + private boolean retainBackBufferFromKeyframe; + private boolean createDefaultLoadControlCalled; + + /** Constructs a new instance. */ + public Builder() { + minBufferAudioMs = DEFAULT_MIN_BUFFER_MS; + minBufferVideoMs = DEFAULT_MAX_BUFFER_MS; + maxBufferMs = DEFAULT_MAX_BUFFER_MS; + bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS; + bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; + targetBufferBytes = DEFAULT_TARGET_BUFFER_BYTES; + prioritizeTimeOverSizeThresholds = DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS; + backBufferDurationMs = DEFAULT_BACK_BUFFER_DURATION_MS; + retainBackBufferFromKeyframe = DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME; + } + + /** + * Sets the {@link DefaultAllocator} used by the loader. + * + * @param allocator The {@link DefaultAllocator}. + * @return This builder, for convenience. + * @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called. + */ + public Builder setAllocator(DefaultAllocator allocator) { + Assertions.checkState(!createDefaultLoadControlCalled); + this.allocator = allocator; + return this; + } + + /** + * Sets the buffer duration parameters. + * + * @param minBufferMs The minimum duration of media that the player will attempt to ensure is + * buffered at all times, in milliseconds. + * @param maxBufferMs The maximum duration of media that the player will attempt to buffer, in + * milliseconds. + * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start + * or resume following a user action such as a seek, in milliseconds. + * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered + * for playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be + * caused by buffer depletion rather than a user action. + * @return This builder, for convenience. + * @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called. + */ + public Builder setBufferDurationsMs( + int minBufferMs, + int maxBufferMs, + int bufferForPlaybackMs, + int bufferForPlaybackAfterRebufferMs) { + Assertions.checkState(!createDefaultLoadControlCalled); + assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0"); + assertGreaterOrEqual( + bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0"); + assertGreaterOrEqual(minBufferMs, bufferForPlaybackMs, "minBufferMs", "bufferForPlaybackMs"); + assertGreaterOrEqual( + minBufferMs, + bufferForPlaybackAfterRebufferMs, + "minBufferMs", + "bufferForPlaybackAfterRebufferMs"); + assertGreaterOrEqual(maxBufferMs, minBufferMs, "maxBufferMs", "minBufferMs"); + this.minBufferAudioMs = minBufferMs; + this.minBufferVideoMs = minBufferMs; + this.maxBufferMs = maxBufferMs; + this.bufferForPlaybackMs = bufferForPlaybackMs; + this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs; + return this; + } + + /** + * Sets the target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the target buffer + * size will be calculated based on the selected tracks. + * + * @param targetBufferBytes The target buffer size in bytes. + * @return This builder, for convenience. + * @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called. + */ + public Builder setTargetBufferBytes(int targetBufferBytes) { + Assertions.checkState(!createDefaultLoadControlCalled); + this.targetBufferBytes = targetBufferBytes; + return this; + } + + /** + * Sets whether the load control prioritizes buffer time constraints over buffer size + * constraints. + * + * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time + * constraints over buffer size constraints. + * @return This builder, for convenience. + * @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called. + */ + public Builder setPrioritizeTimeOverSizeThresholds(boolean prioritizeTimeOverSizeThresholds) { + Assertions.checkState(!createDefaultLoadControlCalled); + this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; + return this; + } + + /** + * Sets the back buffer duration, and whether the back buffer is retained from the previous + * keyframe. + * + * @param backBufferDurationMs The back buffer duration in milliseconds. + * @param retainBackBufferFromKeyframe Whether the back buffer is retained from the previous + * keyframe. + * @return This builder, for convenience. + * @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called. + */ + public Builder setBackBuffer(int backBufferDurationMs, boolean retainBackBufferFromKeyframe) { + Assertions.checkState(!createDefaultLoadControlCalled); + assertGreaterOrEqual(backBufferDurationMs, 0, "backBufferDurationMs", "0"); + this.backBufferDurationMs = backBufferDurationMs; + this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe; + return this; + } + + /** Creates a {@link DefaultLoadControl}. */ + public DefaultLoadControl createDefaultLoadControl() { + Assertions.checkState(!createDefaultLoadControlCalled); + createDefaultLoadControlCalled = true; + if (allocator == null) { + allocator = new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE); + } + return new DefaultLoadControl( + allocator, + minBufferAudioMs, + minBufferVideoMs, + maxBufferMs, + bufferForPlaybackMs, + bufferForPlaybackAfterRebufferMs, + targetBufferBytes, + prioritizeTimeOverSizeThresholds, + backBufferDurationMs, + retainBackBufferFromKeyframe); + } + } private final DefaultAllocator allocator; - private final long minBufferUs; + private final long minBufferAudioUs; + private final long minBufferVideoUs; private final long maxBufferUs; private final long bufferForPlaybackUs; private final long bufferForPlaybackAfterRebufferUs; - private final PriorityTaskManager priorityTaskManager; + private final int targetBufferBytesOverwrite; + private final boolean prioritizeTimeOverSizeThresholds; + private final long backBufferDurationUs; + private final boolean retainBackBufferFromKeyframe; private int targetBufferSize; private boolean isBuffering; + private boolean hasVideo; - /** - * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. - */ + /** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */ + @SuppressWarnings("deprecation") public DefaultLoadControl() { this(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE)); } - /** - * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. - * - * @param allocator The {@link DefaultAllocator} used by the loader. - */ + /** @deprecated Use {@link Builder} instead. */ + @Deprecated public DefaultLoadControl(DefaultAllocator allocator) { - this(allocator, DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS, - DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); + this( + allocator, + /* minBufferAudioMs= */ DEFAULT_MIN_BUFFER_MS, + /* minBufferVideoMs= */ DEFAULT_MAX_BUFFER_MS, + DEFAULT_MAX_BUFFER_MS, + DEFAULT_BUFFER_FOR_PLAYBACK_MS, + DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, + DEFAULT_TARGET_BUFFER_BYTES, + DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS, + DEFAULT_BACK_BUFFER_DURATION_MS, + DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME); } - /** - * Constructs a new instance. - * - * @param allocator The {@link DefaultAllocator} used by the loader. - * @param minBufferMs The minimum duration of media that the player will attempt to ensure is - * buffered at all times, in milliseconds. - * @param maxBufferMs The maximum duration of media that the player will attempt buffer, in - * milliseconds. - * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or - * resume following a user action such as a seek, in milliseconds. - * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for - * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by - * buffer depletion rather than a user action. - */ - public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, - long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) { - this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, - null); + /** @deprecated Use {@link Builder} instead. */ + @Deprecated + public DefaultLoadControl( + DefaultAllocator allocator, + int minBufferMs, + int maxBufferMs, + int bufferForPlaybackMs, + int bufferForPlaybackAfterRebufferMs, + int targetBufferBytes, + boolean prioritizeTimeOverSizeThresholds) { + this( + allocator, + /* minBufferAudioMs= */ minBufferMs, + /* minBufferVideoMs= */ minBufferMs, + maxBufferMs, + bufferForPlaybackMs, + bufferForPlaybackAfterRebufferMs, + targetBufferBytes, + prioritizeTimeOverSizeThresholds, + DEFAULT_BACK_BUFFER_DURATION_MS, + DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME); } - /** - * Constructs a new instance. - * - * @param allocator The {@link DefaultAllocator} used by the loader. - * @param minBufferMs The minimum duration of media that the player will attempt to ensure is - * buffered at all times, in milliseconds. - * @param maxBufferMs The maximum duration of media that the player will attempt buffer, in - * milliseconds. - * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or - * resume following a user action such as a seek, in milliseconds. - * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for - * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by - * buffer depletion rather than a user action. - * @param priorityTaskManager If not null, registers itself as a task with priority - * {@link C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining - * periods. - */ - public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, - long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs, - PriorityTaskManager priorityTaskManager) { + protected DefaultLoadControl( + DefaultAllocator allocator, + int minBufferAudioMs, + int minBufferVideoMs, + int maxBufferMs, + int bufferForPlaybackMs, + int bufferForPlaybackAfterRebufferMs, + int targetBufferBytes, + boolean prioritizeTimeOverSizeThresholds, + int backBufferDurationMs, + boolean retainBackBufferFromKeyframe) { + assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0"); + assertGreaterOrEqual( + bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0"); + assertGreaterOrEqual( + minBufferAudioMs, bufferForPlaybackMs, "minBufferAudioMs", "bufferForPlaybackMs"); + assertGreaterOrEqual( + minBufferVideoMs, bufferForPlaybackMs, "minBufferVideoMs", "bufferForPlaybackMs"); + assertGreaterOrEqual( + minBufferAudioMs, + bufferForPlaybackAfterRebufferMs, + "minBufferAudioMs", + "bufferForPlaybackAfterRebufferMs"); + assertGreaterOrEqual( + minBufferVideoMs, + bufferForPlaybackAfterRebufferMs, + "minBufferVideoMs", + "bufferForPlaybackAfterRebufferMs"); + assertGreaterOrEqual(maxBufferMs, minBufferAudioMs, "maxBufferMs", "minBufferAudioMs"); + assertGreaterOrEqual(maxBufferMs, minBufferVideoMs, "maxBufferMs", "minBufferVideoMs"); + assertGreaterOrEqual(backBufferDurationMs, 0, "backBufferDurationMs", "0"); + this.allocator = allocator; - minBufferUs = minBufferMs * 1000L; - maxBufferUs = maxBufferMs * 1000L; - bufferForPlaybackUs = bufferForPlaybackMs * 1000L; - bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L; - this.priorityTaskManager = priorityTaskManager; + this.minBufferAudioUs = C.msToUs(minBufferAudioMs); + this.minBufferVideoUs = C.msToUs(minBufferVideoMs); + this.maxBufferUs = C.msToUs(maxBufferMs); + this.bufferForPlaybackUs = C.msToUs(bufferForPlaybackMs); + this.bufferForPlaybackAfterRebufferUs = C.msToUs(bufferForPlaybackAfterRebufferMs); + this.targetBufferBytesOverwrite = targetBufferBytes; + this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; + this.backBufferDurationUs = C.msToUs(backBufferDurationMs); + this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe; } @Override @@ -139,12 +347,11 @@ public final class DefaultLoadControl implements LoadControl { @Override public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - targetBufferSize = 0; - for (int i = 0; i < renderers.length; i++) { - if (trackSelections.get(i) != null) { - targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType()); - } - } + hasVideo = hasVideo(renderers, trackSelections); + targetBufferSize = + targetBufferBytesOverwrite == C.LENGTH_UNSET + ? calculateTargetBufferSize(renderers, trackSelections) + : targetBufferBytesOverwrite; allocator.setTargetBufferSize(targetBufferSize); } @@ -164,42 +371,103 @@ public final class DefaultLoadControl implements LoadControl { } @Override - public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) { - long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; - return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs; + public long getBackBufferDurationUs() { + return backBufferDurationUs; } @Override - public boolean shouldContinueLoading(long bufferedDurationUs) { - int bufferTimeState = getBufferTimeState(bufferedDurationUs); + public boolean retainBackBufferFromKeyframe() { + return retainBackBufferFromKeyframe; + } + + @Override + public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; - boolean wasBuffering = isBuffering; - isBuffering = bufferTimeState == BELOW_LOW_WATERMARK - || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached); - if (priorityTaskManager != null && isBuffering != wasBuffering) { - if (isBuffering) { - priorityTaskManager.add(C.PRIORITY_PLAYBACK); - } else { - priorityTaskManager.remove(C.PRIORITY_PLAYBACK); - } + long minBufferUs = hasVideo ? minBufferVideoUs : minBufferAudioUs; + if (playbackSpeed > 1) { + // The playback speed is faster than real time, so scale up the minimum required media + // duration to keep enough media buffered for a playout duration of minBufferUs. + long mediaDurationMinBufferUs = + Util.getMediaDurationForPlayoutDuration(minBufferUs, playbackSpeed); + minBufferUs = Math.min(mediaDurationMinBufferUs, maxBufferUs); } + if (bufferedDurationUs < minBufferUs) { + isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached; + } else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) { + isBuffering = false; + } // Else don't change the buffering state return isBuffering; } - private int getBufferTimeState(long bufferedDurationUs) { - return bufferedDurationUs > maxBufferUs ? ABOVE_HIGH_WATERMARK - : (bufferedDurationUs < minBufferUs ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS); + @Override + public boolean shouldStartPlayback( + long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { + bufferedDurationUs = Util.getPlayoutDurationForMediaDuration(bufferedDurationUs, playbackSpeed); + long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; + return minBufferDurationUs <= 0 + || bufferedDurationUs >= minBufferDurationUs + || (!prioritizeTimeOverSizeThresholds + && allocator.getTotalBytesAllocated() >= targetBufferSize); + } + + /** + * Calculate target buffer size in bytes based on the selected tracks. The player will try not to + * exceed this target buffer. Only used when {@code targetBufferBytes} is {@link C#LENGTH_UNSET}. + * + * @param renderers The renderers for which the track were selected. + * @param trackSelectionArray The selected tracks. + * @return The target buffer size in bytes. + */ + protected int calculateTargetBufferSize( + Renderer[] renderers, TrackSelectionArray trackSelectionArray) { + int targetBufferSize = 0; + for (int i = 0; i < renderers.length; i++) { + if (trackSelectionArray.get(i) != null) { + targetBufferSize += getDefaultBufferSize(renderers[i].getTrackType()); + } + } + return targetBufferSize; } private void reset(boolean resetAllocator) { targetBufferSize = 0; - if (priorityTaskManager != null && isBuffering) { - priorityTaskManager.remove(C.PRIORITY_PLAYBACK); - } isBuffering = false; if (resetAllocator) { allocator.reset(); } } + private static int getDefaultBufferSize(int trackType) { + switch (trackType) { + case C.TRACK_TYPE_DEFAULT: + return DEFAULT_MUXED_BUFFER_SIZE; + case C.TRACK_TYPE_AUDIO: + return DEFAULT_AUDIO_BUFFER_SIZE; + case C.TRACK_TYPE_VIDEO: + return DEFAULT_VIDEO_BUFFER_SIZE; + case C.TRACK_TYPE_TEXT: + return DEFAULT_TEXT_BUFFER_SIZE; + case C.TRACK_TYPE_METADATA: + return DEFAULT_METADATA_BUFFER_SIZE; + case C.TRACK_TYPE_CAMERA_MOTION: + return DEFAULT_CAMERA_MOTION_BUFFER_SIZE; + case C.TRACK_TYPE_NONE: + return 0; + default: + throw new IllegalArgumentException(); + } + } + + private static boolean hasVideo(Renderer[] renderers, TrackSelectionArray trackSelectionArray) { + for (int i = 0; i < renderers.length; i++) { + if (renderers[i].getTrackType() == C.TRACK_TYPE_VIDEO && trackSelectionArray.get(i) != null) { + return true; + } + } + return false; + } + + private static void assertGreaterOrEqual(int value1, int value2, String name1, String name2) { + Assertions.checkArgument(value1 >= value2, name1 + " cannot be less than " + name2); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultMediaClock.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultMediaClock.java new file mode 100644 index 000000000000..9967bfeb9ea7 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultMediaClock.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MediaClock; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.StandaloneMediaClock; + +/** + * Default {@link MediaClock} which uses a renderer media clock and falls back to a + * {@link StandaloneMediaClock} if necessary. + */ +/* package */ final class DefaultMediaClock implements MediaClock { + + /** + * Listener interface to be notified of changes to the active playback parameters. + */ + public interface PlaybackParameterListener { + + /** + * Called when the active playback parameters changed. Will not be called for {@link + * #setPlaybackParameters(PlaybackParameters)}. + * + * @param newPlaybackParameters The newly active {@link PlaybackParameters}. + */ + void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters); + } + + private final StandaloneMediaClock standaloneClock; + private final PlaybackParameterListener listener; + + @Nullable private Renderer rendererClockSource; + @Nullable private MediaClock rendererClock; + private boolean isUsingStandaloneClock; + private boolean standaloneClockIsStarted; + + /** + * Creates a new instance with listener for playback parameter changes and a {@link Clock} to use + * for the standalone clock implementation. + * + * @param listener A {@link PlaybackParameterListener} to listen for playback parameter + * changes. + * @param clock A {@link Clock}. + */ + public DefaultMediaClock(PlaybackParameterListener listener, Clock clock) { + this.listener = listener; + this.standaloneClock = new StandaloneMediaClock(clock); + isUsingStandaloneClock = true; + } + + /** + * Starts the standalone fallback clock. + */ + public void start() { + standaloneClockIsStarted = true; + standaloneClock.start(); + } + + /** + * Stops the standalone fallback clock. + */ + public void stop() { + standaloneClockIsStarted = false; + standaloneClock.stop(); + } + + /** + * Resets the position of the standalone fallback clock. + * + * @param positionUs The position to set in microseconds. + */ + public void resetPosition(long positionUs) { + standaloneClock.resetPosition(positionUs); + } + + /** + * Notifies the media clock that a renderer has been enabled. Starts using the media clock of the + * provided renderer if available. + * + * @param renderer The renderer which has been enabled. + * @throws ExoPlaybackException If the renderer provides a media clock and another renderer media + * clock is already provided. + */ + public void onRendererEnabled(Renderer renderer) throws ExoPlaybackException { + MediaClock rendererMediaClock = renderer.getMediaClock(); + if (rendererMediaClock != null && rendererMediaClock != rendererClock) { + if (rendererClock != null) { + throw ExoPlaybackException.createForUnexpected( + new IllegalStateException("Multiple renderer media clocks enabled.")); + } + this.rendererClock = rendererMediaClock; + this.rendererClockSource = renderer; + rendererClock.setPlaybackParameters(standaloneClock.getPlaybackParameters()); + } + } + + /** + * Notifies the media clock that a renderer has been disabled. Stops using the media clock of this + * renderer if used. + * + * @param renderer The renderer which has been disabled. + */ + public void onRendererDisabled(Renderer renderer) { + if (renderer == rendererClockSource) { + this.rendererClock = null; + this.rendererClockSource = null; + isUsingStandaloneClock = true; + } + } + + /** + * Syncs internal clock if needed and returns current clock position in microseconds. + * + * @param isReadingAhead Whether the renderers are reading ahead. + */ + public long syncAndGetPositionUs(boolean isReadingAhead) { + syncClocks(isReadingAhead); + return getPositionUs(); + } + + // MediaClock implementation. + + @Override + public long getPositionUs() { + return isUsingStandaloneClock ? standaloneClock.getPositionUs() : rendererClock.getPositionUs(); + } + + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + if (rendererClock != null) { + rendererClock.setPlaybackParameters(playbackParameters); + playbackParameters = rendererClock.getPlaybackParameters(); + } + standaloneClock.setPlaybackParameters(playbackParameters); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return rendererClock != null + ? rendererClock.getPlaybackParameters() + : standaloneClock.getPlaybackParameters(); + } + + private void syncClocks(boolean isReadingAhead) { + if (shouldUseStandaloneClock(isReadingAhead)) { + isUsingStandaloneClock = true; + if (standaloneClockIsStarted) { + standaloneClock.start(); + } + return; + } + long rendererClockPositionUs = rendererClock.getPositionUs(); + if (isUsingStandaloneClock) { + // Ensure enabling the renderer clock doesn't jump backwards in time. + if (rendererClockPositionUs < standaloneClock.getPositionUs()) { + standaloneClock.stop(); + return; + } + isUsingStandaloneClock = false; + if (standaloneClockIsStarted) { + standaloneClock.start(); + } + } + // Continuously sync stand-alone clock to renderer clock so that it can take over if needed. + standaloneClock.resetPosition(rendererClockPositionUs); + PlaybackParameters playbackParameters = rendererClock.getPlaybackParameters(); + if (!playbackParameters.equals(standaloneClock.getPlaybackParameters())) { + standaloneClock.setPlaybackParameters(playbackParameters); + listener.onPlaybackParametersChanged(playbackParameters); + } + } + + private boolean shouldUseStandaloneClock(boolean isReadingAhead) { + // Use the standalone clock if the clock providing renderer is not set or has ended. Also use + // the standalone clock if the renderer is not ready and we have finished reading the stream or + // are reading ahead to avoid getting stuck if tracks in the current period have uneven + // durations. See: https://github.com/google/ExoPlayer/issues/1874. + return rendererClockSource == null + || rendererClockSource.isEnded() + || (!rendererClockSource.isReady() + && (isReadingAhead || rendererClockSource.hasReadStreamToEnd())); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultRenderersFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultRenderersFactory.java index 9cfbc97e0222..95fe509ee938 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -16,22 +16,29 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2; import android.content.Context; +import android.media.MediaCodec; import android.os.Handler; import android.os.Looper; -import android.support.annotation.IntDef; -import android.util.Log; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioCapabilities; import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioProcessor; import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioRendererEventListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.DefaultAudioSink; import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecSelector; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataRenderer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.TextOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.TextRenderer; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelector; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoRendererEventListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.spherical.CameraMotionRenderer; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Constructor; @@ -49,11 +56,12 @@ public class DefaultRenderersFactory implements RenderersFactory { public static final long DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS = 5000; /** - * Modes for using extension renderers. + * Modes for using extension renderers. One of {@link #EXTENSION_RENDERER_MODE_OFF}, {@link + * #EXTENSION_RENDERER_MODE_ON} or {@link #EXTENSION_RENDERER_MODE_PREFER}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({EXTENSION_RENDERER_MODE_OFF, EXTENSION_RENDERER_MODE_ON, - EXTENSION_RENDERER_MODE_PREFER}) + @IntDef({EXTENSION_RENDERER_MODE_OFF, EXTENSION_RENDERER_MODE_ON, EXTENSION_RENDERER_MODE_PREFER}) public @interface ExtensionRendererMode {} /** * Do not allow use of extension renderers. @@ -79,99 +87,249 @@ public class DefaultRenderersFactory implements RenderersFactory { protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50; private final Context context; - private final DrmSessionManager drmSessionManager; - private final @ExtensionRendererMode int extensionRendererMode; - private final long allowedVideoJoiningTimeMs; + @Nullable private DrmSessionManager drmSessionManager; + @ExtensionRendererMode private int extensionRendererMode; + private long allowedVideoJoiningTimeMs; + private boolean playClearSamplesWithoutKeys; + private boolean enableDecoderFallback; + private MediaCodecSelector mediaCodecSelector; - /** - * @param context A {@link Context}. - */ + /** @param context A {@link Context}. */ public DefaultRenderersFactory(Context context) { - this(context, null); + this.context = context; + extensionRendererMode = EXTENSION_RENDERER_MODE_OFF; + allowedVideoJoiningTimeMs = DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS; + mediaCodecSelector = MediaCodecSelector.DEFAULT; } /** - * @param context A {@link Context}. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected - * playbacks are not required. + * @deprecated Use {@link #DefaultRenderersFactory(Context)} and pass {@link DrmSessionManager} + * directly to {@link SimpleExoPlayer.Builder}. */ - public DefaultRenderersFactory(Context context, - DrmSessionManager drmSessionManager) { + @Deprecated + @SuppressWarnings("deprecation") + public DefaultRenderersFactory( + Context context, @Nullable DrmSessionManager drmSessionManager) { this(context, drmSessionManager, EXTENSION_RENDERER_MODE_OFF); } /** - * @param context A {@link Context}. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected - * playbacks are not required.. - * @param extensionRendererMode The extension renderer mode, which determines if and how - * available extension renderers are used. Note that extensions must be included in the - * application build for them to be considered available. + * @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link + * #setExtensionRendererMode(int)}. */ - public DefaultRenderersFactory(Context context, - DrmSessionManager drmSessionManager, - @ExtensionRendererMode int extensionRendererMode) { - this(context, drmSessionManager, extensionRendererMode, - DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); + @Deprecated + @SuppressWarnings("deprecation") + public DefaultRenderersFactory( + Context context, @ExtensionRendererMode int extensionRendererMode) { + this(context, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); } /** - * @param context A {@link Context}. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected - * playbacks are not required.. - * @param extensionRendererMode The extension renderer mode, which determines if and how - * available extension renderers are used. Note that extensions must be included in the - * application build for them to be considered available. - * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt - * to seamlessly join an ongoing playback. + * @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link + * #setExtensionRendererMode(int)}, and pass {@link DrmSessionManager} directly to {@link + * SimpleExoPlayer.Builder}. */ - public DefaultRenderersFactory(Context context, - DrmSessionManager drmSessionManager, - @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { + @Deprecated + @SuppressWarnings("deprecation") + public DefaultRenderersFactory( + Context context, + @Nullable DrmSessionManager drmSessionManager, + @ExtensionRendererMode int extensionRendererMode) { + this(context, drmSessionManager, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); + } + + /** + * @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link + * #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}. + */ + @Deprecated + @SuppressWarnings("deprecation") + public DefaultRenderersFactory( + Context context, + @ExtensionRendererMode int extensionRendererMode, + long allowedVideoJoiningTimeMs) { + this(context, null, extensionRendererMode, allowedVideoJoiningTimeMs); + } + + /** + * @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link + * #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}, and pass + * {@link DrmSessionManager} directly to {@link SimpleExoPlayer.Builder}. + */ + @Deprecated + public DefaultRenderersFactory( + Context context, + @Nullable DrmSessionManager drmSessionManager, + @ExtensionRendererMode int extensionRendererMode, + long allowedVideoJoiningTimeMs) { this.context = context; - this.drmSessionManager = drmSessionManager; this.extensionRendererMode = extensionRendererMode; this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs; + this.drmSessionManager = drmSessionManager; + mediaCodecSelector = MediaCodecSelector.DEFAULT; + } + + /** + * Sets the extension renderer mode, which determines if and how available extension renderers are + * used. Note that extensions must be included in the application build for them to be considered + * available. + * + *

The default value is {@link #EXTENSION_RENDERER_MODE_OFF}. + * + * @param extensionRendererMode The extension renderer mode. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory setExtensionRendererMode( + @ExtensionRendererMode int extensionRendererMode) { + this.extensionRendererMode = extensionRendererMode; + return this; + } + + /** + * Sets whether renderers are permitted to play clear regions of encrypted media prior to having + * obtained the keys necessary to decrypt encrypted regions of the media. For encrypted media that + * starts with a short clear region, this allows playback to begin in parallel with key + * acquisition, which can reduce startup latency. + * + *

The default value is {@code false}. + * + * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of + * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of + * the media. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory setPlayClearSamplesWithoutKeys( + boolean playClearSamplesWithoutKeys) { + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + return this; + } + + /** + * Sets whether to enable fallback to lower-priority decoders if decoder initialization fails. + * This may result in using a decoder that is less efficient or slower than the primary decoder. + * + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory setEnableDecoderFallback(boolean enableDecoderFallback) { + this.enableDecoderFallback = enableDecoderFallback; + return this; + } + + /** + * Sets a {@link MediaCodecSelector} for use by {@link MediaCodec} based renderers. + * + *

The default value is {@link MediaCodecSelector#DEFAULT}. + * + * @param mediaCodecSelector The {@link MediaCodecSelector}. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory setMediaCodecSelector(MediaCodecSelector mediaCodecSelector) { + this.mediaCodecSelector = mediaCodecSelector; + return this; + } + + /** + * Sets the maximum duration for which video renderers can attempt to seamlessly join an ongoing + * playback. + * + *

The default value is {@link #DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS}. + * + * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to + * seamlessly join an ongoing playback, in milliseconds. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory setAllowedVideoJoiningTimeMs(long allowedVideoJoiningTimeMs) { + this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs; + return this; } @Override - public Renderer[] createRenderers(Handler eventHandler, + public Renderer[] createRenderers( + Handler eventHandler, VideoRendererEventListener videoRendererEventListener, AudioRendererEventListener audioRendererEventListener, - TextRenderer.Output textRendererOutput, MetadataRenderer.Output metadataRendererOutput) { + TextOutput textRendererOutput, + MetadataOutput metadataRendererOutput, + @Nullable DrmSessionManager drmSessionManager) { + if (drmSessionManager == null) { + drmSessionManager = this.drmSessionManager; + } ArrayList renderersList = new ArrayList<>(); - buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs, - eventHandler, videoRendererEventListener, extensionRendererMode, renderersList); - buildAudioRenderers(context, drmSessionManager, buildAudioProcessors(), - eventHandler, audioRendererEventListener, extensionRendererMode, renderersList); + buildVideoRenderers( + context, + extensionRendererMode, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + enableDecoderFallback, + eventHandler, + videoRendererEventListener, + allowedVideoJoiningTimeMs, + renderersList); + buildAudioRenderers( + context, + extensionRendererMode, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + enableDecoderFallback, + buildAudioProcessors(), + eventHandler, + audioRendererEventListener, + renderersList); buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(), extensionRendererMode, renderersList); buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(), extensionRendererMode, renderersList); + buildCameraMotionRenderers(context, extensionRendererMode, renderersList); buildMiscellaneousRenderers(context, eventHandler, extensionRendererMode, renderersList); - return renderersList.toArray(new Renderer[renderersList.size()]); + return renderersList.toArray(new Renderer[0]); } /** * Builds video renderers for use by the player. * * @param context The {@link Context} associated with the player. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player - * will not be used for DRM protected playbacks. - * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video - * renderers can attempt to seamlessly join an ongoing playback. + * @param extensionRendererMode The extension renderer mode. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will + * not be used for DRM protected playbacks. + * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of + * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of + * the media. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is slower/less efficient than + * the primary decoder. * @param eventHandler A handler associated with the main thread's looper. * @param eventListener An event listener. - * @param extensionRendererMode The extension renderer mode. + * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to + * seamlessly join an ongoing playback, in milliseconds. * @param out An array to which the built renderers should be appended. */ - protected void buildVideoRenderers(Context context, - DrmSessionManager drmSessionManager, long allowedVideoJoiningTimeMs, - Handler eventHandler, VideoRendererEventListener eventListener, - @ExtensionRendererMode int extensionRendererMode, ArrayList out) { - out.add(new MediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT, - allowedVideoJoiningTimeMs, drmSessionManager, false, eventHandler, eventListener, - MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); + protected void buildVideoRenderers( + Context context, + @ExtensionRendererMode int extensionRendererMode, + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, + Handler eventHandler, + VideoRendererEventListener eventListener, + long allowedVideoJoiningTimeMs, + ArrayList out) { + out.add( + new MediaCodecVideoRenderer( + context, + mediaCodecSelector, + allowedVideoJoiningTimeMs, + drmSessionManager, + playClearSamplesWithoutKeys, + enableDecoderFallback, + eventHandler, + eventListener, + MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; @@ -182,18 +340,57 @@ public class DefaultRenderersFactory implements RenderersFactory { } try { - Class clazz = - Class.forName("org.mozilla.thirdparty.com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer"); - Constructor constructor = clazz.getConstructor(boolean.class, long.class, Handler.class, - VideoRendererEventListener.class, int.class); - Renderer renderer = (Renderer) constructor.newInstance(true, allowedVideoJoiningTimeMs, - eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); + // Full class names used for constructor args so the LINT rule triggers if any of them move. + // LINT.IfChange + Class clazz = Class.forName("org.mozilla.thirdparty.com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer"); + Constructor constructor = + clazz.getConstructor( + long.class, + android.os.Handler.class, + org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoRendererEventListener.class, + int.class); + // LINT.ThenChange(../../../../../../../proguard-rules.txt) + Renderer renderer = + (Renderer) + constructor.newInstance( + allowedVideoJoiningTimeMs, + eventHandler, + eventListener, + MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibvpxVideoRenderer."); } catch (ClassNotFoundException e) { // Expected if the app was built without the extension. } catch (Exception e) { - throw new RuntimeException(e); + // The extension is present, but instantiation failed. + throw new RuntimeException("Error instantiating VP9 extension", e); + } + + try { + // Full class names used for constructor args so the LINT rule triggers if any of them move. + // LINT.IfChange + Class clazz = Class.forName("org.mozilla.thirdparty.com.google.android.exoplayer2.ext.av1.Libgav1VideoRenderer"); + Constructor constructor = + clazz.getConstructor( + long.class, + android.os.Handler.class, + org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoRendererEventListener.class, + int.class); + // LINT.ThenChange(../../../../../../../proguard-rules.txt) + Renderer renderer = + (Renderer) + constructor.newInstance( + allowedVideoJoiningTimeMs, + eventHandler, + eventListener, + MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); + out.add(extensionRendererIndex++, renderer); + Log.i(TAG, "Loaded Libgav1VideoRenderer."); + } catch (ClassNotFoundException e) { + // Expected if the app was built without the extension. + } catch (Exception e) { + // The extension is present, but instantiation failed. + throw new RuntimeException("Error instantiating AV1 extension", e); } } @@ -201,22 +398,43 @@ public class DefaultRenderersFactory implements RenderersFactory { * Builds audio renderers for use by the player. * * @param context The {@link Context} associated with the player. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player - * will not be used for DRM protected playbacks. - * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio - * buffers before output. May be empty. + * @param extensionRendererMode The extension renderer mode. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will + * not be used for DRM protected playbacks. + * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of + * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of + * the media. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is slower/less efficient than + * the primary decoder. + * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers + * before output. May be empty. * @param eventHandler A handler to use when invoking event listeners and outputs. * @param eventListener An event listener. - * @param extensionRendererMode The extension renderer mode. * @param out An array to which the built renderers should be appended. */ - protected void buildAudioRenderers(Context context, - DrmSessionManager drmSessionManager, - AudioProcessor[] audioProcessors, Handler eventHandler, - AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode, + protected void buildAudioRenderers( + Context context, + @ExtensionRendererMode int extensionRendererMode, + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, + AudioProcessor[] audioProcessors, + Handler eventHandler, + AudioRendererEventListener eventListener, ArrayList out) { - out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true, - eventHandler, eventListener, AudioCapabilities.getCapabilities(context), audioProcessors)); + out.add( + new MediaCodecAudioRenderer( + context, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + enableDecoderFallback, + eventHandler, + eventListener, + new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors))); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; @@ -227,48 +445,67 @@ public class DefaultRenderersFactory implements RenderersFactory { } try { - Class clazz = - Class.forName("org.mozilla.thirdparty.com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer"); - Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class, AudioProcessor[].class); - Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener, - audioProcessors); + // Full class names used for constructor args so the LINT rule triggers if any of them move. + // LINT.IfChange + Class clazz = Class.forName("org.mozilla.thirdparty.com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer"); + Constructor constructor = + clazz.getConstructor( + android.os.Handler.class, + org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioRendererEventListener.class, + org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioProcessor[].class); + // LINT.ThenChange(../../../../../../../proguard-rules.txt) + Renderer renderer = + (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibopusAudioRenderer."); } catch (ClassNotFoundException e) { // Expected if the app was built without the extension. } catch (Exception e) { - throw new RuntimeException(e); + // The extension is present, but instantiation failed. + throw new RuntimeException("Error instantiating Opus extension", e); } try { - Class clazz = - Class.forName("org.mozilla.thirdparty.com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer"); - Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class, AudioProcessor[].class); - Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener, - audioProcessors); + // Full class names used for constructor args so the LINT rule triggers if any of them move. + // LINT.IfChange + Class clazz = Class.forName("org.mozilla.thirdparty.com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer"); + Constructor constructor = + clazz.getConstructor( + android.os.Handler.class, + org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioRendererEventListener.class, + org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioProcessor[].class); + // LINT.ThenChange(../../../../../../../proguard-rules.txt) + Renderer renderer = + (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibflacAudioRenderer."); } catch (ClassNotFoundException e) { // Expected if the app was built without the extension. } catch (Exception e) { - throw new RuntimeException(e); + // The extension is present, but instantiation failed. + throw new RuntimeException("Error instantiating FLAC extension", e); } try { + // Full class names used for constructor args so the LINT rule triggers if any of them move. + // LINT.IfChange Class clazz = Class.forName("org.mozilla.thirdparty.com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer"); - Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class, AudioProcessor[].class); - Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener, - audioProcessors); + Constructor constructor = + clazz.getConstructor( + android.os.Handler.class, + org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioRendererEventListener.class, + org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioProcessor[].class); + // LINT.ThenChange(../../../../../../../proguard-rules.txt) + Renderer renderer = + (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded FfmpegAudioRenderer."); } catch (ClassNotFoundException e) { // Expected if the app was built without the extension. } catch (Exception e) { - throw new RuntimeException(e); + // The extension is present, but instantiation failed. + throw new RuntimeException("Error instantiating FFmpeg extension", e); } } @@ -277,13 +514,15 @@ public class DefaultRenderersFactory implements RenderersFactory { * * @param context The {@link Context} associated with the player. * @param output An output for the renderers. - * @param outputLooper The looper associated with the thread on which the output should be - * called. + * @param outputLooper The looper associated with the thread on which the output should be called. * @param extensionRendererMode The extension renderer mode. * @param out An array to which the built renderers should be appended. */ - protected void buildTextRenderers(Context context, TextRenderer.Output output, - Looper outputLooper, @ExtensionRendererMode int extensionRendererMode, + protected void buildTextRenderers( + Context context, + TextOutput output, + Looper outputLooper, + @ExtensionRendererMode int extensionRendererMode, ArrayList out) { out.add(new TextRenderer(output, outputLooper)); } @@ -293,17 +532,31 @@ public class DefaultRenderersFactory implements RenderersFactory { * * @param context The {@link Context} associated with the player. * @param output An output for the renderers. - * @param outputLooper The looper associated with the thread on which the output should be - * called. + * @param outputLooper The looper associated with the thread on which the output should be called. * @param extensionRendererMode The extension renderer mode. * @param out An array to which the built renderers should be appended. */ - protected void buildMetadataRenderers(Context context, MetadataRenderer.Output output, - Looper outputLooper, @ExtensionRendererMode int extensionRendererMode, + protected void buildMetadataRenderers( + Context context, + MetadataOutput output, + Looper outputLooper, + @ExtensionRendererMode int extensionRendererMode, ArrayList out) { out.add(new MetadataRenderer(output, outputLooper)); } + /** + * Builds camera motion renderers for use by the player. + * + * @param context The {@link Context} associated with the player. + * @param extensionRendererMode The extension renderer mode. + * @param out An array to which the built renderers should be appended. + */ + protected void buildCameraMotionRenderers( + Context context, @ExtensionRendererMode int extensionRendererMode, ArrayList out) { + out.add(new CameraMotionRenderer()); + } + /** * Builds any miscellaneous renderers used by the player. * diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlaybackException.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlaybackException.java index 7c1227785418..bad5cc769359 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlaybackException.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlaybackException.java @@ -15,10 +15,14 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2; -import android.support.annotation.IntDef; +import android.os.SystemClock; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -28,10 +32,13 @@ import java.lang.annotation.RetentionPolicy; public final class ExoPlaybackException extends Exception { /** - * The type of source that produced the error. + * The type of source that produced the error. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} + * {@link #TYPE_UNEXPECTED}, {@link #TYPE_REMOTE} or {@link #TYPE_OUT_OF_MEMORY}. Note that new + * types may be added in the future and error handling should handle unknown type values. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED}) + @IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED, TYPE_REMOTE, TYPE_OUT_OF_MEMORY}) public @interface Type {} /** * The error occurred loading data from a {@link MediaSource}. @@ -51,11 +58,16 @@ public final class ExoPlaybackException extends Exception { * Call {@link #getUnexpectedException()} to retrieve the underlying cause. */ public static final int TYPE_UNEXPECTED = 2; - /** - * The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and - * {@link #TYPE_UNEXPECTED}. + * The error occurred in a remote component. + * + *

Call {@link #getMessage()} to retrieve the message associated with the error. */ + public static final int TYPE_REMOTE = 3; + /** The error was an {@link OutOfMemoryError}. */ + public static final int TYPE_OUT_OF_MEMORY = 4; + + /** The {@link Type} of the playback failure. */ @Type public final int type; /** @@ -64,15 +76,22 @@ public final class ExoPlaybackException extends Exception { public final int rendererIndex; /** - * Creates an instance of type {@link #TYPE_RENDERER}. - * - * @param cause The cause of the failure. - * @param rendererIndex The index of the renderer in which the failure occurred. - * @return The created instance. + * If {@link #type} is {@link #TYPE_RENDERER}, this is the {@link Format} the renderer was using + * at the time of the exception, or null if the renderer wasn't using a {@link Format}. */ - public static ExoPlaybackException createForRenderer(Exception cause, int rendererIndex) { - return new ExoPlaybackException(TYPE_RENDERER, null, cause, rendererIndex); - } + @Nullable public final Format rendererFormat; + + /** + * If {@link #type} is {@link #TYPE_RENDERER}, this is the level of {@link FormatSupport} of the + * renderer for {@link #rendererFormat}. If {@link #rendererFormat} is null, this is {@link + * RendererCapabilities#FORMAT_HANDLED}. + */ + @FormatSupport public final int rendererFormatSupport; + + /** The value of {@link SystemClock#elapsedRealtime()} when this exception was created. */ + public final long timestampMs; + + @Nullable private final Throwable cause; /** * Creates an instance of type {@link #TYPE_SOURCE}. @@ -81,7 +100,31 @@ public final class ExoPlaybackException extends Exception { * @return The created instance. */ public static ExoPlaybackException createForSource(IOException cause) { - return new ExoPlaybackException(TYPE_SOURCE, null, cause, C.INDEX_UNSET); + return new ExoPlaybackException(TYPE_SOURCE, cause); + } + + /** + * Creates an instance of type {@link #TYPE_RENDERER}. + * + * @param cause The cause of the failure. + * @param rendererIndex The index of the renderer in which the failure occurred. + * @param rendererFormat The {@link Format} the renderer was using at the time of the exception, + * or null if the renderer wasn't using a {@link Format}. + * @param rendererFormatSupport The {@link FormatSupport} of the renderer for {@code + * rendererFormat}. Ignored if {@code rendererFormat} is null. + * @return The created instance. + */ + public static ExoPlaybackException createForRenderer( + Exception cause, + int rendererIndex, + @Nullable Format rendererFormat, + @FormatSupport int rendererFormatSupport) { + return new ExoPlaybackException( + TYPE_RENDERER, + cause, + rendererIndex, + rendererFormat, + rendererFormat == null ? RendererCapabilities.FORMAT_HANDLED : rendererFormatSupport); } /** @@ -90,15 +133,62 @@ public final class ExoPlaybackException extends Exception { * @param cause The cause of the failure. * @return The created instance. */ - /* package */ static ExoPlaybackException createForUnexpected(RuntimeException cause) { - return new ExoPlaybackException(TYPE_UNEXPECTED, null, cause, C.INDEX_UNSET); + public static ExoPlaybackException createForUnexpected(RuntimeException cause) { + return new ExoPlaybackException(TYPE_UNEXPECTED, cause); } - private ExoPlaybackException(@Type int type, String message, Throwable cause, - int rendererIndex) { - super(message, cause); + /** + * Creates an instance of type {@link #TYPE_REMOTE}. + * + * @param message The message associated with the error. + * @return The created instance. + */ + public static ExoPlaybackException createForRemote(String message) { + return new ExoPlaybackException(TYPE_REMOTE, message); + } + + /** + * Creates an instance of type {@link #TYPE_OUT_OF_MEMORY}. + * + * @param cause The cause of the failure. + * @return The created instance. + */ + public static ExoPlaybackException createForOutOfMemoryError(OutOfMemoryError cause) { + return new ExoPlaybackException(TYPE_OUT_OF_MEMORY, cause); + } + + private ExoPlaybackException(@Type int type, Throwable cause) { + this( + type, + cause, + /* rendererIndex= */ C.INDEX_UNSET, + /* rendererFormat= */ null, + /* rendererFormatSupport= */ RendererCapabilities.FORMAT_HANDLED); + } + + private ExoPlaybackException( + @Type int type, + Throwable cause, + int rendererIndex, + @Nullable Format rendererFormat, + @FormatSupport int rendererFormatSupport) { + super(cause); this.type = type; + this.cause = cause; this.rendererIndex = rendererIndex; + this.rendererFormat = rendererFormat; + this.rendererFormatSupport = rendererFormatSupport; + timestampMs = SystemClock.elapsedRealtime(); + } + + private ExoPlaybackException(@Type int type, String message) { + super(message); + this.type = type; + rendererIndex = C.INDEX_UNSET; + rendererFormat = null; + rendererFormatSupport = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; + cause = null; + timestampMs = SystemClock.elapsedRealtime(); } /** @@ -108,7 +198,7 @@ public final class ExoPlaybackException extends Exception { */ public IOException getSourceException() { Assertions.checkState(type == TYPE_SOURCE); - return (IOException) getCause(); + return (IOException) Assertions.checkNotNull(cause); } /** @@ -118,7 +208,7 @@ public final class ExoPlaybackException extends Exception { */ public Exception getRendererException() { Assertions.checkState(type == TYPE_RENDERER); - return (Exception) getCause(); + return (Exception) Assertions.checkNotNull(cause); } /** @@ -128,7 +218,16 @@ public final class ExoPlaybackException extends Exception { */ public RuntimeException getUnexpectedException() { Assertions.checkState(type == TYPE_UNEXPECTED); - return (RuntimeException) getCause(); + return (RuntimeException) Assertions.checkNotNull(cause); } + /** + * Retrieves the underlying error when {@link #type} is {@link #TYPE_OUT_OF_MEMORY}. + * + * @throws IllegalStateException If {@link #type} is not {@link #TYPE_OUT_OF_MEMORY}. + */ + public OutOfMemoryError getOutOfMemoryError() { + Assertions.checkState(type == TYPE_OUT_OF_MEMORY); + return (OutOfMemoryError) Assertions.checkNotNull(cause); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayer.java index 88bc92005007..048c1776c920 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayer.java @@ -15,267 +15,325 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2; -import android.support.annotation.Nullable; +import android.content.Context; +import android.os.Looper; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsCollector; import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataRenderer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ClippingMediaSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ExtractorMediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.LoopingMediaSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MergingMediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ProgressiveMediaSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SingleSampleMediaSource; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.TextRenderer; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelector; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import org.mozilla.thirdparty.com.google.android.exoplayer2.video.MediaCodecVideoRenderer; /** - * An extensible media player exposing traditional high-level media player functionality, such as - * the ability to buffer media, play, pause and seek. Instances can be obtained from - * {@link ExoPlayerFactory}. + * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link + * SimpleExoPlayer.Builder} or {@link ExoPlayer.Builder}. + * + *

Player components

* - *

Player composition

*

ExoPlayer is designed to make few assumptions about (and hence impose few restrictions on) the * type of the media being played, how and where it is stored, and how it is rendered. Rather than * implementing the loading and rendering of media directly, ExoPlayer implementations delegate this * work to components that are injected when a player is created or when it's prepared for playback. * Components common to all ExoPlayer implementations are: + * *

+ * *

An ExoPlayer can be built using the default components provided by the library, but may also * be built using custom implementations if non-standard behaviors are required. For example a * custom LoadControl could be injected to change the player's buffering strategy, or a custom - * Renderer could be injected to use a video codec not supported natively by Android. + * Renderer could be injected to add support for a video codec not supported natively by Android. * *

The concept of injecting components that implement pieces of player functionality is present * throughout the library. The default component implementations listed above delegate work to * further injected components. This allows many sub-components to be individually replaced with * custom implementations. For example the default MediaSource implementations require one or more * {@link DataSource} factories to be injected via their constructors. By providing a custom factory - * it's possible to load data from a non-standard source or through a different network stack. + * it's possible to load data from a non-standard source, or through a different network stack. * *

Threading model

- *

The figure below shows ExoPlayer's threading model.

- *

- * ExoPlayer's threading model - *

+ * + *

The figure below shows ExoPlayer's threading model. + * + *

ExoPlayer's
+ * threading model * *

*/ -public interface ExoPlayer { +public interface ExoPlayer extends Player { /** - * Listener of changes in player state. + * A builder for {@link ExoPlayer} instances. + * + *

See {@link #Builder(Context, Renderer...)} for the list of default values. */ - interface EventListener { + final class Builder { + + private final Renderer[] renderers; + + private Clock clock; + private TrackSelector trackSelector; + private LoadControl loadControl; + private BandwidthMeter bandwidthMeter; + private Looper looper; + private AnalyticsCollector analyticsCollector; + private boolean useLazyPreparation; + private boolean buildCalled; /** - * Called when the timeline and/or manifest has been refreshed. - *

- * Note that if the timeline has changed then a position discontinuity may also have occurred. - * For example the current period index may have changed as a result of periods being added or - * removed from the timeline. The will not be reported via a separate call to - * {@link #onPositionDiscontinuity()}. + * Creates a builder with a list of {@link Renderer Renderers}. * - * @param timeline The latest timeline. Never null, but may be empty. - * @param manifest The latest manifest. May be null. - */ - void onTimelineChanged(Timeline timeline, Object manifest); - - /** - * Called when the available or selected tracks change. + *

The builder uses the following default values: * - * @param trackGroups The available tracks. Never null, but may be of length zero. - * @param trackSelections The track selections for each {@link Renderer}. Never null and always - * of length {@link #getRendererCount()}, but may contain null elements. - */ - void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections); - - /** - * Called when the player starts or stops loading the source. + *

* - * @param isLoading Whether the source is currently being loaded. + * @param context A {@link Context}. + * @param renderers The {@link Renderer Renderers} to be used by the player. */ - void onLoadingChanged(boolean isLoading); - - /** - * Called when the value returned from either {@link #getPlayWhenReady()} or - * {@link #getPlaybackState()} changes. - * - * @param playWhenReady Whether playback will proceed when ready. - * @param playbackState One of the {@code STATE} constants defined in the {@link ExoPlayer} - * interface. - */ - void onPlayerStateChanged(boolean playWhenReady, int playbackState); - - /** - * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE} - * immediately after this method is called. The player instance can still be used, and - * {@link #release()} must still be called on the player should it no longer be required. - * - * @param error The error. - */ - void onPlayerError(ExoPlaybackException error); - - /** - * Called when a position discontinuity occurs without a change to the timeline. A position - * discontinuity occurs when the current window or period index changes (as a result of playback - * transitioning from one period in the timeline to the next), or when the playback position - * jumps within the period currently being played (as a result of a seek being performed, or - * when the source introduces a discontinuity internally). - *

- * When a position discontinuity occurs as a result of a change to the timeline this method is - * not called. {@link #onTimelineChanged(Timeline, Object)} is called in this case. - */ - void onPositionDiscontinuity(); - - /** - * Called when the current playback parameters change. The playback parameters may change due to - * a call to {@link ExoPlayer#setPlaybackParameters(PlaybackParameters)}, or the player itself - * may change them (for example, if audio playback switches to passthrough mode, where speed - * adjustment is no longer possible). - * - * @param playbackParameters The playback parameters. - */ - void onPlaybackParametersChanged(PlaybackParameters playbackParameters); - - } - - /** - * A component of an {@link ExoPlayer} that can receive messages on the playback thread. - *

- * Messages can be delivered to a component via {@link #sendMessages} and - * {@link #blockingSendMessages}. - */ - interface ExoPlayerComponent { - - /** - * Handles a message delivered to the component. Called on the playback thread. - * - * @param messageType The message type. - * @param message The message. - * @throws ExoPlaybackException If an error occurred whilst handling the message. - */ - void handleMessage(int messageType, Object message) throws ExoPlaybackException; - - } - - /** - * Defines a message and a target {@link ExoPlayerComponent} to receive it. - */ - final class ExoPlayerMessage { - - /** - * The target to receive the message. - */ - public final ExoPlayerComponent target; - /** - * The type of the message. - */ - public final int messageType; - /** - * The message. - */ - public final Object message; - - /** - * @param target The target of the message. - * @param messageType The message type. - * @param message The message. - */ - public ExoPlayerMessage(ExoPlayerComponent target, int messageType, Object message) { - this.target = target; - this.messageType = messageType; - this.message = message; + public Builder(Context context, Renderer... renderers) { + this( + renderers, + new DefaultTrackSelector(context), + new DefaultLoadControl(), + DefaultBandwidthMeter.getSingletonInstance(context), + Util.getLooper(), + new AnalyticsCollector(Clock.DEFAULT), + /* useLazyPreparation= */ true, + Clock.DEFAULT); } + /** + * Creates a builder with the specified custom components. + * + *

Note that this constructor is only useful if you try to ensure that ExoPlayer's default + * components can be removed by ProGuard or R8. For most components except renderers, there is + * only a marginal benefit of doing that. + * + * @param renderers The {@link Renderer Renderers} to be used by the player. + * @param trackSelector A {@link TrackSelector}. + * @param loadControl A {@link LoadControl}. + * @param bandwidthMeter A {@link BandwidthMeter}. + * @param looper A {@link Looper} that must be used for all calls to the player. + * @param analyticsCollector An {@link AnalyticsCollector}. + * @param useLazyPreparation Whether media sources should be initialized lazily. + * @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}. + */ + public Builder( + Renderer[] renderers, + TrackSelector trackSelector, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + Looper looper, + AnalyticsCollector analyticsCollector, + boolean useLazyPreparation, + Clock clock) { + Assertions.checkArgument(renderers.length > 0); + this.renderers = renderers; + this.trackSelector = trackSelector; + this.loadControl = loadControl; + this.bandwidthMeter = bandwidthMeter; + this.looper = looper; + this.analyticsCollector = analyticsCollector; + this.useLazyPreparation = useLazyPreparation; + this.clock = clock; + } + + /** + * Sets the {@link TrackSelector} that will be used by the player. + * + * @param trackSelector A {@link TrackSelector}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setTrackSelector(TrackSelector trackSelector) { + Assertions.checkState(!buildCalled); + this.trackSelector = trackSelector; + return this; + } + + /** + * Sets the {@link LoadControl} that will be used by the player. + * + * @param loadControl A {@link LoadControl}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setLoadControl(LoadControl loadControl) { + Assertions.checkState(!buildCalled); + this.loadControl = loadControl; + return this; + } + + /** + * Sets the {@link BandwidthMeter} that will be used by the player. + * + * @param bandwidthMeter A {@link BandwidthMeter}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setBandwidthMeter(BandwidthMeter bandwidthMeter) { + Assertions.checkState(!buildCalled); + this.bandwidthMeter = bandwidthMeter; + return this; + } + + /** + * Sets the {@link Looper} that must be used for all calls to the player and that is used to + * call listeners on. + * + * @param looper A {@link Looper}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setLooper(Looper looper) { + Assertions.checkState(!buildCalled); + this.looper = looper; + return this; + } + + /** + * Sets the {@link AnalyticsCollector} that will collect and forward all player events. + * + * @param analyticsCollector An {@link AnalyticsCollector}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setAnalyticsCollector(AnalyticsCollector analyticsCollector) { + Assertions.checkState(!buildCalled); + this.analyticsCollector = analyticsCollector; + return this; + } + + /** + * Sets whether media sources should be initialized lazily. + * + *

If false, all initial preparation steps (e.g., manifest loads) happen immediately. If + * true, these initial preparations are triggered only when the player starts buffering the + * media. + * + * @param useLazyPreparation Whether to use lazy preparation. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setUseLazyPreparation(boolean useLazyPreparation) { + Assertions.checkState(!buildCalled); + this.useLazyPreparation = useLazyPreparation; + return this; + } + + /** + * Sets the {@link Clock} that will be used by the player. Should only be set for testing + * purposes. + * + * @param clock A {@link Clock}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + @VisibleForTesting + public Builder setClock(Clock clock) { + Assertions.checkState(!buildCalled); + this.clock = clock; + return this; + } + + /** + * Builds an {@link ExoPlayer} instance. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public ExoPlayer build() { + Assertions.checkState(!buildCalled); + buildCalled = true; + return new ExoPlayerImpl( + renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); + } } - /** - * The player does not have a source to play, so it is neither buffering nor ready to play. - */ - int STATE_IDLE = 1; - /** - * The player not able to immediately play from the current position. The cause is - * {@link Renderer} specific, but this state typically occurs when more data needs to be - * loaded to be ready to play, or more data needs to be buffered for playback to resume. - */ - int STATE_BUFFERING = 2; - /** - * The player is able to immediately play from the current position. The player will be playing if - * {@link #getPlayWhenReady()} returns true, and paused otherwise. - */ - int STATE_READY = 3; - /** - * The player has finished playing the media. - */ - int STATE_ENDED = 4; + /** Returns the {@link Looper} associated with the playback thread. */ + Looper getPlaybackLooper(); /** - * Register a listener to receive events from the player. The listener's methods will be called on - * the thread that was used to construct the player. - * - * @param listener The listener to register. + * Retries a failed or stopped playback. Does nothing if the player has been reset, or if playback + * has not failed or been stopped. */ - void addListener(EventListener listener); + void retry(); /** - * Unregister a listener. The listener will no longer receive events from the player. - * - * @param listener The listener to unregister. - */ - void removeListener(EventListener listener); - - /** - * Returns the current state of the player. - * - * @return One of the {@code STATE} constants defined in this interface. - */ - int getPlaybackState(); - - /** - * Prepares the player to play the provided {@link MediaSource}. Equivalent to - * {@code prepare(mediaSource, true, true)}. + * Prepares the player to play the provided {@link MediaSource}. Equivalent to {@code + * prepare(mediaSource, true, true)}. */ void prepare(MediaSource mediaSource); @@ -294,202 +352,53 @@ public interface ExoPlayer { void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); /** - * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. - *

- * If the player is already in the ready state then this method can be used to pause and resume - * playback. + * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message + * will be delivered immediately without blocking on the playback thread. The default {@link + * PlayerMessage#getType()} is 0 and the default {@link PlayerMessage#getPayload()} is null. If a + * position is specified with {@link PlayerMessage#setPosition(long)}, the message will be + * delivered at this position in the current window defined by {@link #getCurrentWindowIndex()}. + * Alternatively, the message can be sent at a specific window using {@link + * PlayerMessage#setPosition(int, long)}. + */ + PlayerMessage createMessage(PlayerMessage.Target target); + + /** + * Sets the parameters that control how seek operations are performed. * - * @param playWhenReady Whether playback should proceed when ready. + * @param seekParameters The seek parameters, or {@code null} to use the defaults. */ - void setPlayWhenReady(boolean playWhenReady); + void setSeekParameters(@Nullable SeekParameters seekParameters); + + /** Returns the currently active {@link SeekParameters} of the player. */ + SeekParameters getSeekParameters(); /** - * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. + * Sets whether the player is allowed to keep holding limited resources such as video decoders, + * even when in the idle state. By doing so, the player may be able to reduce latency when + * starting to play another piece of content for which the same resources are required. * - * @return Whether playback will proceed when ready. - */ - boolean getPlayWhenReady(); - - /** - * Whether the player is currently loading the source. + *

This mode should be used with caution, since holding limited resources may prevent other + * players of media components from acquiring them. It should only be enabled when both + * of the following conditions are true: * - * @return Whether the player is currently loading the source. - */ - boolean isLoading(); - - /** - * Seeks to the default position associated with the current window. The position can depend on - * the type of source passed to {@link #prepare(MediaSource)}. For live streams it will typically - * be the live edge of the window. For other streams it will typically be the start of the window. - */ - void seekToDefaultPosition(); - - /** - * Seeks to the default position associated with the specified window. The position can depend on - * the type of source passed to {@link #prepare(MediaSource)}. For live streams it will typically - * be the live edge of the window. For other streams it will typically be the start of the window. + *

* - * @param windowIndex The index of the window whose associated default position should be seeked - * to. - */ - void seekToDefaultPosition(int windowIndex); - - /** - * Seeks to a position specified in milliseconds in the current window. + *

Note that foreground mode is not useful for switching between content without gaps + * between the playbacks. For this use case {@link #stop} does not need to be called, and simply + * calling {@link #prepare} for the new media will cause limited resources to be retained even if + * foreground mode is not enabled. * - * @param positionMs The seek position in the current window, or {@link C#TIME_UNSET} to seek to - * the window's default position. - */ - void seekTo(long positionMs); - - /** - * Seeks to a position specified in milliseconds in the specified window. + *

If foreground mode is enabled, it's the application's responsibility to disable it when the + * conditions described above no longer hold. * - * @param windowIndex The index of the window. - * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to - * the window's default position. + * @param foregroundMode Whether the player is allowed to keep limited resources even when in the + * idle state. */ - void seekTo(int windowIndex, long positionMs); - - /** - * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the - * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment. - *

- * Playback parameters changes may cause the player to buffer. - * {@link EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever - * the currently active playback parameters change. When that listener is called, the parameters - * passed to it may not match {@code playbackParameters}. For example, the chosen speed or pitch - * may be out of range, in which case they are constrained to a set of permitted values. If it is - * not possible to change the playback parameters, the listener will not be invoked. - * - * @param playbackParameters The playback parameters, or {@code null} to use the defaults. - */ - void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters); - - /** - * Returns the currently active playback parameters. - * - * @see EventListener#onPlaybackParametersChanged(PlaybackParameters) - */ - PlaybackParameters getPlaybackParameters(); - - /** - * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention - * is to pause playback. - *

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

- * Calling this method does not reset the playback position. - */ - void stop(); - - /** - * Releases the player. This method must be called when the player is no longer required. The - * player must not be used after calling this method. - */ - void release(); - - /** - * Sends messages to their target components. The messages are delivered on the playback thread. - * If a component throws an {@link ExoPlaybackException} then it is propagated out of the player - * as an error. - * - * @param messages The messages to be sent. - */ - void sendMessages(ExoPlayerMessage... messages); - - /** - * Variant of {@link #sendMessages(ExoPlayerMessage...)} that blocks until after the messages have - * been delivered. - * - * @param messages The messages to be sent. - */ - void blockingSendMessages(ExoPlayerMessage... messages); - - /** - * Returns the number of renderers. - */ - int getRendererCount(); - - /** - * Returns the track type that the renderer at a given index handles. - * - * @see Renderer#getTrackType() - * @param index The index of the renderer. - * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}. - */ - int getRendererType(int index); - - /** - * Returns the available track groups. - */ - TrackGroupArray getCurrentTrackGroups(); - - /** - * Returns the current track selections for each renderer. - */ - TrackSelectionArray getCurrentTrackSelections(); - - /** - * Returns the current manifest. The type depends on the {@link MediaSource} passed to - * {@link #prepare}. May be null. - */ - Object getCurrentManifest(); - - /** - * Returns the current {@link Timeline}. Never null, but may be empty. - */ - Timeline getCurrentTimeline(); - - /** - * Returns the index of the period currently being played. - */ - int getCurrentPeriodIndex(); - - /** - * Returns the index of the window currently being played. - */ - int getCurrentWindowIndex(); - - /** - * Returns the duration of the current window in milliseconds, or {@link C#TIME_UNSET} if the - * duration is not known. - */ - long getDuration(); - - /** - * Returns the playback position in the current window, in milliseconds. - */ - long getCurrentPosition(); - - /** - * Returns an estimate of the position in the current window up to which data is buffered, in - * milliseconds. - */ - long getBufferedPosition(); - - /** - * Returns an estimate of the percentage in the current window up to which data is buffered, or 0 - * if no estimate is available. - */ - int getBufferedPercentage(); - - /** - * Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is - * empty. - * - * @see Timeline.Window#isDynamic - */ - boolean isCurrentWindowDynamic(); - - /** - * Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is - * empty. - * - * @see Timeline.Window#isSeekable - */ - boolean isCurrentWindowSeekable(); - + void setForegroundMode(boolean foregroundMode); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerFactory.java index ddd8ab734bb3..a2e89fc3ccc4 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -17,158 +17,334 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2; import android.content.Context; import android.os.Looper; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsCollector; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelector; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; -/** - * A factory for {@link ExoPlayer} instances. - */ +/** @deprecated Use {@link SimpleExoPlayer.Builder} or {@link ExoPlayer.Builder} instead. */ +@Deprecated public final class ExoPlayerFactory { private ExoPlayerFactory() {} /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. - * - * @param context A {@link Context}. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}. + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. */ @Deprecated - public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, - LoadControl loadControl) { - RenderersFactory renderersFactory = new DefaultRenderersFactory(context); - return newSimpleInstance(renderersFactory, trackSelector, loadControl); - } - - /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. Available extension renderers are not used. - * - * @param context A {@link Context}. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. - * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}. - */ - @Deprecated - public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, - LoadControl loadControl, DrmSessionManager drmSessionManager) { - RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager); - return newSimpleInstance(renderersFactory, trackSelector, loadControl); - } - - /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. - * - * @param context A {@link Context}. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. - * @param extensionRendererMode The extension renderer mode, which determines if and how available - * extension renderers are used. Note that extensions must be included in the application - * build for them to be considered available. - * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}. - */ - @Deprecated - public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, - LoadControl loadControl, DrmSessionManager drmSessionManager, + @SuppressWarnings("deprecation") + public static SimpleExoPlayer newSimpleInstance( + Context context, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) { - RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager, - extensionRendererMode); - return newSimpleInstance(renderersFactory, trackSelector, loadControl); + RenderersFactory renderersFactory = + new DefaultRenderersFactory(context).setExtensionRendererMode(extensionRendererMode); + return newSimpleInstance( + context, renderersFactory, trackSelector, loadControl, drmSessionManager); } /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. - * - * @param context A {@link Context}. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. - * @param extensionRendererMode The extension renderer mode, which determines if and how available - * extension renderers are used. Note that extensions must be included in the application - * build for them to be considered available. - * @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to - * seamlessly join an ongoing playback. - * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}. + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. */ @Deprecated - public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, - LoadControl loadControl, DrmSessionManager drmSessionManager, + @SuppressWarnings("deprecation") + public static SimpleExoPlayer newSimpleInstance( + Context context, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { - RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager, - extensionRendererMode, allowedVideoJoiningTimeMs); - return newSimpleInstance(renderersFactory, trackSelector, loadControl); + RenderersFactory renderersFactory = + new DefaultRenderersFactory(context) + .setExtensionRendererMode(extensionRendererMode) + .setAllowedVideoJoiningTimeMs(allowedVideoJoiningTimeMs); + return newSimpleInstance( + context, renderersFactory, trackSelector, loadControl, drmSessionManager); } - /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. - * - * @param context A {@link Context}. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") + public static SimpleExoPlayer newSimpleInstance(Context context) { + return newSimpleInstance(context, new DefaultTrackSelector(context)); + } + + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector) { - return newSimpleInstance(new DefaultRenderersFactory(context), trackSelector); + return newSimpleInstance(context, new DefaultRenderersFactory(context), trackSelector); + } + + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") + public static SimpleExoPlayer newSimpleInstance( + Context context, RenderersFactory renderersFactory, TrackSelector trackSelector) { + return newSimpleInstance(context, renderersFactory, trackSelector, new DefaultLoadControl()); + } + + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") + public static SimpleExoPlayer newSimpleInstance( + Context context, TrackSelector trackSelector, LoadControl loadControl) { + RenderersFactory renderersFactory = new DefaultRenderersFactory(context); + return newSimpleInstance(context, renderersFactory, trackSelector, loadControl); } /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. - * - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. */ - public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory, - TrackSelector trackSelector) { - return newSimpleInstance(renderersFactory, trackSelector, new DefaultLoadControl()); + @Deprecated + @SuppressWarnings("deprecation") + public static SimpleExoPlayer newSimpleInstance( + Context context, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager) { + RenderersFactory renderersFactory = new DefaultRenderersFactory(context); + return newSimpleInstance( + context, renderersFactory, trackSelector, loadControl, drmSessionManager); } /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. - * - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. */ - public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory, - TrackSelector trackSelector, LoadControl loadControl) { - return new SimpleExoPlayer(renderersFactory, trackSelector, loadControl); + @Deprecated + @SuppressWarnings("deprecation") + public static SimpleExoPlayer newSimpleInstance( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + @Nullable DrmSessionManager drmSessionManager) { + return newSimpleInstance( + context, renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager); } - /** - * Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. - * - * @param renderers The {@link Renderer}s that will be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - */ - public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector) { - return newInstance(renderers, trackSelector, new DefaultLoadControl()); - } - - /** - * Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. - * - * @param renderers The {@link Renderer}s that will be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - */ - public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector, + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") + public static SimpleExoPlayer newSimpleInstance( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, LoadControl loadControl) { - return new ExoPlayerImpl(renderers, trackSelector, loadControl); + return newSimpleInstance( + context, + renderersFactory, + trackSelector, + loadControl, + /* drmSessionManager= */ null, + Util.getLooper()); } + /** + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. + */ + @Deprecated + @SuppressWarnings("deprecation") + public static SimpleExoPlayer newSimpleInstance( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager) { + return newSimpleInstance( + context, renderersFactory, trackSelector, loadControl, drmSessionManager, Util.getLooper()); + } + + /** + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. + */ + @Deprecated + @SuppressWarnings("deprecation") + public static SimpleExoPlayer newSimpleInstance( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, + BandwidthMeter bandwidthMeter) { + return newSimpleInstance( + context, + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + bandwidthMeter, + new AnalyticsCollector(Clock.DEFAULT), + Util.getLooper()); + } + + /** + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. + */ + @Deprecated + @SuppressWarnings("deprecation") + public static SimpleExoPlayer newSimpleInstance( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, + AnalyticsCollector analyticsCollector) { + return newSimpleInstance( + context, + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + analyticsCollector, + Util.getLooper()); + } + + /** + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. + */ + @Deprecated + @SuppressWarnings("deprecation") + public static SimpleExoPlayer newSimpleInstance( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, + Looper looper) { + return newSimpleInstance( + context, + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + new AnalyticsCollector(Clock.DEFAULT), + looper); + } + + /** + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. + */ + @Deprecated + @SuppressWarnings("deprecation") + public static SimpleExoPlayer newSimpleInstance( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, + AnalyticsCollector analyticsCollector, + Looper looper) { + return newSimpleInstance( + context, + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + DefaultBandwidthMeter.getSingletonInstance(context), + analyticsCollector, + looper); + } + + /** + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. + */ + @SuppressWarnings("deprecation") + @Deprecated + public static SimpleExoPlayer newSimpleInstance( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, + BandwidthMeter bandwidthMeter, + AnalyticsCollector analyticsCollector, + Looper looper) { + return new SimpleExoPlayer( + context, + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + bandwidthMeter, + analyticsCollector, + Clock.DEFAULT, + looper); + } + + /** @deprecated Use {@link ExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") + public static ExoPlayer newInstance( + Context context, Renderer[] renderers, TrackSelector trackSelector) { + return newInstance(context, renderers, trackSelector, new DefaultLoadControl()); + } + + /** @deprecated Use {@link ExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") + public static ExoPlayer newInstance( + Context context, Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { + return newInstance(context, renderers, trackSelector, loadControl, Util.getLooper()); + } + + /** @deprecated Use {@link ExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") + public static ExoPlayer newInstance( + Context context, + Renderer[] renderers, + TrackSelector trackSelector, + LoadControl loadControl, + Looper looper) { + return newInstance( + context, + renderers, + trackSelector, + loadControl, + DefaultBandwidthMeter.getSingletonInstance(context), + looper); + } + + /** @deprecated Use {@link ExoPlayer.Builder} instead. */ + @Deprecated + public static ExoPlayer newInstance( + Context context, + Renderer[] renderers, + TrackSelector trackSelector, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + Looper looper) { + return new ExoPlayerImpl( + renderers, trackSelector, loadControl, bandwidthMeter, Clock.DEFAULT, looper); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImpl.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImpl.java index edfe845c4b96..eb9eaae2cfaa 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -19,47 +19,61 @@ import android.annotation.SuppressLint; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.support.annotation.Nullable; -import android.util.Log; -import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo; -import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo; +import android.util.Pair; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.PlayerMessage.Target; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelector; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectorResult; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; -import java.util.concurrent.CopyOnWriteArraySet; +import java.util.ArrayDeque; +import java.util.concurrent.CopyOnWriteArrayList; /** - * An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayerFactory}. + * An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayer.Builder}. */ -/* package */ final class ExoPlayerImpl implements ExoPlayer { +/* package */ final class ExoPlayerImpl extends BasePlayer implements ExoPlayer { private static final String TAG = "ExoPlayerImpl"; + /** + * This empty track selector result can only be used for {@link PlaybackInfo#trackSelectorResult} + * when the player does not have any track selection made (such as when player is reset, or when + * player seeks to an unprepared period). It will not be used as result of any {@link + * TrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray, MediaPeriodId, Timeline)} + * operation. + */ + /* package */ final TrackSelectorResult emptyTrackSelectorResult; + private final Renderer[] renderers; private final TrackSelector trackSelector; - private final TrackSelectionArray emptyTrackSelections; private final Handler eventHandler; private final ExoPlayerImplInternal internalPlayer; - private final CopyOnWriteArraySet listeners; - private final Timeline.Window window; + private final Handler internalPlayerHandler; + private final CopyOnWriteArrayList listeners; private final Timeline.Period period; + private final ArrayDeque pendingListenerNotifications; - private boolean tracksSelected; + private MediaSource mediaSource; private boolean playWhenReady; - private int playbackState; - private int pendingSeekAcks; - private int pendingPrepareAcks; - private boolean isLoading; - private Timeline timeline; - private Object manifest; - private TrackGroupArray trackGroups; - private TrackSelectionArray trackSelections; + @PlaybackSuppressionReason private int playbackSuppressionReason; + @RepeatMode private int repeatMode; + private boolean shuffleModeEnabled; + private int pendingOperationAcks; + private boolean hasPendingPrepare; + private boolean hasPendingSeek; + private boolean foregroundMode; + private int pendingSetPlaybackParametersAcks; private PlaybackParameters playbackParameters; + private SeekParameters seekParameters; // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; @@ -75,86 +89,200 @@ import java.util.concurrent.CopyOnWriteArraySet; * @param renderers The {@link Renderer}s that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. + * @param clock The {@link Clock} that will be used by the instance. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. */ @SuppressLint("HandlerLeak") - public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { - Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION_SLASHY + " [" + Util.DEVICE_DEBUG_INFO + "]"); + public ExoPlayerImpl( + Renderer[] renderers, + TrackSelector trackSelector, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + Clock clock, + Looper looper) { + Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " [" + + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]"); Assertions.checkState(renderers.length > 0); this.renderers = Assertions.checkNotNull(renderers); this.trackSelector = Assertions.checkNotNull(trackSelector); this.playWhenReady = false; - this.playbackState = STATE_IDLE; - this.listeners = new CopyOnWriteArraySet<>(); - emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]); - timeline = Timeline.EMPTY; - window = new Timeline.Window(); + this.repeatMode = Player.REPEAT_MODE_OFF; + this.shuffleModeEnabled = false; + this.listeners = new CopyOnWriteArrayList<>(); + emptyTrackSelectorResult = + new TrackSelectorResult( + new RendererConfiguration[renderers.length], + new TrackSelection[renderers.length], + null); period = new Timeline.Period(); - trackGroups = TrackGroupArray.EMPTY; - trackSelections = emptyTrackSelections; playbackParameters = PlaybackParameters.DEFAULT; - eventHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - ExoPlayerImpl.this.handleEvent(msg); + seekParameters = SeekParameters.DEFAULT; + playbackSuppressionReason = PLAYBACK_SUPPRESSION_REASON_NONE; + eventHandler = + new Handler(looper) { + @Override + public void handleMessage(Message msg) { + ExoPlayerImpl.this.handleEvent(msg); + } + }; + playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult); + pendingListenerNotifications = new ArrayDeque<>(); + internalPlayer = + new ExoPlayerImplInternal( + renderers, + trackSelector, + emptyTrackSelectorResult, + loadControl, + bandwidthMeter, + playWhenReady, + repeatMode, + shuffleModeEnabled, + eventHandler, + clock); + internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); + } + + @Override + @Nullable + public AudioComponent getAudioComponent() { + return null; + } + + @Override + @Nullable + public VideoComponent getVideoComponent() { + return null; + } + + @Override + @Nullable + public TextComponent getTextComponent() { + return null; + } + + @Override + @Nullable + public MetadataComponent getMetadataComponent() { + return null; + } + + @Override + public Looper getPlaybackLooper() { + return internalPlayer.getPlaybackLooper(); + } + + @Override + public Looper getApplicationLooper() { + return eventHandler.getLooper(); + } + + @Override + public void addListener(Player.EventListener listener) { + listeners.addIfAbsent(new ListenerHolder(listener)); + } + + @Override + public void removeListener(Player.EventListener listener) { + for (ListenerHolder listenerHolder : listeners) { + if (listenerHolder.listener.equals(listener)) { + listenerHolder.release(); + listeners.remove(listenerHolder); } - }; - playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0); - internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady, - eventHandler, playbackInfo, this); - } - - @Override - public void addListener(EventListener listener) { - listeners.add(listener); - } - - @Override - public void removeListener(EventListener listener) { - listeners.remove(listener); + } } @Override + @State public int getPlaybackState() { - return playbackState; + return playbackInfo.playbackState; + } + + @Override + @PlaybackSuppressionReason + public int getPlaybackSuppressionReason() { + return playbackSuppressionReason; + } + + @Override + @Nullable + public ExoPlaybackException getPlaybackError() { + return playbackInfo.playbackError; + } + + @Override + public void retry() { + if (mediaSource != null && playbackInfo.playbackState == Player.STATE_IDLE) { + prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); + } } @Override public void prepare(MediaSource mediaSource) { - prepare(mediaSource, true, true); + prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); } @Override public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - if (resetState) { - if (!timeline.isEmpty() || manifest != null) { - timeline = Timeline.EMPTY; - manifest = null; - for (EventListener listener : listeners) { - listener.onTimelineChanged(timeline, manifest); - } - } - if (tracksSelected) { - tracksSelected = false; - trackGroups = TrackGroupArray.EMPTY; - trackSelections = emptyTrackSelections; - trackSelector.onSelectionActivated(null); - for (EventListener listener : listeners) { - listener.onTracksChanged(trackGroups, trackSelections); - } - } - } - pendingPrepareAcks++; - internalPlayer.prepare(mediaSource, resetPosition); + this.mediaSource = mediaSource; + PlaybackInfo playbackInfo = + getResetPlaybackInfo( + resetPosition, + resetState, + /* resetError= */ true, + /* playbackState= */ Player.STATE_BUFFERING); + // Trigger internal prepare first before updating the playback info and notifying external + // listeners to ensure that new operations issued in the listener notifications reach the + // player after this prepare. The internal player can't change the playback info immediately + // because it uses a callback. + hasPendingPrepare = true; + pendingOperationAcks++; + internalPlayer.prepare(mediaSource, resetPosition, resetState); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + TIMELINE_CHANGE_REASON_RESET, + /* seekProcessed= */ false); } + @Override public void setPlayWhenReady(boolean playWhenReady) { - if (this.playWhenReady != playWhenReady) { - this.playWhenReady = playWhenReady; - internalPlayer.setPlayWhenReady(playWhenReady); - for (EventListener listener : listeners) { - listener.onPlayerStateChanged(playWhenReady, playbackState); - } + setPlayWhenReady(playWhenReady, PLAYBACK_SUPPRESSION_REASON_NONE); + } + + public void setPlayWhenReady( + boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason) { + boolean oldIsPlaying = isPlaying(); + boolean oldInternalPlayWhenReady = + this.playWhenReady && this.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; + boolean internalPlayWhenReady = + playWhenReady && playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; + if (oldInternalPlayWhenReady != internalPlayWhenReady) { + internalPlayer.setPlayWhenReady(internalPlayWhenReady); + } + boolean playWhenReadyChanged = this.playWhenReady != playWhenReady; + boolean suppressionReasonChanged = this.playbackSuppressionReason != playbackSuppressionReason; + this.playWhenReady = playWhenReady; + this.playbackSuppressionReason = playbackSuppressionReason; + boolean isPlaying = isPlaying(); + boolean isPlayingChanged = oldIsPlaying != isPlaying; + if (playWhenReadyChanged || suppressionReasonChanged || isPlayingChanged) { + int playbackState = playbackInfo.playbackState; + notifyListeners( + listener -> { + if (playWhenReadyChanged) { + listener.onPlayerStateChanged(playWhenReady, playbackState); + } + if (suppressionReasonChanged) { + listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason); + } + if (isPlayingChanged) { + listener.onIsPlayingChanged(isPlaying); + } + }); } } @@ -163,59 +291,75 @@ import java.util.concurrent.CopyOnWriteArraySet; return playWhenReady; } + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + if (this.repeatMode != repeatMode) { + this.repeatMode = repeatMode; + internalPlayer.setRepeatMode(repeatMode); + notifyListeners(listener -> listener.onRepeatModeChanged(repeatMode)); + } + } + + @Override + public @RepeatMode int getRepeatMode() { + return repeatMode; + } + + @Override + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + if (this.shuffleModeEnabled != shuffleModeEnabled) { + this.shuffleModeEnabled = shuffleModeEnabled; + internalPlayer.setShuffleModeEnabled(shuffleModeEnabled); + notifyListeners(listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled)); + } + } + + @Override + public boolean getShuffleModeEnabled() { + return shuffleModeEnabled; + } + @Override public boolean isLoading() { - return isLoading; - } - - @Override - public void seekToDefaultPosition() { - seekToDefaultPosition(getCurrentWindowIndex()); - } - - @Override - public void seekToDefaultPosition(int windowIndex) { - seekTo(windowIndex, C.TIME_UNSET); - } - - @Override - public void seekTo(long positionMs) { - seekTo(getCurrentWindowIndex(), positionMs); + return playbackInfo.isLoading; } @Override public void seekTo(int windowIndex, long positionMs) { + Timeline timeline = playbackInfo.timeline; if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); } - pendingSeekAcks++; + hasPendingSeek = true; + pendingOperationAcks++; + if (isPlayingAd()) { + // TODO: Investigate adding support for seeking during ads. This is complicated to do in + // general because the midroll ad preceding the seek destination must be played before the + // content position can be played, if a different ad is playing at the moment. + Log.w(TAG, "seekTo ignored because an ad is playing"); + eventHandler + .obtainMessage( + ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED, + /* operationAcks */ 1, + /* positionDiscontinuityReason */ C.INDEX_UNSET, + playbackInfo) + .sendToTarget(); + return; + } maskingWindowIndex = windowIndex; if (timeline.isEmpty()) { + maskingWindowPositionMs = positionMs == C.TIME_UNSET ? 0 : positionMs; maskingPeriodIndex = 0; } else { - timeline.getWindow(windowIndex, window); - long resolvedPositionMs = - positionMs == C.TIME_UNSET ? window.getDefaultPositionUs() : positionMs; - int periodIndex = window.firstPeriodIndex; - long periodPositionUs = window.getPositionInFirstPeriodUs() + C.msToUs(resolvedPositionMs); - long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs(); - while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs - && periodIndex < window.lastPeriodIndex) { - periodPositionUs -= periodDurationUs; - periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs(); - } - maskingPeriodIndex = periodIndex; - } - if (positionMs == C.TIME_UNSET) { - maskingWindowPositionMs = 0; - internalPlayer.seekTo(timeline, windowIndex, C.TIME_UNSET); - } else { - maskingWindowPositionMs = positionMs; - internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs)); - for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(); - } + long windowPositionUs = positionMs == C.TIME_UNSET + ? timeline.getWindow(windowIndex, window).getDefaultPositionUs() : C.msToUs(positionMs); + Pair periodUidAndPosition = + timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); + maskingWindowPositionMs = C.usToMs(windowPositionUs); + maskingPeriodIndex = timeline.getIndexOfPeriod(periodUidAndPosition.first); } + internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs)); + notifyListeners(listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)); } @Override @@ -223,7 +367,14 @@ import java.util.concurrent.CopyOnWriteArraySet; if (playbackParameters == null) { playbackParameters = PlaybackParameters.DEFAULT; } + if (this.playbackParameters.equals(playbackParameters)) { + return; + } + pendingSetPlaybackParametersAcks++; + this.playbackParameters = playbackParameters; internalPlayer.setPlaybackParameters(playbackParameters); + PlaybackParameters playbackParametersToNotify = playbackParameters; + notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParametersToNotify)); } @Override @@ -232,92 +383,184 @@ import java.util.concurrent.CopyOnWriteArraySet; } @Override - public void stop() { - internalPlayer.stop(); + public void setSeekParameters(@Nullable SeekParameters seekParameters) { + if (seekParameters == null) { + seekParameters = SeekParameters.DEFAULT; + } + if (!this.seekParameters.equals(seekParameters)) { + this.seekParameters = seekParameters; + internalPlayer.setSeekParameters(seekParameters); + } + } + + @Override + public SeekParameters getSeekParameters() { + return seekParameters; + } + + @Override + public void setForegroundMode(boolean foregroundMode) { + if (this.foregroundMode != foregroundMode) { + this.foregroundMode = foregroundMode; + internalPlayer.setForegroundMode(foregroundMode); + } + } + + @Override + public void stop(boolean reset) { + if (reset) { + mediaSource = null; + } + PlaybackInfo playbackInfo = + getResetPlaybackInfo( + /* resetPosition= */ reset, + /* resetState= */ reset, + /* resetError= */ reset, + /* playbackState= */ Player.STATE_IDLE); + // Trigger internal stop first before updating the playback info and notifying external + // listeners to ensure that new operations issued in the listener notifications reach the + // player after this stop. The internal player can't change the playback info immediately + // because it uses a callback. + pendingOperationAcks++; + internalPlayer.stop(reset); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + TIMELINE_CHANGE_REASON_RESET, + /* seekProcessed= */ false); } @Override public void release() { + Log.i(TAG, "Release " + Integer.toHexString(System.identityHashCode(this)) + " [" + + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] [" + + ExoPlayerLibraryInfo.registeredModules() + "]"); + mediaSource = null; internalPlayer.release(); eventHandler.removeCallbacksAndMessages(null); + playbackInfo = + getResetPlaybackInfo( + /* resetPosition= */ false, + /* resetState= */ false, + /* resetError= */ false, + /* playbackState= */ Player.STATE_IDLE); } @Override - public void sendMessages(ExoPlayerMessage... messages) { - internalPlayer.sendMessages(messages); - } - - @Override - public void blockingSendMessages(ExoPlayerMessage... messages) { - internalPlayer.blockingSendMessages(messages); + public PlayerMessage createMessage(Target target) { + return new PlayerMessage( + internalPlayer, + target, + playbackInfo.timeline, + getCurrentWindowIndex(), + internalPlayerHandler); } @Override public int getCurrentPeriodIndex() { - if (timeline.isEmpty() || pendingSeekAcks > 0) { + if (shouldMaskPosition()) { return maskingPeriodIndex; } else { - return playbackInfo.periodIndex; + return playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid); } } @Override public int getCurrentWindowIndex() { - if (timeline.isEmpty() || pendingSeekAcks > 0) { + if (shouldMaskPosition()) { return maskingWindowIndex; } else { - return timeline.getPeriod(playbackInfo.periodIndex, period).windowIndex; + return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period) + .windowIndex; } } @Override public long getDuration() { - if (timeline.isEmpty()) { - return C.TIME_UNSET; + if (isPlayingAd()) { + MediaPeriodId periodId = playbackInfo.periodId; + playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period); + long adDurationUs = period.getAdDurationUs(periodId.adGroupIndex, periodId.adIndexInAdGroup); + return C.usToMs(adDurationUs); } - return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); + return getContentDuration(); } @Override public long getCurrentPosition() { - if (timeline.isEmpty() || pendingSeekAcks > 0) { + if (shouldMaskPosition()) { return maskingWindowPositionMs; + } else if (playbackInfo.periodId.isAd()) { + return C.usToMs(playbackInfo.positionUs); } else { - timeline.getPeriod(playbackInfo.periodIndex, period); - return period.getPositionInWindowMs() + C.usToMs(playbackInfo.positionUs); + return periodPositionUsToWindowPositionMs(playbackInfo.periodId, playbackInfo.positionUs); } } @Override public long getBufferedPosition() { - // TODO - Implement this properly. - if (timeline.isEmpty() || pendingSeekAcks > 0) { - return maskingWindowPositionMs; + if (isPlayingAd()) { + return playbackInfo.loadingMediaPeriodId.equals(playbackInfo.periodId) + ? C.usToMs(playbackInfo.bufferedPositionUs) + : getDuration(); + } + return getContentBufferedPosition(); + } + + @Override + public long getTotalBufferedDuration() { + return C.usToMs(playbackInfo.totalBufferedDurationUs); + } + + @Override + public boolean isPlayingAd() { + return !shouldMaskPosition() && playbackInfo.periodId.isAd(); + } + + @Override + public int getCurrentAdGroupIndex() { + return isPlayingAd() ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET; + } + + @Override + public int getCurrentAdIndexInAdGroup() { + return isPlayingAd() ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET; + } + + @Override + public long getContentPosition() { + if (isPlayingAd()) { + playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period); + return playbackInfo.contentPositionUs == C.TIME_UNSET + ? playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDefaultPositionMs() + : period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs); } else { - timeline.getPeriod(playbackInfo.periodIndex, period); - return period.getPositionInWindowMs() + C.usToMs(playbackInfo.bufferedPositionUs); + return getCurrentPosition(); } } @Override - public int getBufferedPercentage() { - if (timeline.isEmpty()) { - return 0; + public long getContentBufferedPosition() { + if (shouldMaskPosition()) { + return maskingWindowPositionMs; } - long bufferedPosition = getBufferedPosition(); - long duration = getDuration(); - return (bufferedPosition == C.TIME_UNSET || duration == C.TIME_UNSET) ? 0 - : (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration); - } - - @Override - public boolean isCurrentWindowDynamic() { - return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isDynamic; - } - - @Override - public boolean isCurrentWindowSeekable() { - return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isSeekable; + if (playbackInfo.loadingMediaPeriodId.windowSequenceNumber + != playbackInfo.periodId.windowSequenceNumber) { + return playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); + } + long contentBufferedPositionUs = playbackInfo.bufferedPositionUs; + if (playbackInfo.loadingMediaPeriodId.isAd()) { + Timeline.Period loadingPeriod = + playbackInfo.timeline.getPeriodByUid(playbackInfo.loadingMediaPeriodId.periodUid, period); + contentBufferedPositionUs = + loadingPeriod.getAdGroupTimeUs(playbackInfo.loadingMediaPeriodId.adGroupIndex); + if (contentBufferedPositionUs == C.TIME_END_OF_SOURCE) { + contentBufferedPositionUs = loadingPeriod.durationUs; + } + } + return periodPositionUsToWindowPositionMs( + playbackInfo.loadingMediaPeriodId, contentBufferedPositionUs); } @Override @@ -332,111 +575,274 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public TrackGroupArray getCurrentTrackGroups() { - return trackGroups; + return playbackInfo.trackGroups; } @Override public TrackSelectionArray getCurrentTrackSelections() { - return trackSelections; + return playbackInfo.trackSelectorResult.selections; } @Override public Timeline getCurrentTimeline() { - return timeline; - } - - @Override - public Object getCurrentManifest() { - return manifest; + return playbackInfo.timeline; } // Not private so it can be called from an inner class without going through a thunk method. /* package */ void handleEvent(Message msg) { switch (msg.what) { - case ExoPlayerImplInternal.MSG_PREPARE_ACK: { - pendingPrepareAcks--; + case ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED: + handlePlaybackInfo( + (PlaybackInfo) msg.obj, + /* operationAcks= */ msg.arg1, + /* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET, + /* positionDiscontinuityReason= */ msg.arg2); break; - } - case ExoPlayerImplInternal.MSG_STATE_CHANGED: { - playbackState = msg.arg1; - for (EventListener listener : listeners) { - listener.onPlayerStateChanged(playWhenReady, playbackState); - } + case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: + handlePlaybackParameters((PlaybackParameters) msg.obj, /* operationAck= */ msg.arg1 != 0); break; - } - case ExoPlayerImplInternal.MSG_LOADING_CHANGED: { - isLoading = msg.arg1 != 0; - for (EventListener listener : listeners) { - listener.onLoadingChanged(isLoading); - } - break; - } - case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { - if (pendingPrepareAcks == 0) { - TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj; - tracksSelected = true; - trackGroups = trackSelectorResult.groups; - trackSelections = trackSelectorResult.selections; - trackSelector.onSelectionActivated(trackSelectorResult.info); - for (EventListener listener : listeners) { - listener.onTracksChanged(trackGroups, trackSelections); - } - } - break; - } - case ExoPlayerImplInternal.MSG_SEEK_ACK: { - if (--pendingSeekAcks == 0) { - playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj; - if (msg.arg1 != 0) { - for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(); - } - } - } - break; - } - case ExoPlayerImplInternal.MSG_POSITION_DISCONTINUITY: { - if (pendingSeekAcks == 0) { - playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj; - for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(); - } - } - break; - } - case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { - SourceInfo sourceInfo = (SourceInfo) msg.obj; - pendingSeekAcks -= sourceInfo.seekAcks; - if (pendingPrepareAcks == 0) { - timeline = sourceInfo.timeline; - manifest = sourceInfo.manifest; - playbackInfo = sourceInfo.playbackInfo; - for (EventListener listener : listeners) { - listener.onTimelineChanged(timeline, manifest); - } - } - break; - } - case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: { - PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj; - if (!this.playbackParameters.equals(playbackParameters)) { - this.playbackParameters = playbackParameters; - for (EventListener listener : listeners) { - listener.onPlaybackParametersChanged(playbackParameters); - } - } - break; - } - case ExoPlayerImplInternal.MSG_ERROR: { - ExoPlaybackException exception = (ExoPlaybackException) msg.obj; - for (EventListener listener : listeners) { - listener.onPlayerError(exception); - } - break; - } default: throw new IllegalStateException(); } } + private void handlePlaybackParameters( + PlaybackParameters playbackParameters, boolean operationAck) { + if (operationAck) { + pendingSetPlaybackParametersAcks--; + } + if (pendingSetPlaybackParametersAcks == 0) { + if (!this.playbackParameters.equals(playbackParameters)) { + this.playbackParameters = playbackParameters; + notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParameters)); + } + } + } + + private void handlePlaybackInfo( + PlaybackInfo playbackInfo, + int operationAcks, + boolean positionDiscontinuity, + @DiscontinuityReason int positionDiscontinuityReason) { + pendingOperationAcks -= operationAcks; + if (pendingOperationAcks == 0) { + if (playbackInfo.startPositionUs == C.TIME_UNSET) { + // Replace internal unset start position with externally visible start position of zero. + playbackInfo = + playbackInfo.copyWithNewPosition( + playbackInfo.periodId, + /* positionUs= */ 0, + playbackInfo.contentPositionUs, + playbackInfo.totalBufferedDurationUs); + } + if (!this.playbackInfo.timeline.isEmpty() && playbackInfo.timeline.isEmpty()) { + // Update the masking variables, which are used when the timeline becomes empty. + maskingPeriodIndex = 0; + maskingWindowIndex = 0; + maskingWindowPositionMs = 0; + } + @Player.TimelineChangeReason + int timelineChangeReason = + hasPendingPrepare + ? Player.TIMELINE_CHANGE_REASON_PREPARED + : Player.TIMELINE_CHANGE_REASON_DYNAMIC; + boolean seekProcessed = hasPendingSeek; + hasPendingPrepare = false; + hasPendingSeek = false; + updatePlaybackInfo( + playbackInfo, + positionDiscontinuity, + positionDiscontinuityReason, + timelineChangeReason, + seekProcessed); + } + } + + private PlaybackInfo getResetPlaybackInfo( + boolean resetPosition, + boolean resetState, + boolean resetError, + @Player.State int playbackState) { + if (resetPosition) { + maskingWindowIndex = 0; + maskingPeriodIndex = 0; + maskingWindowPositionMs = 0; + } else { + maskingWindowIndex = getCurrentWindowIndex(); + maskingPeriodIndex = getCurrentPeriodIndex(); + maskingWindowPositionMs = getCurrentPosition(); + } + // Also reset period-based PlaybackInfo positions if resetting the state. + resetPosition = resetPosition || resetState; + MediaPeriodId mediaPeriodId = + resetPosition + ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period) + : playbackInfo.periodId; + long startPositionUs = resetPosition ? 0 : playbackInfo.positionUs; + long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; + return new PlaybackInfo( + resetState ? Timeline.EMPTY : playbackInfo.timeline, + mediaPeriodId, + startPositionUs, + contentPositionUs, + playbackState, + resetError ? null : playbackInfo.playbackError, + /* isLoading= */ false, + resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, + resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, + mediaPeriodId, + startPositionUs, + /* totalBufferedDurationUs= */ 0, + startPositionUs); + } + + private void updatePlaybackInfo( + PlaybackInfo playbackInfo, + boolean positionDiscontinuity, + @Player.DiscontinuityReason int positionDiscontinuityReason, + @Player.TimelineChangeReason int timelineChangeReason, + boolean seekProcessed) { + boolean previousIsPlaying = isPlaying(); + // Assign playback info immediately such that all getters return the right values. + PlaybackInfo previousPlaybackInfo = this.playbackInfo; + this.playbackInfo = playbackInfo; + boolean isPlaying = isPlaying(); + notifyListeners( + new PlaybackInfoUpdate( + playbackInfo, + previousPlaybackInfo, + listeners, + trackSelector, + positionDiscontinuity, + positionDiscontinuityReason, + timelineChangeReason, + seekProcessed, + playWhenReady, + /* isPlayingChanged= */ previousIsPlaying != isPlaying)); + } + + private void notifyListeners(ListenerInvocation listenerInvocation) { + CopyOnWriteArrayList listenerSnapshot = new CopyOnWriteArrayList<>(listeners); + notifyListeners(() -> invokeAll(listenerSnapshot, listenerInvocation)); + } + + private void notifyListeners(Runnable listenerNotificationRunnable) { + boolean isRunningRecursiveListenerNotification = !pendingListenerNotifications.isEmpty(); + pendingListenerNotifications.addLast(listenerNotificationRunnable); + if (isRunningRecursiveListenerNotification) { + return; + } + while (!pendingListenerNotifications.isEmpty()) { + pendingListenerNotifications.peekFirst().run(); + pendingListenerNotifications.removeFirst(); + } + } + + private long periodPositionUsToWindowPositionMs(MediaPeriodId periodId, long positionUs) { + long positionMs = C.usToMs(positionUs); + playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period); + positionMs += period.getPositionInWindowMs(); + return positionMs; + } + + private boolean shouldMaskPosition() { + return playbackInfo.timeline.isEmpty() || pendingOperationAcks > 0; + } + + private static final class PlaybackInfoUpdate implements Runnable { + + private final PlaybackInfo playbackInfo; + private final CopyOnWriteArrayList listenerSnapshot; + private final TrackSelector trackSelector; + private final boolean positionDiscontinuity; + private final @Player.DiscontinuityReason int positionDiscontinuityReason; + private final @Player.TimelineChangeReason int timelineChangeReason; + private final boolean seekProcessed; + private final boolean playbackStateChanged; + private final boolean playbackErrorChanged; + private final boolean timelineChanged; + private final boolean isLoadingChanged; + private final boolean trackSelectorResultChanged; + private final boolean playWhenReady; + private final boolean isPlayingChanged; + + public PlaybackInfoUpdate( + PlaybackInfo playbackInfo, + PlaybackInfo previousPlaybackInfo, + CopyOnWriteArrayList listeners, + TrackSelector trackSelector, + boolean positionDiscontinuity, + @DiscontinuityReason int positionDiscontinuityReason, + @TimelineChangeReason int timelineChangeReason, + boolean seekProcessed, + boolean playWhenReady, + boolean isPlayingChanged) { + this.playbackInfo = playbackInfo; + this.listenerSnapshot = new CopyOnWriteArrayList<>(listeners); + this.trackSelector = trackSelector; + this.positionDiscontinuity = positionDiscontinuity; + this.positionDiscontinuityReason = positionDiscontinuityReason; + this.timelineChangeReason = timelineChangeReason; + this.seekProcessed = seekProcessed; + this.playWhenReady = playWhenReady; + this.isPlayingChanged = isPlayingChanged; + playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState; + playbackErrorChanged = + previousPlaybackInfo.playbackError != playbackInfo.playbackError + && playbackInfo.playbackError != null; + timelineChanged = previousPlaybackInfo.timeline != playbackInfo.timeline; + isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading; + trackSelectorResultChanged = + previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult; + } + + @Override + public void run() { + if (timelineChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { + invokeAll( + listenerSnapshot, + listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason)); + } + if (positionDiscontinuity) { + invokeAll( + listenerSnapshot, + listener -> listener.onPositionDiscontinuity(positionDiscontinuityReason)); + } + if (playbackErrorChanged) { + invokeAll(listenerSnapshot, listener -> listener.onPlayerError(playbackInfo.playbackError)); + } + if (trackSelectorResultChanged) { + trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info); + invokeAll( + listenerSnapshot, + listener -> + listener.onTracksChanged( + playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections)); + } + if (isLoadingChanged) { + invokeAll(listenerSnapshot, listener -> listener.onLoadingChanged(playbackInfo.isLoading)); + } + if (playbackStateChanged) { + invokeAll( + listenerSnapshot, + listener -> listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState)); + } + if (isPlayingChanged) { + invokeAll( + listenerSnapshot, + listener -> + listener.onIsPlayingChanged(playbackInfo.playbackState == Player.STATE_READY)); + } + if (seekProcessed) { + invokeAll(listenerSnapshot, EventListener::onSeekProcessed); + } + } + } + + private static void invokeAll( + CopyOnWriteArrayList listeners, ListenerInvocation listenerInvocation) { + for (ListenerHolder listenerHolder : listeners) { + listenerHolder.invoke(listenerInvocation); + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 628d5f5107d8..a4462ad1c439 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -17,87 +17,49 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2; import android.os.Handler; import android.os.HandlerThread; +import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.SystemClock; -import android.util.Log; import android.util.Pair; -import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.DiscontinuityReason; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaPeriod; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleStream; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; -import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelector; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectorResult; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MediaClock; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.StandaloneMediaClock; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.HandlerWrapper; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TraceUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; -/** - * Implements the internal behavior of {@link ExoPlayerImpl}. - */ -/* package */ final class ExoPlayerImplInternal implements Handler.Callback, - MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener { - - /** - * Playback position information which is read on the application's thread by - * {@link ExoPlayerImpl} and read/written internally on the player's thread. - */ - public static final class PlaybackInfo { - - public final int periodIndex; - public final long startPositionUs; - - public volatile long positionUs; - public volatile long bufferedPositionUs; - - public PlaybackInfo(int periodIndex, long startPositionUs) { - this.periodIndex = periodIndex; - this.startPositionUs = startPositionUs; - positionUs = startPositionUs; - bufferedPositionUs = startPositionUs; - } - - public PlaybackInfo copyWithPeriodIndex(int periodIndex) { - PlaybackInfo playbackInfo = new PlaybackInfo(periodIndex, startPositionUs); - playbackInfo.positionUs = positionUs; - playbackInfo.bufferedPositionUs = bufferedPositionUs; - return playbackInfo; - } - - } - - public static final class SourceInfo { - - public final Timeline timeline; - public final Object manifest; - public final PlaybackInfo playbackInfo; - public final int seekAcks; - - public SourceInfo(Timeline timeline, Object manifest, PlaybackInfo playbackInfo, int seekAcks) { - this.timeline = timeline; - this.manifest = manifest; - this.playbackInfo = playbackInfo; - this.seekAcks = seekAcks; - } - - } +/** Implements the internal behavior of {@link ExoPlayerImpl}. */ +/* package */ final class ExoPlayerImplInternal + implements Handler.Callback, + MediaPeriod.Callback, + TrackSelector.InvalidationListener, + MediaSourceCaller, + PlaybackParameterListener, + PlayerMessage.Sender { private static final String TAG = "ExoPlayerImplInternal"; // External messages - public static final int MSG_PREPARE_ACK = 0; - public static final int MSG_STATE_CHANGED = 1; - public static final int MSG_LOADING_CHANGED = 2; - public static final int MSG_TRACKS_CHANGED = 3; - public static final int MSG_SEEK_ACK = 4; - public static final int MSG_POSITION_DISCONTINUITY = 5; - public static final int MSG_SOURCE_INFO_REFRESHED = 6; - public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 7; - public static final int MSG_ERROR = 8; + public static final int MSG_PLAYBACK_INFO_CHANGED = 0; + public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 1; // Internal messages private static final int MSG_PREPARE = 0; @@ -105,104 +67,116 @@ import java.io.IOException; private static final int MSG_DO_SOME_WORK = 2; private static final int MSG_SEEK_TO = 3; private static final int MSG_SET_PLAYBACK_PARAMETERS = 4; - private static final int MSG_STOP = 5; - private static final int MSG_RELEASE = 6; - private static final int MSG_REFRESH_SOURCE_INFO = 7; - private static final int MSG_PERIOD_PREPARED = 8; - private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9; - private static final int MSG_TRACK_SELECTION_INVALIDATED = 10; - private static final int MSG_CUSTOM = 11; + private static final int MSG_SET_SEEK_PARAMETERS = 5; + private static final int MSG_STOP = 6; + private static final int MSG_RELEASE = 7; + private static final int MSG_REFRESH_SOURCE_INFO = 8; + private static final int MSG_PERIOD_PREPARED = 9; + private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 10; + private static final int MSG_TRACK_SELECTION_INVALIDATED = 11; + private static final int MSG_SET_REPEAT_MODE = 12; + private static final int MSG_SET_SHUFFLE_ENABLED = 13; + private static final int MSG_SET_FOREGROUND_MODE = 14; + private static final int MSG_SEND_MESSAGE = 15; + private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 16; + private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 17; - private static final int PREPARING_SOURCE_INTERVAL_MS = 10; - private static final int RENDERING_INTERVAL_MS = 10; + private static final int ACTIVE_INTERVAL_MS = 10; private static final int IDLE_INTERVAL_MS = 1000; - /** - * Limits the maximum number of periods to buffer ahead of the current playing period. The - * buffering policy normally prevents buffering too far ahead, but the policy could allow too many - * small periods to be buffered if the period count were not limited. - */ - private static final int MAXIMUM_BUFFER_AHEAD_PERIODS = 100; - - /** - * Offset added to all sample timestamps read by renderers to make them non-negative. This is - * provided for convenience of sources that may return negative timestamps due to prerolling - * samples from a keyframe before their first sample with timestamp zero, so it must be set to a - * value greater than or equal to the maximum key-frame interval in seekable periods. - */ - private static final int RENDERER_TIMESTAMP_OFFSET_US = 60000000; - private final Renderer[] renderers; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; + private final TrackSelectorResult emptyTrackSelectorResult; private final LoadControl loadControl; - private final StandaloneMediaClock standaloneMediaClock; - private final Handler handler; + private final BandwidthMeter bandwidthMeter; + private final HandlerWrapper handler; private final HandlerThread internalPlaybackThread; private final Handler eventHandler; - private final ExoPlayer player; private final Timeline.Window window; private final Timeline.Period period; + private final long backBufferDurationUs; + private final boolean retainBackBufferFromKeyframe; + private final DefaultMediaClock mediaClock; + private final PlaybackInfoUpdate playbackInfoUpdate; + private final ArrayList pendingMessages; + private final Clock clock; + private final MediaPeriodQueue queue; + + @SuppressWarnings("unused") + private SeekParameters seekParameters; private PlaybackInfo playbackInfo; - private PlaybackParameters playbackParameters; - private Renderer rendererMediaClockSource; - private MediaClock rendererMediaClock; private MediaSource mediaSource; private Renderer[] enabledRenderers; private boolean released; private boolean playWhenReady; private boolean rebuffering; - private boolean isLoading; - private int state; - private int customMessagesSent; - private int customMessagesProcessed; - private long elapsedRealtimeUs; + private boolean shouldContinueLoading; + @Player.RepeatMode private int repeatMode; + private boolean shuffleModeEnabled; + private boolean foregroundMode; - private int pendingInitialSeekCount; - private SeekPosition pendingSeekPosition; + private int pendingPrepareCount; + private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; + private int nextPendingMessageIndex; + private boolean deliverPendingMessageAtStartPositionRequired; - private MediaPeriodHolder loadingPeriodHolder; - private MediaPeriodHolder readingPeriodHolder; - private MediaPeriodHolder playingPeriodHolder; - - private Timeline timeline; - - public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl, boolean playWhenReady, Handler eventHandler, - PlaybackInfo playbackInfo, ExoPlayer player) { + public ExoPlayerImplInternal( + Renderer[] renderers, + TrackSelector trackSelector, + TrackSelectorResult emptyTrackSelectorResult, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + boolean playWhenReady, + @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled, + Handler eventHandler, + Clock clock) { this.renderers = renderers; this.trackSelector = trackSelector; + this.emptyTrackSelectorResult = emptyTrackSelectorResult; this.loadControl = loadControl; + this.bandwidthMeter = bandwidthMeter; this.playWhenReady = playWhenReady; + this.repeatMode = repeatMode; + this.shuffleModeEnabled = shuffleModeEnabled; this.eventHandler = eventHandler; - this.state = ExoPlayer.STATE_IDLE; - this.playbackInfo = playbackInfo; - this.player = player; + this.clock = clock; + this.queue = new MediaPeriodQueue(); + backBufferDurationUs = loadControl.getBackBufferDurationUs(); + retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); + + seekParameters = SeekParameters.DEFAULT; + playbackInfo = + PlaybackInfo.createDummy(/* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult); + playbackInfoUpdate = new PlaybackInfoUpdate(); rendererCapabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { renderers[i].setIndex(i); rendererCapabilities[i] = renderers[i].getCapabilities(); } - standaloneMediaClock = new StandaloneMediaClock(); + mediaClock = new DefaultMediaClock(this, clock); + pendingMessages = new ArrayList<>(); enabledRenderers = new Renderer[0]; window = new Timeline.Window(); period = new Timeline.Period(); - trackSelector.init(this); - playbackParameters = PlaybackParameters.DEFAULT; + trackSelector.init(/* listener= */ this, bandwidthMeter); // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can // not normally change to this priority" is incorrect. - internalPlaybackThread = new HandlerThread("ExoPlayerImplInternal:Handler", - Process.THREAD_PRIORITY_AUDIO); + internalPlaybackThread = + new HandlerThread("ExoPlayerImplInternal:Handler", Process.THREAD_PRIORITY_AUDIO); internalPlaybackThread.start(); - handler = new Handler(internalPlaybackThread.getLooper(), this); + handler = clock.createHandler(internalPlaybackThread.getLooper(), this); + deliverPendingMessageAtStartPositionRequired = true; } - public void prepare(MediaSource mediaSource, boolean resetPosition) { - handler.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, 0, mediaSource) + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + handler + .obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource) .sendToTarget(); } @@ -210,8 +184,17 @@ import java.io.IOException; handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget(); } + public void setRepeatMode(@Player.RepeatMode int repeatMode) { + handler.obtainMessage(MSG_SET_REPEAT_MODE, repeatMode, 0).sendToTarget(); + } + + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + handler.obtainMessage(MSG_SET_SHUFFLE_ENABLED, shuffleModeEnabled ? 1 : 0, 0).sendToTarget(); + } + public void seekTo(Timeline timeline, int windowIndex, long positionUs) { - handler.obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs)) + handler + .obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs)) .sendToTarget(); } @@ -219,55 +202,80 @@ import java.io.IOException; handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget(); } - public void stop() { - handler.sendEmptyMessage(MSG_STOP); + public void setSeekParameters(SeekParameters seekParameters) { + handler.obtainMessage(MSG_SET_SEEK_PARAMETERS, seekParameters).sendToTarget(); } - public void sendMessages(ExoPlayerMessage... messages) { - if (released) { - Log.w(TAG, "Ignoring messages sent after release."); - return; - } - customMessagesSent++; - handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); + public void stop(boolean reset) { + handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } - public synchronized void blockingSendMessages(ExoPlayerMessage... messages) { - if (released) { + @Override + public synchronized void sendMessage(PlayerMessage message) { + if (released || !internalPlaybackThread.isAlive()) { Log.w(TAG, "Ignoring messages sent after release."); + message.markAsProcessed(/* isDelivered= */ false); return; } - int messageNumber = customMessagesSent++; - handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); - while (customMessagesProcessed <= messageNumber) { - try { - wait(); - } catch (InterruptedException e) { + handler.obtainMessage(MSG_SEND_MESSAGE, message).sendToTarget(); + } + + public synchronized void setForegroundMode(boolean foregroundMode) { + if (released || !internalPlaybackThread.isAlive()) { + return; + } + if (foregroundMode) { + handler.obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 1, 0).sendToTarget(); + } else { + AtomicBoolean processedFlag = new AtomicBoolean(); + handler + .obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 0, 0, processedFlag) + .sendToTarget(); + boolean wasInterrupted = false; + while (!processedFlag.get()) { + try { + wait(); + } catch (InterruptedException e) { + wasInterrupted = true; + } + } + if (wasInterrupted) { + // Restore the interrupted status. Thread.currentThread().interrupt(); } } } public synchronized void release() { - if (released) { + if (released || !internalPlaybackThread.isAlive()) { return; } handler.sendEmptyMessage(MSG_RELEASE); + boolean wasInterrupted = false; while (!released) { try { wait(); } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + wasInterrupted = true; } } - internalPlaybackThread.quit(); + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } } - // MediaSource.Listener implementation. + public Looper getPlaybackLooper() { + return internalPlaybackThread.getLooper(); + } + + // MediaSource.MediaSourceCaller implementation. @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - handler.obtainMessage(MSG_REFRESH_SOURCE_INFO, Pair.create(timeline, manifest)).sendToTarget(); + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { + handler + .obtainMessage(MSG_REFRESH_SOURCE_INFO, new MediaSourceRefreshInfo(source, timeline)) + .sendToTarget(); } // MediaPeriod.Callback implementation. @@ -289,109 +297,167 @@ import java.io.IOException; handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED); } + // DefaultMediaClock.PlaybackParameterListener implementation. + + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + sendPlaybackParametersChangedInternal(playbackParameters, /* acknowledgeCommand= */ false); + } + // Handler.Callback implementation. - @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { try { switch (msg.what) { - case MSG_PREPARE: { - prepareInternal((MediaSource) msg.obj, msg.arg1 != 0); - return true; - } - case MSG_SET_PLAY_WHEN_READY: { + case MSG_PREPARE: + prepareInternal( + (MediaSource) msg.obj, + /* resetPosition= */ msg.arg1 != 0, + /* resetState= */ msg.arg2 != 0); + break; + case MSG_SET_PLAY_WHEN_READY: setPlayWhenReadyInternal(msg.arg1 != 0); - return true; - } - case MSG_DO_SOME_WORK: { + break; + case MSG_SET_REPEAT_MODE: + setRepeatModeInternal(msg.arg1); + break; + case MSG_SET_SHUFFLE_ENABLED: + setShuffleModeEnabledInternal(msg.arg1 != 0); + break; + case MSG_DO_SOME_WORK: doSomeWork(); - return true; - } - case MSG_SEEK_TO: { + break; + case MSG_SEEK_TO: seekToInternal((SeekPosition) msg.obj); - return true; - } - case MSG_SET_PLAYBACK_PARAMETERS: { + break; + case MSG_SET_PLAYBACK_PARAMETERS: setPlaybackParametersInternal((PlaybackParameters) msg.obj); - return true; - } - case MSG_STOP: { - stopInternal(); - return true; - } - case MSG_RELEASE: { - releaseInternal(); - return true; - } - case MSG_PERIOD_PREPARED: { + break; + case MSG_SET_SEEK_PARAMETERS: + setSeekParametersInternal((SeekParameters) msg.obj); + break; + case MSG_SET_FOREGROUND_MODE: + setForegroundModeInternal( + /* foregroundMode= */ msg.arg1 != 0, /* processedFlag= */ (AtomicBoolean) msg.obj); + break; + case MSG_STOP: + stopInternal( + /* forceResetRenderers= */ false, + /* resetPositionAndState= */ msg.arg1 != 0, + /* acknowledgeStop= */ true); + break; + case MSG_PERIOD_PREPARED: handlePeriodPrepared((MediaPeriod) msg.obj); - return true; - } - case MSG_REFRESH_SOURCE_INFO: { - handleSourceInfoRefreshed((Pair) msg.obj); - return true; - } - case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: { + break; + case MSG_REFRESH_SOURCE_INFO: + handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj); + break; + case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: handleContinueLoadingRequested((MediaPeriod) msg.obj); - return true; - } - case MSG_TRACK_SELECTION_INVALIDATED: { + break; + case MSG_TRACK_SELECTION_INVALIDATED: reselectTracksInternal(); + break; + case MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL: + handlePlaybackParameters( + (PlaybackParameters) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0); + break; + case MSG_SEND_MESSAGE: + sendMessageInternal((PlayerMessage) msg.obj); + break; + case MSG_SEND_MESSAGE_TO_TARGET_THREAD: + sendMessageToTargetThread((PlayerMessage) msg.obj); + break; + case MSG_RELEASE: + releaseInternal(); + // Return immediately to not send playback info updates after release. return true; - } - case MSG_CUSTOM: { - sendMessagesInternal((ExoPlayerMessage[]) msg.obj); - return true; - } default: return false; } + maybeNotifyPlaybackInfoChanged(); } catch (ExoPlaybackException e) { - Log.e(TAG, "Renderer error.", e); - eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); - stopInternal(); - return true; + Log.e(TAG, getExoPlaybackExceptionMessage(e), e); + stopInternal( + /* forceResetRenderers= */ true, + /* resetPositionAndState= */ false, + /* acknowledgeStop= */ false); + playbackInfo = playbackInfo.copyWithPlaybackError(e); + maybeNotifyPlaybackInfoChanged(); } catch (IOException e) { - Log.e(TAG, "Source error.", e); - eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget(); - stopInternal(); - return true; - } catch (RuntimeException e) { - Log.e(TAG, "Internal runtime error.", e); - eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e)) - .sendToTarget(); - stopInternal(); - return true; + Log.e(TAG, "Source error", e); + stopInternal( + /* forceResetRenderers= */ false, + /* resetPositionAndState= */ false, + /* acknowledgeStop= */ false); + playbackInfo = playbackInfo.copyWithPlaybackError(ExoPlaybackException.createForSource(e)); + maybeNotifyPlaybackInfoChanged(); + } catch (RuntimeException | OutOfMemoryError e) { + Log.e(TAG, "Internal runtime error", e); + ExoPlaybackException error = + e instanceof OutOfMemoryError + ? ExoPlaybackException.createForOutOfMemoryError((OutOfMemoryError) e) + : ExoPlaybackException.createForUnexpected((RuntimeException) e); + stopInternal( + /* forceResetRenderers= */ true, + /* resetPositionAndState= */ false, + /* acknowledgeStop= */ false); + playbackInfo = playbackInfo.copyWithPlaybackError(error); + maybeNotifyPlaybackInfoChanged(); } + return true; } // Private methods. + private String getExoPlaybackExceptionMessage(ExoPlaybackException e) { + if (e.type != ExoPlaybackException.TYPE_RENDERER) { + return "Playback error."; + } + return "Renderer error: index=" + + e.rendererIndex + + ", type=" + + Util.getTrackTypeString(renderers[e.rendererIndex].getTrackType()) + + ", format=" + + e.rendererFormat + + ", rendererSupport=" + + RendererCapabilities.getFormatSupportString(e.rendererFormatSupport); + } + private void setState(int state) { - if (this.state != state) { - this.state = state; - eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget(); + if (playbackInfo.playbackState != state) { + playbackInfo = playbackInfo.copyWithPlaybackState(state); } } - private void setIsLoading(boolean isLoading) { - if (this.isLoading != isLoading) { - this.isLoading = isLoading; - eventHandler.obtainMessage(MSG_LOADING_CHANGED, isLoading ? 1 : 0, 0).sendToTarget(); + private void maybeNotifyPlaybackInfoChanged() { + if (playbackInfoUpdate.hasPendingUpdate(playbackInfo)) { + eventHandler + .obtainMessage( + MSG_PLAYBACK_INFO_CHANGED, + playbackInfoUpdate.operationAcks, + playbackInfoUpdate.positionDiscontinuity + ? playbackInfoUpdate.discontinuityReason + : C.INDEX_UNSET, + playbackInfo) + .sendToTarget(); + playbackInfoUpdate.reset(playbackInfo); } } - private void prepareInternal(MediaSource mediaSource, boolean resetPosition) { - eventHandler.sendEmptyMessage(MSG_PREPARE_ACK); - resetInternal(true); + private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + pendingPrepareCount++; + resetInternal( + /* resetRenderers= */ false, + /* releaseMediaSource= */ true, + resetPosition, + resetState, + /* resetError= */ true); loadControl.onPrepared(); - if (resetPosition) { - playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); - } this.mediaSource = mediaSource; - mediaSource.prepareSource(player, true, this); - setState(ExoPlayer.STATE_BUFFERING); + setState(Player.STATE_BUFFERING); + mediaSource.prepareSource(/* caller= */ this, bandwidthMeter.getTransferListener()); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -402,145 +468,188 @@ import java.io.IOException; stopRenderers(); updatePlaybackPositions(); } else { - if (state == ExoPlayer.STATE_READY) { + if (playbackInfo.playbackState == Player.STATE_READY) { startRenderers(); handler.sendEmptyMessage(MSG_DO_SOME_WORK); - } else if (state == ExoPlayer.STATE_BUFFERING) { + } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) { handler.sendEmptyMessage(MSG_DO_SOME_WORK); } } } + private void setRepeatModeInternal(@Player.RepeatMode int repeatMode) + throws ExoPlaybackException { + this.repeatMode = repeatMode; + if (!queue.updateRepeatMode(repeatMode)) { + seekToCurrentPosition(/* sendDiscontinuity= */ true); + } + handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); + } + + private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled) + throws ExoPlaybackException { + this.shuffleModeEnabled = shuffleModeEnabled; + if (!queue.updateShuffleModeEnabled(shuffleModeEnabled)) { + seekToCurrentPosition(/* sendDiscontinuity= */ true); + } + handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); + } + + private void seekToCurrentPosition(boolean sendDiscontinuity) throws ExoPlaybackException { + // Renderers may have read from a period that's been removed. Seek back to the current + // position of the playing period to make sure none of the removed period is played. + MediaPeriodId periodId = queue.getPlayingPeriod().info.id; + long newPositionUs = + seekToPeriodPosition(periodId, playbackInfo.positionUs, /* forceDisableRenderers= */ true); + if (newPositionUs != playbackInfo.positionUs) { + playbackInfo = copyWithNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs); + if (sendDiscontinuity) { + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); + } + } + } + private void startRenderers() throws ExoPlaybackException { rebuffering = false; - standaloneMediaClock.start(); + mediaClock.start(); for (Renderer renderer : enabledRenderers) { renderer.start(); } } private void stopRenderers() throws ExoPlaybackException { - standaloneMediaClock.stop(); + mediaClock.stop(); for (Renderer renderer : enabledRenderers) { ensureStopped(renderer); } } private void updatePlaybackPositions() throws ExoPlaybackException { + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); if (playingPeriodHolder == null) { return; } // Update the playback position. - long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity(); - if (periodPositionUs != C.TIME_UNSET) { - resetRendererPosition(periodPositionUs); - } else { - if (rendererMediaClockSource != null && !rendererMediaClockSource.isEnded()) { - rendererPositionUs = rendererMediaClock.getPositionUs(); - standaloneMediaClock.setPositionUs(rendererPositionUs); - } else { - rendererPositionUs = standaloneMediaClock.getPositionUs(); + long discontinuityPositionUs = + playingPeriodHolder.prepared + ? playingPeriodHolder.mediaPeriod.readDiscontinuity() + : C.TIME_UNSET; + if (discontinuityPositionUs != C.TIME_UNSET) { + resetRendererPosition(discontinuityPositionUs); + // A MediaPeriod may report a discontinuity at the current playback position to ensure the + // renderers are flushed. Only report the discontinuity externally if the position changed. + if (discontinuityPositionUs != playbackInfo.positionUs) { + playbackInfo = + copyWithNewPosition( + playbackInfo.periodId, discontinuityPositionUs, playbackInfo.contentPositionUs); + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } - periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); + } else { + rendererPositionUs = + mediaClock.syncAndGetPositionUs( + /* isReadingAhead= */ playingPeriodHolder != queue.getReadingPeriod()); + long periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); + maybeTriggerPendingMessages(playbackInfo.positionUs, periodPositionUs); + playbackInfo.positionUs = periodPositionUs; } - playbackInfo.positionUs = periodPositionUs; - elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; - // Update the buffered position. - long bufferedPositionUs = enabledRenderers.length == 0 ? C.TIME_END_OF_SOURCE - : playingPeriodHolder.mediaPeriod.getBufferedPositionUs(); - playbackInfo.bufferedPositionUs = bufferedPositionUs == C.TIME_END_OF_SOURCE - ? timeline.getPeriod(playingPeriodHolder.index, period).getDurationUs() - : bufferedPositionUs; + // Update the buffered position and total buffered duration. + MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod(); + playbackInfo.bufferedPositionUs = loadingPeriod.getBufferedPositionUs(); + playbackInfo.totalBufferedDurationUs = getTotalBufferedDurationUs(); } private void doSomeWork() throws ExoPlaybackException, IOException { - long operationStartTimeMs = SystemClock.elapsedRealtime(); + long operationStartTimeMs = clock.uptimeMillis(); 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); + return; + } + + @Nullable MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); if (playingPeriodHolder == null) { - // We're still waiting for the first period to be prepared. - maybeThrowPeriodPrepareError(); - scheduleNextWork(operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS); + // We're still waiting until the playing period is available. + scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS); return; } TraceUtil.beginSection("doSomeWork"); updatePlaybackPositions(); - playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs); - boolean allRenderersEnded = true; - boolean allRenderersReadyOrEnded = true; - for (Renderer renderer : enabledRenderers) { - // TODO: Each renderer should return the maximum delay before which it wishes to be called - // again. The minimum of these values should then be used as the delay before the next - // invocation of this method. - renderer.render(rendererPositionUs, elapsedRealtimeUs); - allRenderersEnded = allRenderersEnded && renderer.isEnded(); - // Determine whether the renderer is ready (or ended). If it's not, throw an error that's - // preventing the renderer from making progress, if such an error exists. - boolean rendererReadyOrEnded = renderer.isReady() || renderer.isEnded(); - if (!rendererReadyOrEnded) { - renderer.maybeThrowStreamError(); - } - allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded; - } - - if (!allRenderersReadyOrEnded) { - maybeThrowPeriodPrepareError(); - } - - // The standalone media clock never changes playback parameters, so just check the renderer. - if (rendererMediaClock != null) { - PlaybackParameters playbackParameters = rendererMediaClock.getPlaybackParameters(); - if (!playbackParameters.equals(this.playbackParameters)) { - // TODO: Make LoadControl, period transition position projection, adaptive track selection - // and potentially any time-related code in renderers take into account the playback speed. - this.playbackParameters = playbackParameters; - standaloneMediaClock.synchronize(rendererMediaClock); - eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters) - .sendToTarget(); - } - } - - long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.index, period) - .getDurationUs(); - if (allRenderersEnded - && (playingPeriodDurationUs == C.TIME_UNSET - || playingPeriodDurationUs <= playbackInfo.positionUs) - && playingPeriodHolder.isLast) { - setState(ExoPlayer.STATE_ENDED); - stopRenderers(); - } else if (state == ExoPlayer.STATE_BUFFERING) { - boolean isNewlyReady = enabledRenderers.length > 0 - ? (allRenderersReadyOrEnded && haveSufficientBuffer(rebuffering)) - : isTimelineReady(playingPeriodDurationUs); - if (isNewlyReady) { - setState(ExoPlayer.STATE_READY); - if (playWhenReady) { - startRenderers(); + boolean renderersEnded = true; + boolean renderersAllowPlayback = true; + if (playingPeriodHolder.prepared) { + long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; + playingPeriodHolder.mediaPeriod.discardBuffer( + playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe); + for (int i = 0; i < renderers.length; i++) { + Renderer renderer = renderers[i]; + if (renderer.getState() == Renderer.STATE_DISABLED) { + continue; + } + // TODO: Each renderer should return the maximum delay before which it wishes to be called + // again. The minimum of these values should then be used as the delay before the next + // invocation of this method. + renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs); + renderersEnded = renderersEnded && renderer.isEnded(); + // Determine whether the renderer allows playback to continue. Playback can continue if the + // renderer is ready or ended. Also continue playback if the renderer is reading ahead into + // the next stream or is waiting for the next stream. This is to avoid getting stuck if + // tracks in the current period have uneven durations and are still being read by another + // renderer. See: https://github.com/google/ExoPlayer/issues/1874. + boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream(); + boolean isWaitingForNextStream = + !isReadingAhead + && playingPeriodHolder.getNext() != null + && renderer.hasReadStreamToEnd(); + boolean allowsPlayback = + isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded(); + renderersAllowPlayback = renderersAllowPlayback && allowsPlayback; + if (!allowsPlayback) { + renderer.maybeThrowStreamError(); } } - } else if (state == ExoPlayer.STATE_READY) { - boolean isStillReady = enabledRenderers.length > 0 ? allRenderersReadyOrEnded - : isTimelineReady(playingPeriodDurationUs); - if (!isStillReady) { - rebuffering = playWhenReady; - setState(ExoPlayer.STATE_BUFFERING); - stopRenderers(); - } + } else { + playingPeriodHolder.mediaPeriod.maybeThrowPrepareError(); } - if (state == ExoPlayer.STATE_BUFFERING) { + long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; + if (renderersEnded + && playingPeriodHolder.prepared + && (playingPeriodDurationUs == C.TIME_UNSET + || playingPeriodDurationUs <= playbackInfo.positionUs) + && playingPeriodHolder.info.isFinal) { + setState(Player.STATE_ENDED); + stopRenderers(); + } else if (playbackInfo.playbackState == Player.STATE_BUFFERING + && shouldTransitionToReadyState(renderersAllowPlayback)) { + setState(Player.STATE_READY); + if (playWhenReady) { + startRenderers(); + } + } else if (playbackInfo.playbackState == Player.STATE_READY + && !(enabledRenderers.length == 0 ? isTimelineReady() : renderersAllowPlayback)) { + rebuffering = playWhenReady; + setState(Player.STATE_BUFFERING); + stopRenderers(); + } + + if (playbackInfo.playbackState == Player.STATE_BUFFERING) { for (Renderer renderer : enabledRenderers) { renderer.maybeThrowStreamError(); } } - if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) { - scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS); - } else if (enabledRenderers.length != 0) { + if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY) + || playbackInfo.playbackState == Player.STATE_BUFFERING) { + scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS); + } else if (enabledRenderers.length != 0 && playbackInfo.playbackState != Player.STATE_ENDED) { scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS); } else { handler.removeMessages(MSG_DO_SOME_WORK); @@ -551,198 +660,468 @@ import java.io.IOException; private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) { handler.removeMessages(MSG_DO_SOME_WORK); - long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs; - long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime(); - if (nextOperationDelayMs <= 0) { - handler.sendEmptyMessage(MSG_DO_SOME_WORK); - } else { - handler.sendEmptyMessageDelayed(MSG_DO_SOME_WORK, nextOperationDelayMs); - } + handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs); } private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { - if (timeline == null) { - pendingInitialSeekCount++; - pendingSeekPosition = seekPosition; - return; - } + playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); - Pair periodPosition = resolveSeekPosition(seekPosition); - if (periodPosition == null) { + MediaPeriodId periodId; + long periodPositionUs; + long contentPositionUs; + boolean seekPositionAdjusted; + Pair resolvedSeekPosition = + resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true); + if (resolvedSeekPosition == null) { // The seek position was valid for the timeline that it was performed into, but the - // timeline has changed and a suitable seek position could not be resolved in the new one. - playbackInfo = new PlaybackInfo(0, 0); - eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, playbackInfo).sendToTarget(); - // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't - // ignored. - playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); - setState(ExoPlayer.STATE_ENDED); - // Reset, but retain the source so that it can still be used should a seek occur. - resetInternal(false); - return; + // timeline has changed or is not ready and a suitable seek position could not be resolved. + periodId = playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period); + periodPositionUs = C.TIME_UNSET; + contentPositionUs = C.TIME_UNSET; + seekPositionAdjusted = true; + } else { + // Update the resolved seek position to take ads into account. + Object periodUid = resolvedSeekPosition.first; + contentPositionUs = resolvedSeekPosition.second; + periodId = queue.resolveMediaPeriodIdForAds(periodUid, contentPositionUs); + if (periodId.isAd()) { + periodPositionUs = 0; + seekPositionAdjusted = true; + } else { + periodPositionUs = resolvedSeekPosition.second; + seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; + } } - boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; - int periodIndex = periodPosition.first; - long periodPositionUs = periodPosition.second; - try { - if (periodIndex == playbackInfo.periodIndex - && ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000))) { - // Seek position equals the current position. Do nothing. - return; + if (mediaSource == null || pendingPrepareCount > 0) { + // Save seek position for later, as we are still waiting for a prepared source. + pendingInitialSeekPosition = seekPosition; + } else if (periodPositionUs == C.TIME_UNSET) { + // End playback, as we didn't manage to find a valid seek position. + setState(Player.STATE_ENDED); + resetInternal( + /* resetRenderers= */ false, + /* releaseMediaSource= */ false, + /* resetPosition= */ true, + /* resetState= */ false, + /* resetError= */ true); + } else { + // Execute the seek in the current media periods. + long newPeriodPositionUs = periodPositionUs; + if (periodId.equals(playbackInfo.periodId)) { + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + if (playingPeriodHolder != null + && playingPeriodHolder.prepared + && newPeriodPositionUs != 0) { + newPeriodPositionUs = + playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs( + newPeriodPositionUs, seekParameters); + } + if (C.usToMs(newPeriodPositionUs) == C.usToMs(playbackInfo.positionUs)) { + // Seek will be performed to the current position. Do nothing. + periodPositionUs = playbackInfo.positionUs; + return; + } + } + newPeriodPositionUs = seekToPeriodPosition(periodId, newPeriodPositionUs); + seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; + periodPositionUs = newPeriodPositionUs; } - long newPeriodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs); - seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; - periodPositionUs = newPeriodPositionUs; } finally { - playbackInfo = new PlaybackInfo(periodIndex, periodPositionUs); - eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo) - .sendToTarget(); + playbackInfo = copyWithNewPosition(periodId, periodPositionUs, contentPositionUs); + if (seekPositionAdjusted) { + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT); + } } } - private long seekToPeriodPosition(int periodIndex, long periodPositionUs) + private long seekToPeriodPosition(MediaPeriodId periodId, long periodPositionUs) + throws ExoPlaybackException { + // Force disable renderers if they are reading from a period other than the one being played. + return seekToPeriodPosition( + periodId, periodPositionUs, queue.getPlayingPeriod() != queue.getReadingPeriod()); + } + + private long seekToPeriodPosition( + MediaPeriodId periodId, long periodPositionUs, boolean forceDisableRenderers) throws ExoPlaybackException { stopRenderers(); rebuffering = false; - setState(ExoPlayer.STATE_BUFFERING); - - MediaPeriodHolder newPlayingPeriodHolder = null; - if (playingPeriodHolder == null) { - // We're still waiting for the first period to be prepared. - if (loadingPeriodHolder != null) { - loadingPeriodHolder.release(); - } - } else { - // Clear the timeline, but keep the requested period if it is already prepared. - MediaPeriodHolder periodHolder = playingPeriodHolder; - while (periodHolder != null) { - if (periodHolder.index == periodIndex && periodHolder.prepared) { - newPlayingPeriodHolder = periodHolder; - } else { - periodHolder.release(); - } - periodHolder = periodHolder.next; - } + if (playbackInfo.playbackState != Player.STATE_IDLE && !playbackInfo.timeline.isEmpty()) { + setState(Player.STATE_BUFFERING); } - // Disable all the renderers if the period being played is changing, or if the renderers are - // reading from a period other than the one being played. - if (playingPeriodHolder != newPlayingPeriodHolder - || playingPeriodHolder != readingPeriodHolder) { + // Clear the timeline, but keep the requested period if it is already prepared. + MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); + MediaPeriodHolder newPlayingPeriodHolder = oldPlayingPeriodHolder; + while (newPlayingPeriodHolder != null) { + if (periodId.equals(newPlayingPeriodHolder.info.id) && newPlayingPeriodHolder.prepared) { + queue.removeAfter(newPlayingPeriodHolder); + break; + } + newPlayingPeriodHolder = queue.advancePlayingPeriod(); + } + + // Disable all renderers if the period being played is changing, if the seek results in negative + // renderer timestamps, or if forced. + if (forceDisableRenderers + || oldPlayingPeriodHolder != newPlayingPeriodHolder + || (newPlayingPeriodHolder != null + && newPlayingPeriodHolder.toRendererTime(periodPositionUs) < 0)) { for (Renderer renderer : enabledRenderers) { - renderer.disable(); + disableRenderer(renderer); } enabledRenderers = new Renderer[0]; - rendererMediaClock = null; - rendererMediaClockSource = null; - playingPeriodHolder = null; + oldPlayingPeriodHolder = null; + if (newPlayingPeriodHolder != null) { + newPlayingPeriodHolder.setRendererOffset(/* rendererPositionOffsetUs= */ 0); + } } // Update the holders. if (newPlayingPeriodHolder != null) { - newPlayingPeriodHolder.next = null; - loadingPeriodHolder = newPlayingPeriodHolder; - readingPeriodHolder = newPlayingPeriodHolder; - setPlayingPeriodHolder(newPlayingPeriodHolder); - if (playingPeriodHolder.hasEnabledTracks) { - periodPositionUs = playingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs); + updatePlayingPeriodRenderers(oldPlayingPeriodHolder); + if (newPlayingPeriodHolder.hasEnabledTracks) { + periodPositionUs = newPlayingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs); + newPlayingPeriodHolder.mediaPeriod.discardBuffer( + periodPositionUs - backBufferDurationUs, retainBackBufferFromKeyframe); } resetRendererPosition(periodPositionUs); maybeContinueLoading(); } else { - loadingPeriodHolder = null; - readingPeriodHolder = null; - playingPeriodHolder = null; + queue.clear(/* keepFrontPeriodUid= */ true); + // New period has not been prepared. + playbackInfo = + playbackInfo.copyWithTrackInfo(TrackGroupArray.EMPTY, emptyTrackSelectorResult); resetRendererPosition(periodPositionUs); } + handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); handler.sendEmptyMessage(MSG_DO_SOME_WORK); return periodPositionUs; } private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException { - rendererPositionUs = playingPeriodHolder == null - ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US - : playingPeriodHolder.toRendererTime(periodPositionUs); - standaloneMediaClock.setPositionUs(rendererPositionUs); + MediaPeriodHolder playingMediaPeriod = queue.getPlayingPeriod(); + rendererPositionUs = + playingMediaPeriod == null + ? periodPositionUs + : playingMediaPeriod.toRendererTime(periodPositionUs); + mediaClock.resetPosition(rendererPositionUs); for (Renderer renderer : enabledRenderers) { renderer.resetPosition(rendererPositionUs); } + notifyTrackSelectionDiscontinuity(); } private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) { - playbackParameters = rendererMediaClock != null - ? rendererMediaClock.setPlaybackParameters(playbackParameters) - : standaloneMediaClock.setPlaybackParameters(playbackParameters); - this.playbackParameters = playbackParameters; - eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget(); + mediaClock.setPlaybackParameters(playbackParameters); + sendPlaybackParametersChangedInternal( + mediaClock.getPlaybackParameters(), /* acknowledgeCommand= */ true); } - private void stopInternal() { - resetInternal(true); + private void setSeekParametersInternal(SeekParameters seekParameters) { + this.seekParameters = seekParameters; + } + + private void setForegroundModeInternal( + boolean foregroundMode, @Nullable AtomicBoolean processedFlag) { + if (this.foregroundMode != foregroundMode) { + this.foregroundMode = foregroundMode; + if (!foregroundMode) { + for (Renderer renderer : renderers) { + if (renderer.getState() == Renderer.STATE_DISABLED) { + renderer.reset(); + } + } + } + } + if (processedFlag != null) { + synchronized (this) { + processedFlag.set(true); + notifyAll(); + } + } + } + + private void stopInternal( + boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop) { + resetInternal( + /* resetRenderers= */ forceResetRenderers || !foregroundMode, + /* releaseMediaSource= */ true, + /* resetPosition= */ resetPositionAndState, + /* resetState= */ resetPositionAndState, + /* resetError= */ resetPositionAndState); + playbackInfoUpdate.incrementPendingOperationAcks( + pendingPrepareCount + (acknowledgeStop ? 1 : 0)); + pendingPrepareCount = 0; loadControl.onStopped(); - setState(ExoPlayer.STATE_IDLE); + setState(Player.STATE_IDLE); } private void releaseInternal() { - resetInternal(true); + resetInternal( + /* resetRenderers= */ true, + /* releaseMediaSource= */ true, + /* resetPosition= */ true, + /* resetState= */ true, + /* resetError= */ false); loadControl.onReleased(); - setState(ExoPlayer.STATE_IDLE); + setState(Player.STATE_IDLE); + internalPlaybackThread.quit(); synchronized (this) { released = true; notifyAll(); } } - private void resetInternal(boolean releaseMediaSource) { + private void resetInternal( + boolean resetRenderers, + boolean releaseMediaSource, + boolean resetPosition, + boolean resetState, + boolean resetError) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; - standaloneMediaClock.stop(); - rendererMediaClock = null; - rendererMediaClockSource = null; - rendererPositionUs = RENDERER_TIMESTAMP_OFFSET_US; + mediaClock.stop(); + rendererPositionUs = 0; for (Renderer renderer : enabledRenderers) { try { - ensureStopped(renderer); - renderer.disable(); + disableRenderer(renderer); } catch (ExoPlaybackException | RuntimeException e) { // There's nothing we can do. - Log.e(TAG, "Stop failed.", e); + Log.e(TAG, "Disable failed.", e); + } + } + if (resetRenderers) { + for (Renderer renderer : renderers) { + try { + renderer.reset(); + } catch (RuntimeException e) { + // There's nothing we can do. + Log.e(TAG, "Reset failed.", e); + } } } enabledRenderers = new Renderer[0]; - releasePeriodHoldersFrom(playingPeriodHolder != null ? playingPeriodHolder - : loadingPeriodHolder); - loadingPeriodHolder = null; - readingPeriodHolder = null; - playingPeriodHolder = null; - setIsLoading(false); + + if (resetPosition) { + pendingInitialSeekPosition = null; + } else if (resetState) { + // When resetting the state, also reset the period-based PlaybackInfo position and convert + // existing position to initial seek instead. + resetPosition = true; + if (pendingInitialSeekPosition == null && !playbackInfo.timeline.isEmpty()) { + playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period); + long windowPositionUs = playbackInfo.positionUs + period.getPositionInWindowUs(); + pendingInitialSeekPosition = + new SeekPosition(Timeline.EMPTY, period.windowIndex, windowPositionUs); + } + } + + queue.clear(/* keepFrontPeriodUid= */ !resetState); + shouldContinueLoading = false; + if (resetState) { + queue.setTimeline(Timeline.EMPTY); + for (PendingMessageInfo pendingMessageInfo : pendingMessages) { + pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false); + } + pendingMessages.clear(); + nextPendingMessageIndex = 0; + } + MediaPeriodId mediaPeriodId = + resetPosition + ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period) + : playbackInfo.periodId; + // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored. + long startPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.positionUs; + long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; + playbackInfo = + new PlaybackInfo( + resetState ? Timeline.EMPTY : playbackInfo.timeline, + mediaPeriodId, + startPositionUs, + contentPositionUs, + playbackInfo.playbackState, + resetError ? null : playbackInfo.playbackError, + /* isLoading= */ false, + resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, + resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, + mediaPeriodId, + startPositionUs, + /* totalBufferedDurationUs= */ 0, + startPositionUs); if (releaseMediaSource) { if (mediaSource != null) { - mediaSource.releaseSource(); + mediaSource.releaseSource(/* caller= */ this); mediaSource = null; } - timeline = null; } } - private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException { - try { - for (ExoPlayerMessage message : messages) { - message.target.handleMessage(message.messageType, message.message); + private void sendMessageInternal(PlayerMessage message) throws ExoPlaybackException { + if (message.getPositionMs() == C.TIME_UNSET) { + // If no delivery time is specified, trigger immediate message delivery. + sendMessageToTarget(message); + } else if (mediaSource == null || pendingPrepareCount > 0) { + // Still waiting for initial timeline to resolve position. + pendingMessages.add(new PendingMessageInfo(message)); + } else { + PendingMessageInfo pendingMessageInfo = new PendingMessageInfo(message); + if (resolvePendingMessagePosition(pendingMessageInfo)) { + pendingMessages.add(pendingMessageInfo); + // Ensure new message is inserted according to playback order. + Collections.sort(pendingMessages); + } else { + message.markAsProcessed(/* isDelivered= */ false); } - if (mediaSource != null) { + } + } + + private void sendMessageToTarget(PlayerMessage message) throws ExoPlaybackException { + if (message.getHandler().getLooper() == handler.getLooper()) { + deliverMessage(message); + if (playbackInfo.playbackState == Player.STATE_READY + || playbackInfo.playbackState == Player.STATE_BUFFERING) { // The message may have caused something to change that now requires us to do work. handler.sendEmptyMessage(MSG_DO_SOME_WORK); } + } else { + handler.obtainMessage(MSG_SEND_MESSAGE_TO_TARGET_THREAD, message).sendToTarget(); + } + } + + private void sendMessageToTargetThread(final PlayerMessage message) { + Handler handler = message.getHandler(); + if (!handler.getLooper().getThread().isAlive()) { + Log.w("TAG", "Trying to send message on a dead thread."); + message.markAsProcessed(/* isDelivered= */ false); + return; + } + handler.post( + () -> { + try { + deliverMessage(message); + } catch (ExoPlaybackException e) { + Log.e(TAG, "Unexpected error delivering message on external thread.", e); + throw new RuntimeException(e); + } + }); + } + + private void deliverMessage(PlayerMessage message) throws ExoPlaybackException { + if (message.isCanceled()) { + return; + } + try { + message.getTarget().handleMessage(message.getType(), message.getPayload()); } finally { - synchronized (this) { - customMessagesProcessed++; - notifyAll(); + message.markAsProcessed(/* isDelivered= */ true); + } + } + + private void resolvePendingMessagePositions() { + for (int i = pendingMessages.size() - 1; i >= 0; i--) { + if (!resolvePendingMessagePosition(pendingMessages.get(i))) { + // Unable to resolve a new position for the message. Remove it. + pendingMessages.get(i).message.markAsProcessed(/* isDelivered= */ false); + pendingMessages.remove(i); } } + // Re-sort messages by playback order. + Collections.sort(pendingMessages); + } + + private boolean resolvePendingMessagePosition(PendingMessageInfo pendingMessageInfo) { + if (pendingMessageInfo.resolvedPeriodUid == null) { + // Position is still unresolved. Try to find window in current timeline. + Pair periodPosition = + resolveSeekPosition( + new SeekPosition( + pendingMessageInfo.message.getTimeline(), + pendingMessageInfo.message.getWindowIndex(), + C.msToUs(pendingMessageInfo.message.getPositionMs())), + /* trySubsequentPeriods= */ false); + if (periodPosition == null) { + return false; + } + pendingMessageInfo.setResolvedPosition( + playbackInfo.timeline.getIndexOfPeriod(periodPosition.first), + periodPosition.second, + periodPosition.first); + } else { + // Position has been resolved for a previous timeline. Try to find the updated period index. + int index = playbackInfo.timeline.getIndexOfPeriod(pendingMessageInfo.resolvedPeriodUid); + if (index == C.INDEX_UNSET) { + return false; + } + pendingMessageInfo.resolvedPeriodIndex = index; + } + return true; + } + + private void maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPeriodPositionUs) + throws ExoPlaybackException { + if (pendingMessages.isEmpty() || playbackInfo.periodId.isAd()) { + return; + } + // If this is the first call from the start position, include oldPeriodPositionUs in potential + // trigger positions, but make sure we deliver it only once. + if (playbackInfo.startPositionUs == oldPeriodPositionUs + && deliverPendingMessageAtStartPositionRequired) { + oldPeriodPositionUs--; + } + deliverPendingMessageAtStartPositionRequired = false; + + // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages) + int currentPeriodIndex = + playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid); + PendingMessageInfo previousInfo = + nextPendingMessageIndex > 0 ? pendingMessages.get(nextPendingMessageIndex - 1) : null; + while (previousInfo != null + && (previousInfo.resolvedPeriodIndex > currentPeriodIndex + || (previousInfo.resolvedPeriodIndex == currentPeriodIndex + && previousInfo.resolvedPeriodTimeUs > oldPeriodPositionUs))) { + nextPendingMessageIndex--; + previousInfo = + nextPendingMessageIndex > 0 ? pendingMessages.get(nextPendingMessageIndex - 1) : null; + } + PendingMessageInfo nextInfo = + nextPendingMessageIndex < pendingMessages.size() + ? pendingMessages.get(nextPendingMessageIndex) + : null; + while (nextInfo != null + && nextInfo.resolvedPeriodUid != null + && (nextInfo.resolvedPeriodIndex < currentPeriodIndex + || (nextInfo.resolvedPeriodIndex == currentPeriodIndex + && nextInfo.resolvedPeriodTimeUs <= oldPeriodPositionUs))) { + nextPendingMessageIndex++; + nextInfo = + nextPendingMessageIndex < pendingMessages.size() + ? pendingMessages.get(nextPendingMessageIndex) + : null; + } + // Check if any message falls within the covered time span. + while (nextInfo != null + && nextInfo.resolvedPeriodUid != null + && nextInfo.resolvedPeriodIndex == currentPeriodIndex + && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs + && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { + try { + sendMessageToTarget(nextInfo.message); + } finally { + if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) { + pendingMessages.remove(nextPendingMessageIndex); + } else { + nextPendingMessageIndex++; + } + } + nextInfo = + nextPendingMessageIndex < pendingMessages.size() + ? pendingMessages.get(nextPendingMessageIndex) + : null; + } } private void ensureStopped(Renderer renderer) throws ExoPlaybackException { @@ -751,20 +1130,26 @@ import java.io.IOException; } } + private void disableRenderer(Renderer renderer) throws ExoPlaybackException { + mediaClock.onRendererDisabled(renderer); + ensureStopped(renderer); + renderer.disable(); + } + private void reselectTracksInternal() throws ExoPlaybackException { - if (playingPeriodHolder == null) { - // We don't have tracks yet, so we don't care. - return; - } + float playbackSpeed = mediaClock.getPlaybackParameters().speed; // Reselect tracks on each period in turn, until the selection changes. - MediaPeriodHolder periodHolder = playingPeriodHolder; + MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); boolean selectionsChangedForReadPeriod = true; + TrackSelectorResult newTrackSelectorResult; while (true) { if (periodHolder == null || !periodHolder.prepared) { // The reselection did not change any prepared periods. return; } - if (periodHolder.selectTracks()) { + newTrackSelectorResult = periodHolder.selectTracks(playbackSpeed, playbackInfo.timeline); + if (!newTrackSelectorResult.isEquivalent(periodHolder.getTrackSelectorResult())) { // Selected tracks have changed for this period. break; } @@ -772,22 +1157,24 @@ import java.io.IOException; // The track reselection didn't affect any period that has been read. selectionsChangedForReadPeriod = false; } - periodHolder = periodHolder.next; + periodHolder = periodHolder.getNext(); } if (selectionsChangedForReadPeriod) { // Update streams and rebuffer for the new selection, recreating all streams if reading ahead. - boolean recreateStreams = readingPeriodHolder != playingPeriodHolder; - releasePeriodHoldersFrom(playingPeriodHolder.next); - playingPeriodHolder.next = null; - loadingPeriodHolder = playingPeriodHolder; - readingPeriodHolder = playingPeriodHolder; + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + boolean recreateStreams = queue.removeAfter(playingPeriodHolder); boolean[] streamResetFlags = new boolean[renderers.length]; - long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( - playbackInfo.positionUs, recreateStreams, streamResetFlags); - if (periodPositionUs != playbackInfo.positionUs) { - playbackInfo.positionUs = periodPositionUs; + long periodPositionUs = + playingPeriodHolder.applyTrackSelection( + newTrackSelectorResult, playbackInfo.positionUs, recreateStreams, streamResetFlags); + if (playbackInfo.playbackState != Player.STATE_ENDED + && periodPositionUs != playbackInfo.positionUs) { + playbackInfo = + copyWithNewPosition( + playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs); + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); resetRendererPosition(periodPositionUs); } @@ -803,757 +1190,761 @@ import java.io.IOException; if (rendererWasEnabledFlags[i]) { if (sampleStream != renderer.getStream()) { // We need to disable the renderer. - if (renderer == rendererMediaClockSource) { - // The renderer is providing the media clock. - if (sampleStream == null) { - // The renderer won't be re-enabled. Sync standaloneMediaClock so that it can take - // over timing responsibilities. - standaloneMediaClock.synchronize(rendererMediaClock); - } - rendererMediaClock = null; - rendererMediaClockSource = null; - } - ensureStopped(renderer); - renderer.disable(); + disableRenderer(renderer); } else if (streamResetFlags[i]) { // The renderer will continue to consume from its current stream, but needs to be reset. renderer.resetPosition(rendererPositionUs); } } } - eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult) - .sendToTarget(); + playbackInfo = + playbackInfo.copyWithTrackInfo( + playingPeriodHolder.getTrackGroups(), playingPeriodHolder.getTrackSelectorResult()); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } else { // Release and re-prepare/buffer periods after the one whose selection changed. - loadingPeriodHolder = periodHolder; - periodHolder = loadingPeriodHolder.next; - while (periodHolder != null) { - periodHolder.release(); - periodHolder = periodHolder.next; - } - loadingPeriodHolder.next = null; - if (loadingPeriodHolder.prepared) { - long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.startPositionUs, - loadingPeriodHolder.toPeriodTime(rendererPositionUs)); - loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); + queue.removeAfter(periodHolder); + if (periodHolder.prepared) { + long loadingPeriodPositionUs = + Math.max( + periodHolder.info.startPositionUs, periodHolder.toPeriodTime(rendererPositionUs)); + periodHolder.applyTrackSelection(newTrackSelectorResult, loadingPeriodPositionUs, false); } } - maybeContinueLoading(); - updatePlaybackPositions(); - handler.sendEmptyMessage(MSG_DO_SOME_WORK); - } - - private boolean isTimelineReady(long playingPeriodDurationUs) { - return playingPeriodDurationUs == C.TIME_UNSET - || playbackInfo.positionUs < playingPeriodDurationUs - || (playingPeriodHolder.next != null && playingPeriodHolder.next.prepared); - } - - private boolean haveSufficientBuffer(boolean rebuffering) { - long loadingPeriodBufferedPositionUs = !loadingPeriodHolder.prepared - ? loadingPeriodHolder.startPositionUs - : loadingPeriodHolder.mediaPeriod.getBufferedPositionUs(); - if (loadingPeriodBufferedPositionUs == C.TIME_END_OF_SOURCE) { - if (loadingPeriodHolder.isLast) { - return true; - } - loadingPeriodBufferedPositionUs = timeline.getPeriod(loadingPeriodHolder.index, period) - .getDurationUs(); + handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ true); + if (playbackInfo.playbackState != Player.STATE_ENDED) { + maybeContinueLoading(); + updatePlaybackPositions(); + handler.sendEmptyMessage(MSG_DO_SOME_WORK); } - return loadControl.shouldStartPlayback( - loadingPeriodBufferedPositionUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs), - rebuffering); } - private void maybeThrowPeriodPrepareError() throws IOException { - if (loadingPeriodHolder != null && !loadingPeriodHolder.prepared - && (readingPeriodHolder == null || readingPeriodHolder.next == loadingPeriodHolder)) { + private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) { + MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); + while (periodHolder != null) { + TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll(); + for (TrackSelection trackSelection : trackSelections) { + if (trackSelection != null) { + trackSelection.onPlaybackSpeed(playbackSpeed); + } + } + periodHolder = periodHolder.getNext(); + } + } + + private void notifyTrackSelectionDiscontinuity() { + MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); + while (periodHolder != null) { + TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll(); + for (TrackSelection trackSelection : trackSelections) { + if (trackSelection != null) { + trackSelection.onDiscontinuity(); + } + } + periodHolder = periodHolder.getNext(); + } + } + + private boolean shouldTransitionToReadyState(boolean renderersReadyOrEnded) { + if (enabledRenderers.length == 0) { + // If there are no enabled renderers, determine whether we're ready based on the timeline. + return isTimelineReady(); + } + if (!renderersReadyOrEnded) { + return false; + } + if (!playbackInfo.isLoading) { + // Renderers are ready and we're not loading. Transition to ready, since the alternative is + // getting stuck waiting for additional media that's not being loaded. + return true; + } + // Renderers are ready and we're loading. Ask the LoadControl whether to transition. + MediaPeriodHolder loadingHolder = queue.getLoadingPeriod(); + boolean bufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal; + return bufferedToEnd + || loadControl.shouldStartPlayback( + getTotalBufferedDurationUs(), mediaClock.getPlaybackParameters().speed, rebuffering); + } + + private boolean isTimelineReady() { + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; + return playingPeriodHolder.prepared + && (playingPeriodDurationUs == C.TIME_UNSET + || playbackInfo.positionUs < playingPeriodDurationUs); + } + + private void maybeThrowSourceInfoRefreshError() throws IOException { + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); + if (loadingPeriodHolder != null) { + // Defer throwing until we read all available media periods. for (Renderer renderer : enabledRenderers) { if (!renderer.hasReadStreamToEnd()) { return; } } - loadingPeriodHolder.mediaPeriod.maybeThrowPrepareError(); } + mediaSource.maybeThrowSourceInfoRefreshError(); } - private void handleSourceInfoRefreshed(Pair timelineAndManifest) + private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo) throws ExoPlaybackException { - Timeline oldTimeline = timeline; - timeline = timelineAndManifest.first; - Object manifest = timelineAndManifest.second; - - int processedInitialSeekCount = 0; - if (oldTimeline == null) { - if (pendingInitialSeekCount > 0) { - Pair periodPosition = resolveSeekPosition(pendingSeekPosition); - processedInitialSeekCount = pendingInitialSeekCount; - pendingInitialSeekCount = 0; - pendingSeekPosition = null; - if (periodPosition == null) { - // The seek position was valid for the timeline that it was performed into, but the - // timeline has changed and a suitable seek position could not be resolved in the new one. - handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); - return; - } - playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second); - } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { - if (timeline.isEmpty()) { - handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); - return; - } - Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); - playbackInfo = new PlaybackInfo(defaultPosition.first, defaultPosition.second); - } - } - - MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder - : loadingPeriodHolder; - if (periodHolder == null) { - // We don't have any period holders, so we're done. - notifySourceInfoRefresh(manifest, processedInitialSeekCount); + if (sourceRefreshInfo.source != mediaSource) { + // Stale event. return; } + playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); + pendingPrepareCount = 0; - int periodIndex = timeline.getIndexOfPeriod(periodHolder.uid); - if (periodIndex == C.INDEX_UNSET) { - // We didn't find the current period in the new timeline. Attempt to resolve a subsequent - // period whose window we can restart from. - int newPeriodIndex = resolveSubsequentPeriod(periodHolder.index, oldTimeline, timeline); - if (newPeriodIndex == C.INDEX_UNSET) { - // We failed to resolve a suitable restart position. - handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); + Timeline oldTimeline = playbackInfo.timeline; + Timeline timeline = sourceRefreshInfo.timeline; + queue.setTimeline(timeline); + playbackInfo = playbackInfo.copyWithTimeline(timeline); + resolvePendingMessagePositions(); + + MediaPeriodId newPeriodId = playbackInfo.periodId; + long oldContentPositionUs = + playbackInfo.periodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs; + long newContentPositionUs = oldContentPositionUs; + if (pendingInitialSeekPosition != null) { + // Resolve initial seek position. + Pair periodPosition = + resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); + pendingInitialSeekPosition = null; + if (periodPosition == null) { + // The seek position was valid for the timeline that it was performed into, but the + // timeline has changed and a suitable seek position could not be resolved in the new one. + handleSourceInfoRefreshEndedPlayback(); return; } - // We resolved a subsequent period. Seek to the default position in the corresponding window. - Pair defaultPosition = getPeriodPosition( - timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET); - newPeriodIndex = defaultPosition.first; - long newPositionUs = defaultPosition.second; - timeline.getPeriod(newPeriodIndex, period, true); - // Clear the index of each holder that doesn't contain the default position. If a holder - // contains the default position then update its index so it can be re-used when seeking. - Object newPeriodUid = period.uid; - periodHolder.index = C.INDEX_UNSET; - while (periodHolder.next != null) { - periodHolder = periodHolder.next; - periodHolder.index = periodHolder.uid.equals(newPeriodUid) ? newPeriodIndex : C.INDEX_UNSET; + newContentPositionUs = periodPosition.second; + newPeriodId = queue.resolveMediaPeriodIdForAds(periodPosition.first, newContentPositionUs); + } else if (oldContentPositionUs == C.TIME_UNSET && !timeline.isEmpty()) { + // Resolve unset start position to default position. + Pair defaultPosition = + getPeriodPosition( + timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); + newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, defaultPosition.second); + if (!newPeriodId.isAd()) { + // Keep unset start position if we need to play an ad first. + newContentPositionUs = defaultPosition.second; + } + } else if (timeline.getIndexOfPeriod(newPeriodId.periodUid) == C.INDEX_UNSET) { + // The current period isn't in the new timeline. Attempt to resolve a subsequent period whose + // window we can restart from. + Object newPeriodUid = resolveSubsequentPeriod(newPeriodId.periodUid, oldTimeline, timeline); + if (newPeriodUid == null) { + // We failed to resolve a suitable restart position. + handleSourceInfoRefreshEndedPlayback(); + return; + } + // We resolved a subsequent period. Start at the default position in the corresponding window. + Pair defaultPosition = + getPeriodPosition( + timeline, timeline.getPeriodByUid(newPeriodUid, period).windowIndex, C.TIME_UNSET); + newContentPositionUs = defaultPosition.second; + newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, newContentPositionUs); + } else { + // Recheck if the current ad still needs to be played or if we need to start playing an ad. + newPeriodId = + queue.resolveMediaPeriodIdForAds(playbackInfo.periodId.periodUid, newContentPositionUs); + if (!playbackInfo.periodId.isAd() && !newPeriodId.isAd()) { + // Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and + // only MediaPeriodId.nextAdGroupIndex may have changed. This postpones a potential + // discontinuity until we reach the former next ad group position. + newPeriodId = playbackInfo.periodId; + } + } + + if (playbackInfo.periodId.equals(newPeriodId) && oldContentPositionUs == newContentPositionUs) { + // We can keep the current playing period. Update the rest of the queued periods. + if (!queue.updateQueuedPeriods(rendererPositionUs, getMaxRendererReadPositionUs())) { + seekToCurrentPosition(/* sendDiscontinuity= */ false); + } + } else { + // Something changed. Seek to new start position. + MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); + if (periodHolder != null) { + // Update the new playing media period info if it already exists. + while (periodHolder.getNext() != null) { + periodHolder = periodHolder.getNext(); + if (periodHolder.info.id.equals(newPeriodId)) { + periodHolder.info = queue.getUpdatedMediaPeriodInfo(periodHolder.info); + } + } } // Actually do the seek. - newPositionUs = seekToPeriodPosition(newPeriodIndex, newPositionUs); - playbackInfo = new PlaybackInfo(newPeriodIndex, newPositionUs); - notifySourceInfoRefresh(manifest, processedInitialSeekCount); - return; + long newPositionUs = newPeriodId.isAd() ? 0 : newContentPositionUs; + long seekedToPositionUs = seekToPeriodPosition(newPeriodId, newPositionUs); + playbackInfo = copyWithNewPosition(newPeriodId, seekedToPositionUs, newContentPositionUs); } + handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); + } - // The current period is in the new timeline. Update the holder and playbackInfo. - timeline.getPeriod(periodIndex, period); - boolean isLastPeriod = periodIndex == timeline.getPeriodCount() - 1 - && !timeline.getWindow(period.windowIndex, window).isDynamic; - periodHolder.setIndex(periodIndex, isLastPeriod); - boolean seenReadingPeriod = periodHolder == readingPeriodHolder; - if (periodIndex != playbackInfo.periodIndex) { - playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); + private long getMaxRendererReadPositionUs() { + MediaPeriodHolder readingHolder = queue.getReadingPeriod(); + if (readingHolder == null) { + return 0; } - - // If there are subsequent holders, update the index for each of them. If we find a holder - // that's inconsistent with the new timeline then take appropriate action. - while (periodHolder.next != null) { - MediaPeriodHolder previousPeriodHolder = periodHolder; - periodHolder = periodHolder.next; - periodIndex++; - timeline.getPeriod(periodIndex, period, true); - isLastPeriod = periodIndex == timeline.getPeriodCount() - 1 - && !timeline.getWindow(period.windowIndex, window).isDynamic; - if (periodHolder.uid.equals(period.uid)) { - // The holder is consistent with the new timeline. Update its index and continue. - periodHolder.setIndex(periodIndex, isLastPeriod); - seenReadingPeriod |= (periodHolder == readingPeriodHolder); + long maxReadPositionUs = readingHolder.getRendererOffset(); + if (!readingHolder.prepared) { + return maxReadPositionUs; + } + for (int i = 0; i < renderers.length; i++) { + if (renderers[i].getState() == Renderer.STATE_DISABLED + || renderers[i].getStream() != readingHolder.sampleStreams[i]) { + // Ignore disabled renderers and renderers with sample streams from previous periods. + continue; + } + long readingPositionUs = renderers[i].getReadingPositionUs(); + if (readingPositionUs == C.TIME_END_OF_SOURCE) { + return C.TIME_END_OF_SOURCE; } else { - // The holder is inconsistent with the new timeline. - if (!seenReadingPeriod) { - // Renderers may have read from a period that's been removed. Seek back to the current - // position of the playing period to make sure none of the removed period is played. - periodIndex = playingPeriodHolder.index; - long newPositionUs = seekToPeriodPosition(periodIndex, playbackInfo.positionUs); - playbackInfo = new PlaybackInfo(periodIndex, newPositionUs); - } else { - // Update the loading period to be the last period that's still valid, and release all - // subsequent periods. - loadingPeriodHolder = previousPeriodHolder; - loadingPeriodHolder.next = null; - // Release the rest of the timeline. - releasePeriodHoldersFrom(periodHolder); - } - break; + maxReadPositionUs = Math.max(readingPositionUs, maxReadPositionUs); } } - - notifySourceInfoRefresh(manifest, processedInitialSeekCount); + return maxReadPositionUs; } - private void handleSourceInfoRefreshEndedPlayback(Object manifest, - int processedInitialSeekCount) { - // Set the playback position to (0,0) for notifying the eventHandler. - playbackInfo = new PlaybackInfo(0, 0); - notifySourceInfoRefresh(manifest, processedInitialSeekCount); - // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't ignored. - playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); - setState(ExoPlayer.STATE_ENDED); + private void handleSourceInfoRefreshEndedPlayback() { + if (playbackInfo.playbackState != Player.STATE_IDLE) { + setState(Player.STATE_ENDED); + } // Reset, but retain the source so that it can still be used should a seek occur. - resetInternal(false); - } - - private void notifySourceInfoRefresh(Object manifest, int processedInitialSeekCount) { - eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, - new SourceInfo(timeline, manifest, playbackInfo, processedInitialSeekCount)).sendToTarget(); + resetInternal( + /* resetRenderers= */ false, + /* releaseMediaSource= */ false, + /* resetPosition= */ true, + /* resetState= */ false, + /* resetError= */ true); } /** * Given a period index into an old timeline, finds the first subsequent period that also exists - * in a new timeline. The index of this period in the new timeline is returned. + * in a new timeline. The uid of this period in the new timeline is returned. * - * @param oldPeriodIndex The index of the period in the old timeline. + * @param oldPeriodUid The index of the period in the old timeline. * @param oldTimeline The old timeline. * @param newTimeline The new timeline. - * @return The index in the new timeline of the first subsequent period, or {@link C#INDEX_UNSET} - * if no such period was found. + * @return The uid in the new timeline of the first subsequent period, or null if no such period + * was found. */ - private int resolveSubsequentPeriod(int oldPeriodIndex, Timeline oldTimeline, - Timeline newTimeline) { + private @Nullable Object resolveSubsequentPeriod( + Object oldPeriodUid, Timeline oldTimeline, Timeline newTimeline) { + int oldPeriodIndex = oldTimeline.getIndexOfPeriod(oldPeriodUid); int newPeriodIndex = C.INDEX_UNSET; - while (newPeriodIndex == C.INDEX_UNSET && oldPeriodIndex < oldTimeline.getPeriodCount() - 1) { - newPeriodIndex = newTimeline.getIndexOfPeriod( - oldTimeline.getPeriod(++oldPeriodIndex, period, true).uid); + int maxIterations = oldTimeline.getPeriodCount(); + for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) { + oldPeriodIndex = + oldTimeline.getNextPeriodIndex( + oldPeriodIndex, period, window, repeatMode, shuffleModeEnabled); + if (oldPeriodIndex == C.INDEX_UNSET) { + // We've reached the end of the old timeline. + break; + } + newPeriodIndex = newTimeline.getIndexOfPeriod(oldTimeline.getUidOfPeriod(oldPeriodIndex)); } - return newPeriodIndex; + return newPeriodIndex == C.INDEX_UNSET ? null : newTimeline.getUidOfPeriod(newPeriodIndex); } /** - * Converts a {@link SeekPosition} into the corresponding (periodIndex, periodPositionUs) for the + * Converts a {@link SeekPosition} into the corresponding (periodUid, periodPositionUs) for the * internal timeline. * * @param seekPosition The position to resolve. + * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching + * period if the original period is no longer available. * @return The resolved position, or null if resolution was not successful. * @throws IllegalSeekPositionException If the window index of the seek position is outside the * bounds of the timeline. */ - private Pair resolveSeekPosition(SeekPosition seekPosition) { + @Nullable + private Pair resolveSeekPosition( + SeekPosition seekPosition, boolean trySubsequentPeriods) { + Timeline timeline = playbackInfo.timeline; Timeline seekTimeline = seekPosition.timeline; + if (timeline.isEmpty()) { + // We don't have a valid timeline yet, so we can't resolve the position. + return null; + } if (seekTimeline.isEmpty()) { - // The application performed a blind seek without a non-empty timeline (most likely based on + // The application performed a blind seek with an empty timeline (most likely based on // knowledge of what the future timeline will be). Use the internal timeline. seekTimeline = timeline; } // Map the SeekPosition to a position in the corresponding timeline. - Pair periodPosition; + Pair periodPosition; try { - periodPosition = getPeriodPosition(seekTimeline, seekPosition.windowIndex, - seekPosition.windowPositionUs); + periodPosition = + seekTimeline.getPeriodPosition( + window, period, seekPosition.windowIndex, seekPosition.windowPositionUs); } catch (IndexOutOfBoundsException e) { // The window index of the seek position was outside the bounds of the timeline. - throw new IllegalSeekPositionException(timeline, seekPosition.windowIndex, - seekPosition.windowPositionUs); + return null; } if (timeline == seekTimeline) { // Our internal timeline is the seek timeline, so the mapped position is correct. return periodPosition; } // Attempt to find the mapped period in the internal timeline. - int periodIndex = timeline.getIndexOfPeriod( - seekTimeline.getPeriod(periodPosition.first, period, true).uid); + int periodIndex = timeline.getIndexOfPeriod(periodPosition.first); if (periodIndex != C.INDEX_UNSET) { // We successfully located the period in the internal timeline. - return Pair.create(periodIndex, periodPosition.second); + return periodPosition; } - // Try and find a subsequent period from the seek timeline in the internal timeline. - periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); - if (periodIndex != C.INDEX_UNSET) { - // We found one. Map the SeekPosition onto the corresponding default position. - return getPeriodPosition(timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET); + if (trySubsequentPeriods) { + // Try and find a subsequent period from the seek timeline in the internal timeline. + @Nullable + Object periodUid = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); + if (periodUid != null) { + // We found one. Use the default position of the corresponding window. + return getPeriodPosition( + timeline, timeline.getPeriodByUid(periodUid, period).windowIndex, C.TIME_UNSET); + } } // We didn't find one. Give up. return null; } /** - * Calls {@link #getPeriodPosition(Timeline, int, long)} using the current timeline. + * Calls {@link Timeline#getPeriodPosition(Timeline.Window, Timeline.Period, int, long)} using the + * current timeline. */ - private Pair getPeriodPosition(int windowIndex, long windowPositionUs) { - return getPeriodPosition(timeline, windowIndex, windowPositionUs); - } - - /** - * Calls {@link #getPeriodPosition(Timeline, int, long, long)} with a zero default position - * projection. - */ - private Pair getPeriodPosition(Timeline timeline, int windowIndex, - long windowPositionUs) { - return getPeriodPosition(timeline, windowIndex, windowPositionUs, 0); - } - - /** - * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs). - * - * @param timeline The timeline containing the window. - * @param windowIndex The window index. - * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default - * start position. - * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the - * duration into the future by which the window's position should be projected. - * @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs} - * is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's - * position could not be projected by {@code defaultPositionProjectionUs}. - */ - private Pair getPeriodPosition(Timeline timeline, int windowIndex, - long windowPositionUs, long defaultPositionProjectionUs) { - Assertions.checkIndex(windowIndex, 0, timeline.getWindowCount()); - timeline.getWindow(windowIndex, window, false, defaultPositionProjectionUs); - if (windowPositionUs == C.TIME_UNSET) { - windowPositionUs = window.getDefaultPositionUs(); - if (windowPositionUs == C.TIME_UNSET) { - return null; - } - } - int periodIndex = window.firstPeriodIndex; - long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs; - long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs(); - while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs - && periodIndex < window.lastPeriodIndex) { - periodPositionUs -= periodDurationUs; - periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs(); - } - return Pair.create(periodIndex, periodPositionUs); + private Pair getPeriodPosition( + Timeline timeline, int windowIndex, long windowPositionUs) { + return timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); } private void updatePeriods() throws ExoPlaybackException, IOException { - if (timeline == null) { + if (mediaSource == null) { + // The player has no media source yet. + return; + } + if (pendingPrepareCount > 0) { // We're waiting to get information about periods. mediaSource.maybeThrowSourceInfoRefreshError(); return; } - - // Update the loading period if required. maybeUpdateLoadingPeriod(); - if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { - setIsLoading(false); - } else if (loadingPeriodHolder != null && loadingPeriodHolder.needsContinueLoading) { + maybeUpdateReadingPeriod(); + maybeUpdatePlayingPeriod(); + } + + private void maybeUpdateLoadingPeriod() throws ExoPlaybackException, IOException { + queue.reevaluateBuffer(rendererPositionUs); + if (queue.shouldLoadNextMediaPeriod()) { + MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo); + if (info == null) { + maybeThrowSourceInfoRefreshError(); + } else { + MediaPeriodHolder mediaPeriodHolder = + queue.enqueueNextMediaPeriodHolder( + rendererCapabilities, + trackSelector, + loadControl.getAllocator(), + mediaSource, + info, + emptyTrackSelectorResult); + mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); + if (queue.getPlayingPeriod() == mediaPeriodHolder) { + resetRendererPosition(mediaPeriodHolder.getStartPositionRendererTime()); + } + handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); + } + } + if (shouldContinueLoading) { + shouldContinueLoading = isLoadingPossible(); + updateIsLoading(); + } else { maybeContinueLoading(); } + } - if (playingPeriodHolder == null) { - // We're waiting for the first period to be prepared. + private void maybeUpdateReadingPeriod() throws ExoPlaybackException { + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + if (readingPeriodHolder == null) { return; } - // Update the playing and reading periods. - while (playingPeriodHolder != readingPeriodHolder - && rendererPositionUs >= playingPeriodHolder.next.rendererPositionOffsetUs) { - // All enabled renderers' streams have been read to the end, and the playback position reached - // the end of the playing period, so advance playback to the next period. - playingPeriodHolder.release(); - setPlayingPeriodHolder(playingPeriodHolder.next); - playbackInfo = new PlaybackInfo(playingPeriodHolder.index, - playingPeriodHolder.startPositionUs); - updatePlaybackPositions(); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); - } - - if (readingPeriodHolder.isLast) { - for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; - // Defer setting the stream as final until the renderer has actually consumed the whole - // stream in case of playlist changes that cause the stream to be no longer final. - if (sampleStream != null && renderer.getStream() == sampleStream - && renderer.hasReadStreamToEnd()) { - renderer.setCurrentStreamFinal(); + if (readingPeriodHolder.getNext() == null) { + // We don't have a successor to advance the reading period to. + if (readingPeriodHolder.info.isFinal) { + for (int i = 0; i < renderers.length; i++) { + Renderer renderer = renderers[i]; + SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; + // Defer setting the stream as final until the renderer has actually consumed the whole + // stream in case of playlist changes that cause the stream to be no longer final. + if (sampleStream != null + && renderer.getStream() == sampleStream + && renderer.hasReadStreamToEnd()) { + renderer.setCurrentStreamFinal(); + } } } return; } + if (!hasReadingPeriodFinishedReading()) { + return; + } + + if (!readingPeriodHolder.getNext().prepared) { + // The successor is not prepared yet. + return; + } + + TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult(); + readingPeriodHolder = queue.advanceReadingPeriod(); + TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult(); + + if (readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET) { + // The new period starts with a discontinuity, so the renderers will play out all data, then + // be disabled and re-enabled when they start playing the next period. + setAllRendererStreamsFinal(); + return; + } + for (int i = 0; i < renderers.length; i++) { + Renderer renderer = renderers[i]; + boolean rendererWasEnabled = oldTrackSelectorResult.isRendererEnabled(i); + if (rendererWasEnabled && !renderer.isCurrentStreamFinal()) { + // The renderer is enabled and its stream is not final, so we still have a chance to replace + // the sample streams. + TrackSelection newSelection = newTrackSelectorResult.selections.get(i); + boolean newRendererEnabled = newTrackSelectorResult.isRendererEnabled(i); + boolean isNoSampleRenderer = rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE; + RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i]; + RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i]; + if (newRendererEnabled && newConfig.equals(oldConfig) && !isNoSampleRenderer) { + // Replace the renderer's SampleStream so the transition to playing the next period can + // be seamless. + // This should be avoided for no-sample renderer, because skipping ahead for such + // renderer doesn't have any benefit (the renderer does not consume the sample stream), + // and it will change the provided rendererOffsetUs while the renderer is still + // rendering from the playing media period. + Format[] formats = getFormats(newSelection); + renderer.replaceStream( + formats, + readingPeriodHolder.sampleStreams[i], + readingPeriodHolder.getRendererOffset()); + } else { + // The renderer will be disabled when transitioning to playing the next period, because + // there's no new selection, or because a configuration change is required, or because + // it's a no-sample renderer for which rendererOffsetUs should be updated only when + // starting to play the next period. Mark the SampleStream as final to play out any + // remaining data. + renderer.setCurrentStreamFinal(); + } + } + } + } + + private void maybeUpdatePlayingPeriod() throws ExoPlaybackException { + boolean advancedPlayingPeriod = false; + while (shouldAdvancePlayingPeriod()) { + if (advancedPlayingPeriod) { + // If we advance more than one period at a time, notify listeners after each update. + maybeNotifyPlaybackInfoChanged(); + } + MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); + if (oldPlayingPeriodHolder == queue.getReadingPeriod()) { + // The reading period hasn't advanced yet, so we can't seamlessly replace the SampleStreams + // anymore and need to re-enable the renderers. Set all current streams final to do that. + setAllRendererStreamsFinal(); + } + MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod(); + updatePlayingPeriodRenderers(oldPlayingPeriodHolder); + playbackInfo = + copyWithNewPosition( + newPlayingPeriodHolder.info.id, + newPlayingPeriodHolder.info.startPositionUs, + newPlayingPeriodHolder.info.contentPositionUs); + int discontinuityReason = + oldPlayingPeriodHolder.info.isLastInTimelinePeriod + ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION + : Player.DISCONTINUITY_REASON_AD_INSERTION; + playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason); + updatePlaybackPositions(); + advancedPlayingPeriod = true; + } + } + + private boolean shouldAdvancePlayingPeriod() { + if (!playWhenReady) { + return false; + } + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + if (playingPeriodHolder == null) { + return false; + } + MediaPeriodHolder nextPlayingPeriodHolder = playingPeriodHolder.getNext(); + if (nextPlayingPeriodHolder == null) { + return false; + } + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + if (playingPeriodHolder == readingPeriodHolder && !hasReadingPeriodFinishedReading()) { + return false; + } + return rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime(); + } + + private boolean hasReadingPeriodFinishedReading() { + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + if (!readingPeriodHolder.prepared) { + return false; + } for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; if (renderer.getStream() != sampleStream || (sampleStream != null && !renderer.hasReadStreamToEnd())) { - return; + // The current reading period is still being read by at least one renderer. + return false; } } + return true; + } - if (readingPeriodHolder.next != null && readingPeriodHolder.next.prepared) { - TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.trackSelectorResult; - readingPeriodHolder = readingPeriodHolder.next; - TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.trackSelectorResult; - - boolean initialDiscontinuity = - readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET; - for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - TrackSelection oldSelection = oldTrackSelectorResult.selections.get(i); - if (oldSelection == null) { - // The renderer has no current stream and will be enabled when we play the next period. - } else if (initialDiscontinuity) { - // The new period starts with a discontinuity, so the renderer will play out all data then - // be disabled and re-enabled when it starts playing the next period. - renderer.setCurrentStreamFinal(); - } else if (!renderer.isCurrentStreamFinal()) { - TrackSelection newSelection = newTrackSelectorResult.selections.get(i); - RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i]; - RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i]; - if (newSelection != null && newConfig.equals(oldConfig)) { - // Replace the renderer's SampleStream so the transition to playing the next period can - // be seamless. - Format[] formats = new Format[newSelection.length()]; - for (int j = 0; j < formats.length; j++) { - formats[j] = newSelection.getFormat(j); - } - renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i], - readingPeriodHolder.getRendererOffset()); - } else { - // The renderer will be disabled when transitioning to playing the next period, either - // because there's no new selection or because a configuration change is required. Mark - // the SampleStream as final to play out any remaining data. - renderer.setCurrentStreamFinal(); - } - } + private void setAllRendererStreamsFinal() { + for (Renderer renderer : renderers) { + if (renderer.getStream() != null) { + renderer.setCurrentStreamFinal(); } } } - private void maybeUpdateLoadingPeriod() throws IOException { - int newLoadingPeriodIndex; - if (loadingPeriodHolder == null) { - newLoadingPeriodIndex = playbackInfo.periodIndex; - } else { - int loadingPeriodIndex = loadingPeriodHolder.index; - if (loadingPeriodHolder.isLast || !loadingPeriodHolder.isFullyBuffered() - || timeline.getPeriod(loadingPeriodIndex, period).getDurationUs() == C.TIME_UNSET) { - // Either the existing loading period is the last period, or we are not ready to advance to - // loading the next period because it hasn't been fully buffered or its duration is unknown. - return; - } - if (playingPeriodHolder != null - && loadingPeriodIndex - playingPeriodHolder.index == MAXIMUM_BUFFER_AHEAD_PERIODS) { - // We are already buffering the maximum number of periods ahead. - return; - } - newLoadingPeriodIndex = loadingPeriodHolder.index + 1; - } - - if (newLoadingPeriodIndex >= timeline.getPeriodCount()) { - // The next period is not available yet. - mediaSource.maybeThrowSourceInfoRefreshError(); - return; - } - - long newLoadingPeriodStartPositionUs; - if (loadingPeriodHolder == null) { - newLoadingPeriodStartPositionUs = playbackInfo.positionUs; - } else { - int newLoadingWindowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex; - if (newLoadingPeriodIndex - != timeline.getWindow(newLoadingWindowIndex, window).firstPeriodIndex) { - // We're starting to buffer a new period in the current window. Always start from the - // beginning of the period. - newLoadingPeriodStartPositionUs = 0; - } else { - // We're starting to buffer a new window. When playback transitions to this window we'll - // want it to be from its default start position. The expected delay until playback - // transitions is equal the duration of media that's currently buffered (assuming no - // interruptions). Hence we project the default start position forward by the duration of - // the buffer, and start buffering from this point. - long defaultPositionProjectionUs = loadingPeriodHolder.getRendererOffset() - + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() - - rendererPositionUs; - Pair defaultPosition = getPeriodPosition(timeline, newLoadingWindowIndex, - C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs)); - if (defaultPosition == null) { - return; - } - - newLoadingPeriodIndex = defaultPosition.first; - newLoadingPeriodStartPositionUs = defaultPosition.second; - } - } - - long rendererPositionOffsetUs = loadingPeriodHolder == null - ? newLoadingPeriodStartPositionUs + RENDERER_TIMESTAMP_OFFSET_US - : (loadingPeriodHolder.getRendererOffset() - + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()); - timeline.getPeriod(newLoadingPeriodIndex, period, true); - boolean isLastPeriod = newLoadingPeriodIndex == timeline.getPeriodCount() - 1 - && !timeline.getWindow(period.windowIndex, window).isDynamic; - MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, - rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid, - newLoadingPeriodIndex, isLastPeriod, newLoadingPeriodStartPositionUs); - if (loadingPeriodHolder != null) { - loadingPeriodHolder.next = newPeriodHolder; - } - loadingPeriodHolder = newPeriodHolder; - loadingPeriodHolder.mediaPeriod.prepare(this); - setIsLoading(true); - } - - private void handlePeriodPrepared(MediaPeriod period) throws ExoPlaybackException { - if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) { + private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackException { + if (!queue.isLoading(mediaPeriod)) { // Stale event. return; } - loadingPeriodHolder.handlePrepared(); - if (playingPeriodHolder == null) { - // This is the first prepared period, so start playing it. - readingPeriodHolder = loadingPeriodHolder; - resetRendererPosition(readingPeriodHolder.startPositionUs); - setPlayingPeriodHolder(readingPeriodHolder); + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); + loadingPeriodHolder.handlePrepared( + mediaClock.getPlaybackParameters().speed, playbackInfo.timeline); + updateLoadControlTrackSelection( + loadingPeriodHolder.getTrackGroups(), loadingPeriodHolder.getTrackSelectorResult()); + if (loadingPeriodHolder == queue.getPlayingPeriod()) { + // This is the first prepared period, so update the position and the renderers. + resetRendererPosition(loadingPeriodHolder.info.startPositionUs); + updatePlayingPeriodRenderers(/* oldPlayingPeriodHolder= */ null); } maybeContinueLoading(); } - private void handleContinueLoadingRequested(MediaPeriod period) { - if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) { + private void handleContinueLoadingRequested(MediaPeriod mediaPeriod) { + if (!queue.isLoading(mediaPeriod)) { // Stale event. return; } + queue.reevaluateBuffer(rendererPositionUs); maybeContinueLoading(); } + private void handlePlaybackParameters( + PlaybackParameters playbackParameters, boolean acknowledgeCommand) + throws ExoPlaybackException { + eventHandler + .obtainMessage( + MSG_PLAYBACK_PARAMETERS_CHANGED, acknowledgeCommand ? 1 : 0, 0, playbackParameters) + .sendToTarget(); + updateTrackSelectionPlaybackSpeed(playbackParameters.speed); + for (Renderer renderer : renderers) { + if (renderer != null) { + renderer.setOperatingRate(playbackParameters.speed); + } + } + } + private void maybeContinueLoading() { - long nextLoadPositionUs = !loadingPeriodHolder.prepared ? 0 - : loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs(); + shouldContinueLoading = shouldContinueLoading(); + if (shouldContinueLoading) { + queue.getLoadingPeriod().continueLoading(rendererPositionUs); + } + updateIsLoading(); + } + + private boolean shouldContinueLoading() { + if (!isLoadingPossible()) { + return false; + } + long bufferedDurationUs = + getTotalBufferedDurationUs(queue.getLoadingPeriod().getNextLoadPositionUs()); + float playbackSpeed = mediaClock.getPlaybackParameters().speed; + return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); + } + + private boolean isLoadingPossible() { + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); + if (loadingPeriodHolder == null) { + return false; + } + long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs(); if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { - setIsLoading(false); - } else { - long loadingPeriodPositionUs = loadingPeriodHolder.toPeriodTime(rendererPositionUs); - long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs; - boolean continueLoading = loadControl.shouldContinueLoading(bufferedDurationUs); - setIsLoading(continueLoading); - if (continueLoading) { - loadingPeriodHolder.needsContinueLoading = false; - loadingPeriodHolder.mediaPeriod.continueLoading(loadingPeriodPositionUs); - } else { - loadingPeriodHolder.needsContinueLoading = true; - } + return false; + } + return true; + } + + private void updateIsLoading() { + MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod(); + boolean isLoading = + shouldContinueLoading || (loadingPeriod != null && loadingPeriod.mediaPeriod.isLoading()); + if (isLoading != playbackInfo.isLoading) { + playbackInfo = playbackInfo.copyWithIsLoading(isLoading); } } - private void releasePeriodHoldersFrom(MediaPeriodHolder periodHolder) { - while (periodHolder != null) { - periodHolder.release(); - periodHolder = periodHolder.next; - } + private PlaybackInfo copyWithNewPosition( + MediaPeriodId mediaPeriodId, long positionUs, long contentPositionUs) { + deliverPendingMessageAtStartPositionRequired = true; + return playbackInfo.copyWithNewPosition( + mediaPeriodId, positionUs, contentPositionUs, getTotalBufferedDurationUs()); } - private void setPlayingPeriodHolder(MediaPeriodHolder periodHolder) throws ExoPlaybackException { - if (playingPeriodHolder == periodHolder) { + @SuppressWarnings("ParameterNotNullable") + private void updatePlayingPeriodRenderers(@Nullable MediaPeriodHolder oldPlayingPeriodHolder) + throws ExoPlaybackException { + MediaPeriodHolder newPlayingPeriodHolder = queue.getPlayingPeriod(); + if (newPlayingPeriodHolder == null || oldPlayingPeriodHolder == newPlayingPeriodHolder) { return; } - int enabledRendererCount = 0; boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED; - TrackSelection newSelection = periodHolder.trackSelectorResult.selections.get(i); - if (newSelection != null) { + if (newPlayingPeriodHolder.getTrackSelectorResult().isRendererEnabled(i)) { enabledRendererCount++; } - if (rendererWasEnabledFlags[i] && (newSelection == null - || (renderer.isCurrentStreamFinal() - && renderer.getStream() == playingPeriodHolder.sampleStreams[i]))) { + if (rendererWasEnabledFlags[i] + && (!newPlayingPeriodHolder.getTrackSelectorResult().isRendererEnabled(i) + || (renderer.isCurrentStreamFinal() + && renderer.getStream() == oldPlayingPeriodHolder.sampleStreams[i]))) { // The renderer should be disabled before playing the next period, either because it's not // needed to play the next period, or because we need to re-enable it as its current stream // is final and it's not reading ahead. - if (renderer == rendererMediaClockSource) { - // Sync standaloneMediaClock so that it can take over timing responsibilities. - standaloneMediaClock.synchronize(rendererMediaClock); - rendererMediaClock = null; - rendererMediaClockSource = null; - } - ensureStopped(renderer); - renderer.disable(); + disableRenderer(renderer); } } - - playingPeriodHolder = periodHolder; - eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult).sendToTarget(); + playbackInfo = + playbackInfo.copyWithTrackInfo( + newPlayingPeriodHolder.getTrackGroups(), + newPlayingPeriodHolder.getTrackSelectorResult()); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } - private void enableRenderers(boolean[] rendererWasEnabledFlags, int enabledRendererCount) + private void enableRenderers(boolean[] rendererWasEnabledFlags, int totalEnabledRendererCount) throws ExoPlaybackException { - enabledRenderers = new Renderer[enabledRendererCount]; - enabledRendererCount = 0; + enabledRenderers = new Renderer[totalEnabledRendererCount]; + int enabledRendererCount = 0; + TrackSelectorResult trackSelectorResult = queue.getPlayingPeriod().getTrackSelectorResult(); + // Reset all disabled renderers before enabling any new ones. This makes sure resources released + // by the disabled renderers will be available to renderers that are being enabled. for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - TrackSelection newSelection = playingPeriodHolder.trackSelectorResult.selections.get(i); - if (newSelection != null) { - enabledRenderers[enabledRendererCount++] = renderer; - if (renderer.getState() == Renderer.STATE_DISABLED) { - RendererConfiguration rendererConfiguration = - playingPeriodHolder.trackSelectorResult.rendererConfigurations[i]; - // The renderer needs enabling with its new track selection. - boolean playing = playWhenReady && state == ExoPlayer.STATE_READY; - // Consider as joining only if the renderer was previously disabled. - boolean joining = !rendererWasEnabledFlags[i] && playing; - // Build an array of formats contained by the selection. - Format[] formats = new Format[newSelection.length()]; - for (int j = 0; j < formats.length; j++) { - formats[j] = newSelection.getFormat(j); - } - // Enable the renderer. - renderer.enable(rendererConfiguration, formats, playingPeriodHolder.sampleStreams[i], - rendererPositionUs, joining, playingPeriodHolder.getRendererOffset()); - MediaClock mediaClock = renderer.getMediaClock(); - if (mediaClock != null) { - if (rendererMediaClock != null) { - throw ExoPlaybackException.createForUnexpected( - new IllegalStateException("Multiple renderer media clocks enabled.")); - } - rendererMediaClock = mediaClock; - rendererMediaClockSource = renderer; - rendererMediaClock.setPlaybackParameters(playbackParameters); - } - // Start the renderer if playing. - if (playing) { - renderer.start(); - } - } + if (!trackSelectorResult.isRendererEnabled(i)) { + renderers[i].reset(); + } + } + // Enable the renderers. + for (int i = 0; i < renderers.length; i++) { + if (trackSelectorResult.isRendererEnabled(i)) { + enableRenderer(i, rendererWasEnabledFlags[i], enabledRendererCount++); } } } - /** - * Holds a {@link MediaPeriod} with information required to play it as part of a timeline. - */ - private static final class MediaPeriodHolder { - - public final MediaPeriod mediaPeriod; - public final Object uid; - public final SampleStream[] sampleStreams; - public final boolean[] mayRetainStreamFlags; - public final long rendererPositionOffsetUs; - - public int index; - public long startPositionUs; - public boolean isLast; - public boolean prepared; - public boolean hasEnabledTracks; - public MediaPeriodHolder next; - public boolean needsContinueLoading; - public TrackSelectorResult trackSelectorResult; - - private final Renderer[] renderers; - private final RendererCapabilities[] rendererCapabilities; - private final TrackSelector trackSelector; - private final LoadControl loadControl; - private final MediaSource mediaSource; - - private TrackSelectorResult periodTrackSelectorResult; - - public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities, - long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl, - MediaSource mediaSource, Object periodUid, int periodIndex, boolean isLastPeriod, - long startPositionUs) { - this.renderers = renderers; - this.rendererCapabilities = rendererCapabilities; - this.rendererPositionOffsetUs = rendererPositionOffsetUs; - this.trackSelector = trackSelector; - this.loadControl = loadControl; - this.mediaSource = mediaSource; - this.uid = Assertions.checkNotNull(periodUid); - this.index = periodIndex; - this.isLast = isLastPeriod; - this.startPositionUs = startPositionUs; - sampleStreams = new SampleStream[renderers.length]; - mayRetainStreamFlags = new boolean[renderers.length]; - mediaPeriod = mediaSource.createPeriod(periodIndex, loadControl.getAllocator(), - startPositionUs); - } - - public long toRendererTime(long periodTimeUs) { - return periodTimeUs + getRendererOffset(); - } - - public long toPeriodTime(long rendererTimeUs) { - return rendererTimeUs - getRendererOffset(); - } - - public long getRendererOffset() { - return rendererPositionOffsetUs - startPositionUs; - } - - public void setIndex(int index, boolean isLast) { - this.index = index; - this.isLast = isLast; - } - - public boolean isFullyBuffered() { - return prepared - && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE); - } - - public void handlePrepared() throws ExoPlaybackException { - prepared = true; - selectTracks(); - startPositionUs = updatePeriodTrackSelection(startPositionUs, false); - } - - public boolean selectTracks() throws ExoPlaybackException { - TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities, - mediaPeriod.getTrackGroups()); - if (selectorResult.isEquivalent(periodTrackSelectorResult)) { - return false; - } - trackSelectorResult = selectorResult; - return true; - } - - public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams) { - return updatePeriodTrackSelection(positionUs, forceRecreateStreams, - new boolean[renderers.length]); - } - - public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams, - boolean[] streamResetFlags) { - TrackSelectionArray trackSelections = trackSelectorResult.selections; - for (int i = 0; i < trackSelections.length; i++) { - mayRetainStreamFlags[i] = !forceRecreateStreams - && trackSelectorResult.isEquivalent(periodTrackSelectorResult, i); - } - - // Disable streams on the period and get new streams for updated/newly-enabled tracks. - positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags, - sampleStreams, streamResetFlags, positionUs); - periodTrackSelectorResult = trackSelectorResult; - - // Update whether we have enabled tracks and sanity check the expected streams are non-null. - hasEnabledTracks = false; - for (int i = 0; i < sampleStreams.length; i++) { - if (sampleStreams[i] != null) { - Assertions.checkState(trackSelections.get(i) != null); - hasEnabledTracks = true; - } else { - Assertions.checkState(trackSelections.get(i) == null); - } - } - - // The track selection has changed. - loadControl.onTracksSelected(renderers, trackSelectorResult.groups, trackSelections); - return positionUs; - } - - public void release() { - try { - mediaSource.releasePeriod(mediaPeriod); - } catch (RuntimeException e) { - // There's nothing we can do. - Log.e(TAG, "Period release failed.", e); + private void enableRenderer( + int rendererIndex, boolean wasRendererEnabled, int enabledRendererIndex) + throws ExoPlaybackException { + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + Renderer renderer = renderers[rendererIndex]; + enabledRenderers[enabledRendererIndex] = renderer; + if (renderer.getState() == Renderer.STATE_DISABLED) { + TrackSelectorResult trackSelectorResult = playingPeriodHolder.getTrackSelectorResult(); + RendererConfiguration rendererConfiguration = + trackSelectorResult.rendererConfigurations[rendererIndex]; + TrackSelection newSelection = trackSelectorResult.selections.get(rendererIndex); + Format[] formats = getFormats(newSelection); + // The renderer needs enabling with its new track selection. + boolean playing = playWhenReady && playbackInfo.playbackState == Player.STATE_READY; + // Consider as joining only if the renderer was previously disabled. + boolean joining = !wasRendererEnabled && playing; + // Enable the renderer. + renderer.enable( + rendererConfiguration, + formats, + playingPeriodHolder.sampleStreams[rendererIndex], + rendererPositionUs, + joining, + playingPeriodHolder.getRendererOffset()); + mediaClock.onRendererEnabled(renderer); + // Start the renderer if playing. + if (playing) { + renderer.start(); } } + } + private void handleLoadingMediaPeriodChanged(boolean loadingTrackSelectionChanged) { + MediaPeriodHolder loadingMediaPeriodHolder = queue.getLoadingPeriod(); + MediaPeriodId loadingMediaPeriodId = + loadingMediaPeriodHolder == null ? playbackInfo.periodId : loadingMediaPeriodHolder.info.id; + boolean loadingMediaPeriodChanged = + !playbackInfo.loadingMediaPeriodId.equals(loadingMediaPeriodId); + if (loadingMediaPeriodChanged) { + playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(loadingMediaPeriodId); + } + playbackInfo.bufferedPositionUs = + loadingMediaPeriodHolder == null + ? playbackInfo.positionUs + : loadingMediaPeriodHolder.getBufferedPositionUs(); + playbackInfo.totalBufferedDurationUs = getTotalBufferedDurationUs(); + if ((loadingMediaPeriodChanged || loadingTrackSelectionChanged) + && loadingMediaPeriodHolder != null + && loadingMediaPeriodHolder.prepared) { + updateLoadControlTrackSelection( + loadingMediaPeriodHolder.getTrackGroups(), + loadingMediaPeriodHolder.getTrackSelectorResult()); + } + } + + private long getTotalBufferedDurationUs() { + return getTotalBufferedDurationUs(playbackInfo.bufferedPositionUs); + } + + private long getTotalBufferedDurationUs(long bufferedPositionInLoadingPeriodUs) { + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); + if (loadingPeriodHolder == null) { + return 0; + } + long totalBufferedDurationUs = + bufferedPositionInLoadingPeriodUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs); + return Math.max(0, totalBufferedDurationUs); + } + + private void updateLoadControlTrackSelection( + TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) { + loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections); + } + + private void sendPlaybackParametersChangedInternal( + PlaybackParameters playbackParameters, boolean acknowledgeCommand) { + handler + .obtainMessage( + MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL, + acknowledgeCommand ? 1 : 0, + 0, + playbackParameters) + .sendToTarget(); + } + + private static Format[] getFormats(TrackSelection newSelection) { + // Build an array of formats contained by the selection. + int length = newSelection != null ? newSelection.length() : 0; + Format[] formats = new Format[length]; + for (int i = 0; i < length; i++) { + formats[i] = newSelection.getFormat(i); + } + return formats; } private static final class SeekPosition { @@ -1567,7 +1958,88 @@ import java.io.IOException; this.windowIndex = windowIndex; this.windowPositionUs = windowPositionUs; } + } + private static final class PendingMessageInfo implements Comparable { + + public final PlayerMessage message; + + public int resolvedPeriodIndex; + public long resolvedPeriodTimeUs; + @Nullable public Object resolvedPeriodUid; + + public PendingMessageInfo(PlayerMessage message) { + this.message = message; + } + + public void setResolvedPosition(int periodIndex, long periodTimeUs, Object periodUid) { + resolvedPeriodIndex = periodIndex; + resolvedPeriodTimeUs = periodTimeUs; + resolvedPeriodUid = periodUid; + } + + @Override + public int compareTo(PendingMessageInfo other) { + if ((resolvedPeriodUid == null) != (other.resolvedPeriodUid == null)) { + // PendingMessageInfos with a resolved period position are always smaller. + return resolvedPeriodUid != null ? -1 : 1; + } + if (resolvedPeriodUid == null) { + // Don't sort message with unresolved positions. + return 0; + } + // Sort resolved media times by period index and then by period position. + int comparePeriodIndex = resolvedPeriodIndex - other.resolvedPeriodIndex; + if (comparePeriodIndex != 0) { + return comparePeriodIndex; + } + return Util.compareLong(resolvedPeriodTimeUs, other.resolvedPeriodTimeUs); + } + } + + private static final class MediaSourceRefreshInfo { + + public final MediaSource source; + public final Timeline timeline; + + public MediaSourceRefreshInfo(MediaSource source, Timeline timeline) { + this.source = source; + this.timeline = timeline; + } + } + + private static final class PlaybackInfoUpdate { + + private PlaybackInfo lastPlaybackInfo; + private int operationAcks; + private boolean positionDiscontinuity; + private @DiscontinuityReason int discontinuityReason; + + public boolean hasPendingUpdate(PlaybackInfo playbackInfo) { + return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity; + } + + public void reset(PlaybackInfo playbackInfo) { + lastPlaybackInfo = playbackInfo; + operationAcks = 0; + positionDiscontinuity = false; + } + + public void incrementPendingOperationAcks(int operationAcks) { + this.operationAcks += operationAcks; + } + + public void setPositionDiscontinuity(@DiscontinuityReason int discontinuityReason) { + if (positionDiscontinuity + && this.discontinuityReason != Player.DISCONTINUITY_REASON_INTERNAL) { + // We always prefer non-internal discontinuity reasons. We also assume that we won't report + // more than one non-internal discontinuity per message iteration. + Assertions.checkArgument(discontinuityReason == Player.DISCONTINUITY_REASON_INTERNAL); + return; + } + positionDiscontinuity = true; + this.discontinuityReason = discontinuityReason; + } } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 8ec75f7e8a1e..545017a21579 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -15,43 +15,72 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2; +import java.util.HashSet; + /** * Information about the ExoPlayer library. */ -public interface ExoPlayerLibraryInfo { +public final class ExoPlayerLibraryInfo { /** - * The version of the library expressed as a string, for example "1.2.3". + * A tag to use when logging library information. */ + public static final String TAG = "ExoPlayer"; + + /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - String VERSION = "2.4.0"; + public static final String VERSION = "2.11.4"; - /** - * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. - */ + /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - String VERSION_SLASHY = "ExoPlayerLib/2.4.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.4"; /** * The version of the library expressed as an integer, for example 1002003. - *

- * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the + * + *

Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - int VERSION_INT = 2004000; + public static final int VERSION_INT = 2011004; /** * Whether the library was compiled with {@link org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions} * checks enabled. */ - boolean ASSERTIONS_ENABLED = true; + public static final boolean ASSERTIONS_ENABLED = true; + + /** Whether an exception should be thrown in case of an OpenGl error. */ + public static final boolean GL_ASSERTIONS_ENABLED = false; /** * Whether the library was compiled with {@link org.mozilla.thirdparty.com.google.android.exoplayer2.util.TraceUtil} * trace enabled. */ - boolean TRACE_ENABLED = true; + public static final boolean TRACE_ENABLED = true; + + private static final HashSet registeredModules = new HashSet<>(); + private static String registeredModulesString = "goog.exo.core"; + + private ExoPlayerLibraryInfo() {} // Prevents instantiation. + + /** + * Returns a string consisting of registered module names separated by ", ". + */ + public static synchronized String registeredModules() { + return registeredModulesString; + } + + /** + * Registers a module to be returned in the {@link #registeredModules()} string. + * + * @param name The name of the module being registered. + */ + public static synchronized void registerModule(String name) { + if (registeredModules.add(name)) { + registeredModulesString = registeredModulesString + ", " + name; + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Format.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Format.java index ad89e4354ae5..9d7518f6f03a 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Format.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Format.java @@ -15,17 +15,16 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.media.MediaFormat; import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaCrypto; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import org.mozilla.thirdparty.com.google.android.exoplayer2.video.ColorInfo; -import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -47,29 +46,27 @@ public final class Format implements Parcelable { */ public static final long OFFSET_SAMPLE_RELATIVE = Long.MAX_VALUE; - /** - * An identifier for the format, or null if unknown or not applicable. - */ - public final String id; + /** An identifier for the format, or null if unknown or not applicable. */ + @Nullable public final String id; + /** The human readable label, or null if unknown or not applicable. */ + @Nullable public final String label; + /** Track selection flags. */ + @C.SelectionFlags public final int selectionFlags; + /** Track role flags. */ + @C.RoleFlags public final int roleFlags; /** * The average bandwidth in bits per second, or {@link #NO_VALUE} if unknown or not applicable. */ public final int bitrate; - /** - * Codecs of the format as described in RFC 6381, or null if unknown or not applicable. - */ - public final String codecs; - /** - * Metadata, or null if unknown or not applicable. - */ - public final Metadata metadata; + /** Codecs of the format as described in RFC 6381, or null if unknown or not applicable. */ + @Nullable public final String codecs; + /** Metadata, or null if unknown or not applicable. */ + @Nullable public final Metadata metadata; // Container specific. - /** - * The mime type of the container, or null if unknown or not applicable. - */ - public final String containerMimeType; + /** The mime type of the container, or null if unknown or not applicable. */ + @Nullable public final String containerMimeType; // Elementary stream specific. @@ -77,7 +74,7 @@ public final class Format implements Parcelable { * The mime type of the elementary stream (i.e. the individual samples), or null if unknown or not * applicable. */ - public final String sampleMimeType; + @Nullable public final String sampleMimeType; /** * The maximum size of a buffer of data (typically one sample), or {@link #NO_VALUE} if unknown or * not applicable. @@ -88,10 +85,15 @@ public final class Format implements Parcelable { * if initialization data is not required. */ public final List initializationData; + /** DRM initialization data if the stream is protected, or null otherwise. */ + @Nullable public final DrmInitData drmInitData; + /** - * DRM initialization data if the stream is protected, or null otherwise. + * For samples that contain subsamples, this is an offset that should be added to subsample + * timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are + * relative to the timestamps of their parent samples. */ - public final DrmInitData drmInitData; + public final long subsampleOffsetUs; // Video specific. @@ -109,14 +111,10 @@ public final class Format implements Parcelable { public final float frameRate; /** * The clockwise rotation that should be applied to the video for it to be rendered in the correct - * orientation, or {@link #NO_VALUE} if unknown or not applicable. Only 0, 90, 180 and 270 are - * supported. + * orientation, or 0 if unknown or not applicable. Only 0, 90, 180 and 270 are supported. */ public final int rotationDegrees; - /** - * The width to height ratio of pixels in the video, or {@link #NO_VALUE} if unknown or not - * applicable. - */ + /** The width to height ratio of pixels in the video, or 1.0 if unknown or not applicable. */ public final float pixelWidthHeightRatio; /** * The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo @@ -125,14 +123,10 @@ public final class Format implements Parcelable { */ @C.StereoMode public final int stereoMode; - /** - * The projection data for 360/VR video, or null if not applicable. - */ - public final byte[] projectionData; - /** - * The color metadata associated with the video, helps with accurate color reproduction. - */ - public final ColorInfo colorInfo; + /** The projection data for 360/VR video, or null if not applicable. */ + @Nullable public final byte[] projectionData; + /** The color metadata associated with the video, helps with accurate color reproduction. */ + @Nullable public final ColorInfo colorInfo; // Audio specific. @@ -144,307 +138,1041 @@ public final class Format implements Parcelable { * The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable. */ public final int sampleRate; + /** The {@link C.PcmEncoding} for PCM audio. Set to {@link #NO_VALUE} for other media types. */ + public final @C.PcmEncoding int pcmEncoding; /** - * The encoding for PCM audio streams. If {@link #sampleMimeType} is {@link MimeTypes#AUDIO_RAW} - * then one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT}, - * {@link C#ENCODING_PCM_24BIT} and {@link C#ENCODING_PCM_32BIT}. Set to {@link #NO_VALUE} for - * other media types. - */ - @C.PcmEncoding - public final int pcmEncoding; - /** - * The number of samples to trim from the start of the decoded audio stream. + * The number of frames to trim from the start of the decoded audio stream, or 0 if not + * applicable. */ public final int encoderDelay; /** - * The number of samples to trim from the end of the decoded audio stream. + * The number of frames to trim from the end of the decoded audio stream, or 0 if not applicable. */ public final int encoderPadding; - // Text specific. - - /** - * For samples that contain subsamples, this is an offset that should be added to subsample - * timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are - * relative to the timestamps of their parent samples. - */ - public final long subsampleOffsetUs; - // Audio and text specific. - /** - * Track selection flags. - */ - @C.SelectionFlags - public final int selectionFlags; - - /** - * The language, or null if unknown or not applicable. - */ - public final String language; - + /** The language as an IETF BCP 47 conformant tag, or null if unknown or not applicable. */ + @Nullable public final String language; /** * The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. */ public final int accessibilityChannel; + // Provided by source. + + /** + * The type of the {@link ExoMediaCrypto} provided by the media source, if the media source can + * acquire a {@link DrmSession} for {@link #drmInitData}. Null if the media source cannot acquire + * a session for {@link #drmInitData}, or if not applicable. + */ + @Nullable public final Class exoMediaCryptoType; + // Lazily initialized hashcode. private int hashCode; // Video. - public static Format createVideoContainerFormat(String id, String containerMimeType, - String sampleMimeType, String codecs, int bitrate, int width, int height, - float frameRate, List initializationData, @C.SelectionFlags int selectionFlags) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width, - height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, selectionFlags, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, - initializationData, null, null); + /** + * @deprecated Use {@link #createVideoContainerFormat(String, String, String, String, String, + * Metadata, int, int, int, float, List, int, int)} instead. + */ + @Deprecated + public static Format createVideoContainerFormat( + @Nullable String id, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int width, + int height, + float frameRate, + @Nullable List initializationData, + @C.SelectionFlags int selectionFlags) { + return createVideoContainerFormat( + id, + /* label= */ null, + containerMimeType, + sampleMimeType, + codecs, + /* metadata= */ null, + bitrate, + width, + height, + frameRate, + initializationData, + selectionFlags, + /* roleFlags= */ 0); } - public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, int maxInputSize, int width, int height, float frameRate, - List initializationData, DrmInitData drmInitData) { - return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width, - height, frameRate, initializationData, NO_VALUE, NO_VALUE, drmInitData); + public static Format createVideoContainerFormat( + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + @Nullable Metadata metadata, + int bitrate, + int width, + int height, + float frameRate, + @Nullable List initializationData, + @C.SelectionFlags int selectionFlags, + @C.RoleFlags int roleFlags) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + /* maxInputSize= */ NO_VALUE, + initializationData, + /* drmInitData= */ null, + OFFSET_SAMPLE_RELATIVE, + width, + height, + frameRate, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + /* channelCount= */ NO_VALUE, + /* sampleRate= */ NO_VALUE, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + /* language= */ null, + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } - public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, int maxInputSize, int width, int height, float frameRate, - List initializationData, int rotationDegrees, float pixelWidthHeightRatio, - DrmInitData drmInitData) { - return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width, - height, frameRate, initializationData, rotationDegrees, pixelWidthHeightRatio, null, - NO_VALUE, null, drmInitData); + public static Format createVideoSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int width, + int height, + float frameRate, + @Nullable List initializationData, + @Nullable DrmInitData drmInitData) { + return createVideoSampleFormat( + id, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + width, + height, + frameRate, + initializationData, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + drmInitData); } - public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, int maxInputSize, int width, int height, float frameRate, - List initializationData, int rotationDegrees, float pixelWidthHeightRatio, - byte[] projectionData, @C.StereoMode int stereoMode, ColorInfo colorInfo, - DrmInitData drmInitData) { - return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height, - frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, - colorInfo, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE, - OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, null); + public static Format createVideoSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int width, + int height, + float frameRate, + @Nullable List initializationData, + int rotationDegrees, + float pixelWidthHeightRatio, + @Nullable DrmInitData drmInitData) { + return createVideoSampleFormat( + id, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + width, + height, + frameRate, + initializationData, + rotationDegrees, + pixelWidthHeightRatio, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + drmInitData); + } + + public static Format createVideoSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int width, + int height, + float frameRate, + @Nullable List initializationData, + int rotationDegrees, + float pixelWidthHeightRatio, + @Nullable byte[] projectionData, + @C.StereoMode int stereoMode, + @Nullable ColorInfo colorInfo, + @Nullable DrmInitData drmInitData) { + return new Format( + id, + /* label= */ null, + /* selectionFlags= */ 0, + /* roleFlags= */ 0, + bitrate, + codecs, + /* metadata= */ null, + /* containerMimeType= */ null, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + OFFSET_SAMPLE_RELATIVE, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + /* channelCount= */ NO_VALUE, + /* sampleRate= */ NO_VALUE, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + /* language= */ null, + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Audio. - public static Format createAudioContainerFormat(String id, String containerMimeType, - String sampleMimeType, String codecs, int bitrate, int channelCount, int sampleRate, - List initializationData, @C.SelectionFlags int selectionFlags, String language) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate, - NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, - initializationData, null, null); + /** + * @deprecated Use {@link #createAudioContainerFormat(String, String, String, String, String, + * Metadata, int, int, int, List, int, int, String)} instead. + */ + @Deprecated + public static Format createAudioContainerFormat( + @Nullable String id, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int channelCount, + int sampleRate, + @Nullable List initializationData, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { + return createAudioContainerFormat( + id, + /* label= */ null, + containerMimeType, + sampleMimeType, + codecs, + /* metadata= */ null, + bitrate, + channelCount, + sampleRate, + initializationData, + selectionFlags, + /* roleFlags= */ 0, + language); } - public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, int maxInputSize, int channelCount, int sampleRate, - List initializationData, DrmInitData drmInitData, - @C.SelectionFlags int selectionFlags, String language) { - return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount, - sampleRate, NO_VALUE, initializationData, drmInitData, selectionFlags, language); + public static Format createAudioContainerFormat( + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + @Nullable Metadata metadata, + int bitrate, + int channelCount, + int sampleRate, + @Nullable List initializationData, + @C.SelectionFlags int selectionFlags, + @C.RoleFlags int roleFlags, + @Nullable String language) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + /* maxInputSize= */ NO_VALUE, + initializationData, + /* drmInitData= */ null, + OFFSET_SAMPLE_RELATIVE, + /* width= */ NO_VALUE, + /* height= */ NO_VALUE, + /* frameRate= */ NO_VALUE, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + channelCount, + sampleRate, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + language, + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } - public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, int maxInputSize, int channelCount, int sampleRate, - @C.PcmEncoding int pcmEncoding, List initializationData, DrmInitData drmInitData, - @C.SelectionFlags int selectionFlags, String language) { - return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount, - sampleRate, pcmEncoding, NO_VALUE, NO_VALUE, initializationData, drmInitData, - selectionFlags, language, null); + public static Format createAudioSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int channelCount, + int sampleRate, + @Nullable List initializationData, + @Nullable DrmInitData drmInitData, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { + return createAudioSampleFormat( + id, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + channelCount, + sampleRate, + /* pcmEncoding= */ NO_VALUE, + initializationData, + drmInitData, + selectionFlags, + language); } - public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, int maxInputSize, int channelCount, int sampleRate, - @C.PcmEncoding int pcmEncoding, int encoderDelay, int encoderPadding, - List initializationData, DrmInitData drmInitData, - @C.SelectionFlags int selectionFlags, String language, Metadata metadata) { - return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate, pcmEncoding, - encoderDelay, encoderPadding, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, - initializationData, drmInitData, metadata); + public static Format createAudioSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int channelCount, + int sampleRate, + @C.PcmEncoding int pcmEncoding, + @Nullable List initializationData, + @Nullable DrmInitData drmInitData, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { + return createAudioSampleFormat( + id, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + channelCount, + sampleRate, + pcmEncoding, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + initializationData, + drmInitData, + selectionFlags, + language, + /* metadata= */ null); + } + + public static Format createAudioSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + int maxInputSize, + int channelCount, + int sampleRate, + @C.PcmEncoding int pcmEncoding, + int encoderDelay, + int encoderPadding, + @Nullable List initializationData, + @Nullable DrmInitData drmInitData, + @C.SelectionFlags int selectionFlags, + @Nullable String language, + @Nullable Metadata metadata) { + return new Format( + id, + /* label= */ null, + selectionFlags, + /* roleFlags= */ 0, + bitrate, + codecs, + metadata, + /* containerMimeType= */ null, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + OFFSET_SAMPLE_RELATIVE, + /* width= */ NO_VALUE, + /* height= */ NO_VALUE, + /* frameRate= */ NO_VALUE, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Text. - public static Format createTextContainerFormat(String id, String containerMimeType, - String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, - String language) { - return createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate, - selectionFlags, language, NO_VALUE); + public static Format createTextContainerFormat( + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @C.RoleFlags int roleFlags, + @Nullable String language) { + return createTextContainerFormat( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + selectionFlags, + roleFlags, + language, + /* accessibilityChannel= */ NO_VALUE); } - public static Format createTextContainerFormat(String id, String containerMimeType, - String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, - String language, int accessibilityChannel) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, accessibilityChannel, - OFFSET_SAMPLE_RELATIVE, null, null, null); + public static Format createTextContainerFormat( + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @C.RoleFlags int roleFlags, + @Nullable String language, + int accessibilityChannel) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + /* metadata= */ null, + containerMimeType, + sampleMimeType, + /* maxInputSize= */ NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null, + OFFSET_SAMPLE_RELATIVE, + /* width= */ NO_VALUE, + /* height= */ NO_VALUE, + /* frameRate= */ NO_VALUE, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + /* channelCount= */ NO_VALUE, + /* sampleRate= */ NO_VALUE, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + language, + accessibilityChannel, + /* exoMediaCryptoType= */ null); } - public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData) { - return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, - NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.emptyList()); + public static Format createTextSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { + return createTextSampleFormat(id, sampleMimeType, selectionFlags, language, null); } - public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel, - DrmInitData drmInitData) { - return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, - accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.emptyList()); + public static Format createTextSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @C.SelectionFlags int selectionFlags, + @Nullable String language, + @Nullable DrmInitData drmInitData) { + return createTextSampleFormat( + id, + sampleMimeType, + /* codecs= */ null, + /* bitrate= */ NO_VALUE, + selectionFlags, + language, + NO_VALUE, + drmInitData, + OFFSET_SAMPLE_RELATIVE, + Collections.emptyList()); } - public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData, + public static Format createTextSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @Nullable String language, + int accessibilityChannel, + @Nullable DrmInitData drmInitData) { + return createTextSampleFormat( + id, + sampleMimeType, + codecs, + bitrate, + selectionFlags, + language, + accessibilityChannel, + drmInitData, + OFFSET_SAMPLE_RELATIVE, + Collections.emptyList()); + } + + public static Format createTextSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @Nullable String language, + @Nullable DrmInitData drmInitData, long subsampleOffsetUs) { - return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, - NO_VALUE, drmInitData, subsampleOffsetUs, Collections.emptyList()); + return createTextSampleFormat( + id, + sampleMimeType, + codecs, + bitrate, + selectionFlags, + language, + /* accessibilityChannel= */ NO_VALUE, + drmInitData, + subsampleOffsetUs, + Collections.emptyList()); } - public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, @C.SelectionFlags int selectionFlags, String language, - int accessibilityChannel, DrmInitData drmInitData, long subsampleOffsetUs, - List initializationData) { - return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, - initializationData, drmInitData, null); + public static Format createTextSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @Nullable String language, + int accessibilityChannel, + @Nullable DrmInitData drmInitData, + long subsampleOffsetUs, + @Nullable List initializationData) { + return new Format( + id, + /* label= */ null, + selectionFlags, + /* roleFlags= */ 0, + bitrate, + codecs, + /* metadata= */ null, + /* containerMimeType= */ null, + sampleMimeType, + /* maxInputSize= */ NO_VALUE, + initializationData, + drmInitData, + subsampleOffsetUs, + /* width= */ NO_VALUE, + /* height= */ NO_VALUE, + /* frameRate= */ NO_VALUE, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + /* channelCount= */ NO_VALUE, + /* sampleRate= */ NO_VALUE, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + language, + accessibilityChannel, + /* exoMediaCryptoType= */ null); } // Image. - public static Format createImageSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, List initializationData, String language, DrmInitData drmInitData) { - return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, 0, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, - null); + public static Format createImageSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @Nullable List initializationData, + @Nullable String language, + @Nullable DrmInitData drmInitData) { + return new Format( + id, + /* label= */ null, + selectionFlags, + /* roleFlags= */ 0, + bitrate, + codecs, + /* metadata=*/ null, + /* containerMimeType= */ null, + sampleMimeType, + /* maxInputSize= */ NO_VALUE, + initializationData, + drmInitData, + OFFSET_SAMPLE_RELATIVE, + /* width= */ NO_VALUE, + /* height= */ NO_VALUE, + /* frameRate= */ NO_VALUE, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + /* channelCount= */ NO_VALUE, + /* sampleRate= */ NO_VALUE, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + language, + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Generic. - public static Format createContainerFormat(String id, String containerMimeType, - String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags, - String language) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null, - null); + /** + * @deprecated Use {@link #createContainerFormat(String, String, String, String, String, int, int, + * int, String)} instead. + */ + @Deprecated + public static Format createContainerFormat( + @Nullable String id, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { + return createContainerFormat( + id, + /* label= */ null, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + selectionFlags, + /* roleFlags= */ 0, + language); } - public static Format createSampleFormat(String id, String sampleMimeType, - long subsampleOffsetUs) { - return new Format(id, null, sampleMimeType, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, 0, null, NO_VALUE, subsampleOffsetUs, null, null, null); + public static Format createContainerFormat( + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + @C.RoleFlags int roleFlags, + @Nullable String language) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + /* metadata= */ null, + containerMimeType, + sampleMimeType, + /* maxInputSize= */ NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null, + OFFSET_SAMPLE_RELATIVE, + /* width= */ NO_VALUE, + /* height= */ NO_VALUE, + /* frameRate= */ NO_VALUE, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + /* channelCount= */ NO_VALUE, + /* sampleRate= */ NO_VALUE, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + language, + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } - public static Format createSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, DrmInitData drmInitData) { - return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, drmInitData, null); + public static Format createSampleFormat( + @Nullable String id, @Nullable String sampleMimeType, long subsampleOffsetUs) { + return new Format( + id, + /* label= */ null, + /* selectionFlags= */ 0, + /* roleFlags= */ 0, + /* bitrate= */ NO_VALUE, + /* codecs= */ null, + /* metadata= */ null, + /* containerMimeType= */ null, + sampleMimeType, + /* maxInputSize= */ NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null, + subsampleOffsetUs, + /* width= */ NO_VALUE, + /* height= */ NO_VALUE, + /* frameRate= */ NO_VALUE, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + /* channelCount= */ NO_VALUE, + /* sampleRate= */ NO_VALUE, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + /* language= */ null, + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } - /* package */ Format(String id, String containerMimeType, String sampleMimeType, String codecs, - int bitrate, int maxInputSize, int width, int height, float frameRate, int rotationDegrees, - float pixelWidthHeightRatio, byte[] projectionData, @C.StereoMode int stereoMode, - ColorInfo colorInfo, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, - int encoderDelay, int encoderPadding, @C.SelectionFlags int selectionFlags, String language, - int accessibilityChannel, long subsampleOffsetUs, List initializationData, - DrmInitData drmInitData, Metadata metadata) { + public static Format createSampleFormat( + @Nullable String id, + @Nullable String sampleMimeType, + @Nullable String codecs, + int bitrate, + @Nullable DrmInitData drmInitData) { + return new Format( + id, + /* label= */ null, + /* selectionFlags= */ 0, + /* roleFlags= */ 0, + bitrate, + codecs, + /* metadata= */ null, + /* containerMimeType= */ null, + sampleMimeType, + /* maxInputSize= */ NO_VALUE, + /* initializationData= */ null, + drmInitData, + OFFSET_SAMPLE_RELATIVE, + /* width= */ NO_VALUE, + /* height= */ NO_VALUE, + /* frameRate= */ NO_VALUE, + /* rotationDegrees= */ NO_VALUE, + /* pixelWidthHeightRatio= */ NO_VALUE, + /* projectionData= */ null, + /* stereoMode= */ NO_VALUE, + /* colorInfo= */ null, + /* channelCount= */ NO_VALUE, + /* sampleRate= */ NO_VALUE, + /* pcmEncoding= */ NO_VALUE, + /* encoderDelay= */ NO_VALUE, + /* encoderPadding= */ NO_VALUE, + /* language= */ null, + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); + } + + /* package */ Format( + @Nullable String id, + @Nullable String label, + @C.SelectionFlags int selectionFlags, + @C.RoleFlags int roleFlags, + int bitrate, + @Nullable String codecs, + @Nullable Metadata metadata, + // Container specific. + @Nullable String containerMimeType, + // Elementary stream specific. + @Nullable String sampleMimeType, + int maxInputSize, + @Nullable List initializationData, + @Nullable DrmInitData drmInitData, + long subsampleOffsetUs, + // Video specific. + int width, + int height, + float frameRate, + int rotationDegrees, + float pixelWidthHeightRatio, + @Nullable byte[] projectionData, + @C.StereoMode int stereoMode, + @Nullable ColorInfo colorInfo, + // Audio specific. + int channelCount, + int sampleRate, + @C.PcmEncoding int pcmEncoding, + int encoderDelay, + int encoderPadding, + // Audio and text specific. + @Nullable String language, + int accessibilityChannel, + // Provided by source. + @Nullable Class exoMediaCryptoType) { this.id = id; - this.containerMimeType = containerMimeType; - this.sampleMimeType = sampleMimeType; - this.codecs = codecs; + this.label = label; + this.selectionFlags = selectionFlags; + this.roleFlags = roleFlags; this.bitrate = bitrate; + this.codecs = codecs; + this.metadata = metadata; + // Container specific. + this.containerMimeType = containerMimeType; + // Elementary stream specific. + this.sampleMimeType = sampleMimeType; this.maxInputSize = maxInputSize; + this.initializationData = + initializationData == null ? Collections.emptyList() : initializationData; + this.drmInitData = drmInitData; + this.subsampleOffsetUs = subsampleOffsetUs; + // Video specific. this.width = width; this.height = height; this.frameRate = frameRate; - this.rotationDegrees = rotationDegrees; - this.pixelWidthHeightRatio = pixelWidthHeightRatio; + this.rotationDegrees = rotationDegrees == Format.NO_VALUE ? 0 : rotationDegrees; + this.pixelWidthHeightRatio = + pixelWidthHeightRatio == Format.NO_VALUE ? 1 : pixelWidthHeightRatio; this.projectionData = projectionData; this.stereoMode = stereoMode; this.colorInfo = colorInfo; + // Audio specific. this.channelCount = channelCount; this.sampleRate = sampleRate; this.pcmEncoding = pcmEncoding; - this.encoderDelay = encoderDelay; - this.encoderPadding = encoderPadding; - this.selectionFlags = selectionFlags; - this.language = language; + this.encoderDelay = encoderDelay == Format.NO_VALUE ? 0 : encoderDelay; + this.encoderPadding = encoderPadding == Format.NO_VALUE ? 0 : encoderPadding; + // Audio and text specific. + this.language = Util.normalizeLanguageCode(language); this.accessibilityChannel = accessibilityChannel; - this.subsampleOffsetUs = subsampleOffsetUs; - this.initializationData = initializationData == null ? Collections.emptyList() - : initializationData; - this.drmInitData = drmInitData; - this.metadata = metadata; + // Provided by source. + this.exoMediaCryptoType = exoMediaCryptoType; } @SuppressWarnings("ResourceType") /* package */ Format(Parcel in) { id = in.readString(); - containerMimeType = in.readString(); - sampleMimeType = in.readString(); - codecs = in.readString(); - bitrate = in.readInt(); - maxInputSize = in.readInt(); - width = in.readInt(); - height = in.readInt(); - frameRate = in.readFloat(); - rotationDegrees = in.readInt(); - pixelWidthHeightRatio = in.readFloat(); - boolean hasProjectionData = in.readInt() != 0; - projectionData = hasProjectionData ? in.createByteArray() : null; - stereoMode = in.readInt(); - colorInfo = in.readParcelable(ColorInfo.class.getClassLoader()); - channelCount = in.readInt(); - sampleRate = in.readInt(); - pcmEncoding = in.readInt(); - encoderDelay = in.readInt(); - encoderPadding = in.readInt(); + label = in.readString(); selectionFlags = in.readInt(); - language = in.readString(); - accessibilityChannel = in.readInt(); - subsampleOffsetUs = in.readLong(); + roleFlags = in.readInt(); + bitrate = in.readInt(); + codecs = in.readString(); + metadata = in.readParcelable(Metadata.class.getClassLoader()); + // Container specific. + containerMimeType = in.readString(); + // Elementary stream specific. + sampleMimeType = in.readString(); + maxInputSize = in.readInt(); int initializationDataSize = in.readInt(); initializationData = new ArrayList<>(initializationDataSize); for (int i = 0; i < initializationDataSize; i++) { initializationData.add(in.createByteArray()); } drmInitData = in.readParcelable(DrmInitData.class.getClassLoader()); - metadata = in.readParcelable(Metadata.class.getClassLoader()); + subsampleOffsetUs = in.readLong(); + // Video specific. + width = in.readInt(); + height = in.readInt(); + frameRate = in.readFloat(); + rotationDegrees = in.readInt(); + pixelWidthHeightRatio = in.readFloat(); + boolean hasProjectionData = Util.readBoolean(in); + projectionData = hasProjectionData ? in.createByteArray() : null; + stereoMode = in.readInt(); + colorInfo = in.readParcelable(ColorInfo.class.getClassLoader()); + // Audio specific. + channelCount = in.readInt(); + sampleRate = in.readInt(); + pcmEncoding = in.readInt(); + encoderDelay = in.readInt(); + encoderPadding = in.readInt(); + // Audio and text specific. + language = in.readString(); + accessibilityChannel = in.readInt(); + // Provided by source. + exoMediaCryptoType = null; } public Format copyWithMaxInputSize(int maxInputSize) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, - width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, - encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, - initializationData, drmInitData, metadata); + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, - width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, - encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, - initializationData, drmInitData, metadata); + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); } - public Format copyWithContainerInfo(String id, String codecs, int bitrate, int width, int height, - @C.SelectionFlags int selectionFlags, String language) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, - width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, - encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, - initializationData, drmInitData, metadata); + public Format copyWithLabel(@Nullable String label) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); + } + + public Format copyWithContainerInfo( + @Nullable String id, + @Nullable String label, + @Nullable String sampleMimeType, + @Nullable String codecs, + @Nullable Metadata metadata, + int bitrate, + int width, + int height, + int channelCount, + @C.SelectionFlags int selectionFlags, + @Nullable String language) { + + if (this.metadata != null) { + metadata = this.metadata.copyWithAppendedEntriesFrom(metadata); + } + + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); } @SuppressWarnings("ReferenceEquality") @@ -453,43 +1181,323 @@ public final class Format implements Parcelable { // No need to copy from ourselves. return this; } + + int trackType = MimeTypes.getTrackType(sampleMimeType); + + // Use manifest value only. String id = manifestFormat.id; - String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs; + + // Prefer manifest values, but fill in from sample format if missing. + String label = manifestFormat.label != null ? manifestFormat.label : this.label; + String language = this.language; + if ((trackType == C.TRACK_TYPE_TEXT || trackType == C.TRACK_TYPE_AUDIO) + && manifestFormat.language != null) { + language = manifestFormat.language; + } + + // Prefer sample format values, but fill in from manifest if missing. int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate; - float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate; - @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags; - String language = this.language == null ? manifestFormat.language : this.language; - DrmInitData drmInitData = manifestFormat.drmInitData != null ? manifestFormat.drmInitData - : this.drmInitData; - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, - height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, - colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, - selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, - drmInitData, metadata); + String codecs = this.codecs; + if (codecs == null) { + // The manifest format may be muxed, so filter only codecs of this format's type. If we still + // have more than one codec then we're unable to uniquely identify which codec to fill in. + String codecsOfType = Util.getCodecsOfType(manifestFormat.codecs, trackType); + if (Util.splitCodecs(codecsOfType).length == 1) { + codecs = codecsOfType; + } + } + + Metadata metadata = + this.metadata == null + ? manifestFormat.metadata + : this.metadata.copyWithAppendedEntriesFrom(manifestFormat.metadata); + + float frameRate = this.frameRate; + if (frameRate == NO_VALUE && trackType == C.TRACK_TYPE_VIDEO) { + frameRate = manifestFormat.frameRate; + } + + // Merge manifest and sample format values. + @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags; + @C.RoleFlags int roleFlags = this.roleFlags | manifestFormat.roleFlags; + DrmInitData drmInitData = + DrmInitData.createSessionCreationData(manifestFormat.drmInitData, this.drmInitData); + + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, - width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, - encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, - initializationData, drmInitData, metadata); + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); } - public Format copyWithDrmInitData(DrmInitData drmInitData) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, - width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, - encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, - initializationData, drmInitData, metadata); + public Format copyWithFrameRate(float frameRate) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); } - public Format copyWithMetadata(Metadata metadata) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, - width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, - encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, - initializationData, drmInitData, metadata); + public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) { + return copyWithAdjustments(drmInitData, metadata); + } + + public Format copyWithMetadata(@Nullable Metadata metadata) { + return copyWithAdjustments(drmInitData, metadata); + } + + @SuppressWarnings("ReferenceEquality") + public Format copyWithAdjustments( + @Nullable DrmInitData drmInitData, @Nullable Metadata metadata) { + if (drmInitData == this.drmInitData && metadata == this.metadata) { + return this; + } + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); + } + + public Format copyWithRotationDegrees(int rotationDegrees) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); + } + + public Format copyWithBitrate(int bitrate) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); + } + + public Format copyWithVideoSize(int width, int height) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); + } + + public Format copyWithExoMediaCryptoType( + @Nullable Class exoMediaCryptoType) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); } /** @@ -500,62 +1508,83 @@ public final class Format implements Parcelable { return width == NO_VALUE || height == NO_VALUE ? NO_VALUE : (width * height); } - /** - * Returns a {@link MediaFormat} representation of this format. - */ - @SuppressLint("InlinedApi") - @TargetApi(16) - public final MediaFormat getFrameworkMediaFormatV16() { - MediaFormat format = new MediaFormat(); - format.setString(MediaFormat.KEY_MIME, sampleMimeType); - maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language); - maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize); - maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width); - maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height); - maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate); - maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees); - maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount); - maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate); - maybeSetIntegerV16(format, "encoder-delay", encoderDelay); - maybeSetIntegerV16(format, "encoder-padding", encoderPadding); - for (int i = 0; i < initializationData.size(); i++) { - format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i))); - } - maybeSetColorInfoV24(format, colorInfo); - return format; - } - @Override public String toString() { - return "Format(" + id + ", " + containerMimeType + ", " + sampleMimeType + ", " + bitrate + ", " - + language + ", [" + width + ", " + height + ", " + frameRate + "]" - + ", [" + channelCount + ", " + sampleRate + "])"; + return "Format(" + + id + + ", " + + label + + ", " + + containerMimeType + + ", " + + sampleMimeType + + ", " + + codecs + + ", " + + bitrate + + ", " + + language + + ", [" + + width + + ", " + + height + + ", " + + frameRate + + "]" + + ", [" + + channelCount + + ", " + + sampleRate + + "])"; } @Override public int hashCode() { if (hashCode == 0) { + // Some fields for which hashing is expensive are deliberately omitted. int result = 17; result = 31 * result + (id == null ? 0 : id.hashCode()); - result = 31 * result + (containerMimeType == null ? 0 : containerMimeType.hashCode()); - result = 31 * result + (sampleMimeType == null ? 0 : sampleMimeType.hashCode()); - result = 31 * result + (codecs == null ? 0 : codecs.hashCode()); + result = 31 * result + (label != null ? label.hashCode() : 0); + result = 31 * result + selectionFlags; + result = 31 * result + roleFlags; result = 31 * result + bitrate; + result = 31 * result + (codecs == null ? 0 : codecs.hashCode()); + result = 31 * result + (metadata == null ? 0 : metadata.hashCode()); + // Container specific. + result = 31 * result + (containerMimeType == null ? 0 : containerMimeType.hashCode()); + // Elementary stream specific. + result = 31 * result + (sampleMimeType == null ? 0 : sampleMimeType.hashCode()); + result = 31 * result + maxInputSize; + // [Omitted] initializationData. + // [Omitted] drmInitData. + result = 31 * result + (int) subsampleOffsetUs; + // Video specific. result = 31 * result + width; result = 31 * result + height; + result = 31 * result + Float.floatToIntBits(frameRate); + result = 31 * result + rotationDegrees; + result = 31 * result + Float.floatToIntBits(pixelWidthHeightRatio); + // [Omitted] projectionData. + result = 31 * result + stereoMode; + // [Omitted] colorInfo. + // Audio specific. result = 31 * result + channelCount; result = 31 * result + sampleRate; + result = 31 * result + pcmEncoding; + result = 31 * result + encoderDelay; + result = 31 * result + encoderPadding; + // Audio and text specific. result = 31 * result + (language == null ? 0 : language.hashCode()); result = 31 * result + accessibilityChannel; - result = 31 * result + (drmInitData == null ? 0 : drmInitData.hashCode()); - result = 31 * result + (metadata == null ? 0 : metadata.hashCode()); + // Provided by source. + result = 31 * result + (exoMediaCryptoType == null ? 0 : exoMediaCryptoType.hashCode()); hashCode = result; } return hashCode; } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -563,24 +1592,51 @@ public final class Format implements Parcelable { return false; } Format other = (Format) obj; - if (bitrate != other.bitrate || maxInputSize != other.maxInputSize - || width != other.width || height != other.height || frameRate != other.frameRate - || rotationDegrees != other.rotationDegrees - || pixelWidthHeightRatio != other.pixelWidthHeightRatio || stereoMode != other.stereoMode - || channelCount != other.channelCount || sampleRate != other.sampleRate - || pcmEncoding != other.pcmEncoding || encoderDelay != other.encoderDelay - || encoderPadding != other.encoderPadding || subsampleOffsetUs != other.subsampleOffsetUs - || selectionFlags != other.selectionFlags || !Util.areEqual(id, other.id) - || !Util.areEqual(language, other.language) - || accessibilityChannel != other.accessibilityChannel - || !Util.areEqual(containerMimeType, other.containerMimeType) - || !Util.areEqual(sampleMimeType, other.sampleMimeType) - || !Util.areEqual(codecs, other.codecs) - || !Util.areEqual(drmInitData, other.drmInitData) - || !Util.areEqual(metadata, other.metadata) - || !Util.areEqual(colorInfo, other.colorInfo) - || !Arrays.equals(projectionData, other.projectionData) - || initializationData.size() != other.initializationData.size()) { + if (hashCode != 0 && other.hashCode != 0 && hashCode != other.hashCode) { + return false; + } + // Field equality checks ordered by type, with the cheapest checks first. + return selectionFlags == other.selectionFlags + && roleFlags == other.roleFlags + && bitrate == other.bitrate + && maxInputSize == other.maxInputSize + && subsampleOffsetUs == other.subsampleOffsetUs + && width == other.width + && height == other.height + && rotationDegrees == other.rotationDegrees + && stereoMode == other.stereoMode + && channelCount == other.channelCount + && sampleRate == other.sampleRate + && pcmEncoding == other.pcmEncoding + && encoderDelay == other.encoderDelay + && encoderPadding == other.encoderPadding + && accessibilityChannel == other.accessibilityChannel + && Float.compare(frameRate, other.frameRate) == 0 + && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0 + && Util.areEqual(exoMediaCryptoType, other.exoMediaCryptoType) + && Util.areEqual(id, other.id) + && Util.areEqual(label, other.label) + && Util.areEqual(codecs, other.codecs) + && Util.areEqual(containerMimeType, other.containerMimeType) + && Util.areEqual(sampleMimeType, other.sampleMimeType) + && Util.areEqual(language, other.language) + && Arrays.equals(projectionData, other.projectionData) + && Util.areEqual(metadata, other.metadata) + && Util.areEqual(colorInfo, other.colorInfo) + && Util.areEqual(drmInitData, other.drmInitData) + && initializationDataEquals(other); + } + + /** + * Returns whether the {@link #initializationData}s belonging to this format and {@code other} are + * equal. + * + * @param other The other format whose {@link #initializationData} is being compared. + * @return Whether the {@link #initializationData}s belonging to this format and {@code other} are + * equal. + */ + public boolean initializationDataEquals(Format other) { + if (initializationData.size() != other.initializationData.size()) { return false; } for (int i = 0; i < initializationData.size(); i++) { @@ -591,51 +1647,10 @@ public final class Format implements Parcelable { return true; } - @TargetApi(24) - private static void maybeSetColorInfoV24(MediaFormat format, ColorInfo colorInfo) { - if (colorInfo == null) { - return; - } - maybeSetIntegerV16(format, "color-transfer", colorInfo.colorTransfer); - maybeSetIntegerV16(format, "color-standard", colorInfo.colorSpace); - maybeSetIntegerV16(format, "color-range", colorInfo.colorRange); - maybeSetByteBufferV16(format, "hdr-static-info", colorInfo.hdrStaticInfo); - } - - @TargetApi(16) - private static void maybeSetStringV16(MediaFormat format, String key, String value) { - if (value != null) { - format.setString(key, value); - } - } - - @TargetApi(16) - private static void maybeSetIntegerV16(MediaFormat format, String key, int value) { - if (value != NO_VALUE) { - format.setInteger(key, value); - } - } - - @TargetApi(16) - private static void maybeSetFloatV16(MediaFormat format, String key, float value) { - if (value != NO_VALUE) { - format.setFloat(key, value); - } - } - - @TargetApi(16) - private static void maybeSetByteBufferV16(MediaFormat format, String key, byte[] value) { - if (value != null) { - format.setByteBuffer(key, ByteBuffer.wrap(value)); - } - } - // Utility methods - /** - * Returns a prettier {@link String} than {@link #toString()}, intended for logging. - */ - public static String toLogString(Format format) { + /** Returns a prettier {@link String} than {@link #toString()}, intended for logging. */ + public static String toLogString(@Nullable Format format) { if (format == null) { return "null"; } @@ -644,6 +1659,9 @@ public final class Format implements Parcelable { if (format.bitrate != Format.NO_VALUE) { builder.append(", bitrate=").append(format.bitrate); } + if (format.codecs != null) { + builder.append(", codecs=").append(format.codecs); + } if (format.width != Format.NO_VALUE && format.height != Format.NO_VALUE) { builder.append(", res=").append(format.width).append("x").append(format.height); } @@ -659,6 +1677,9 @@ public final class Format implements Parcelable { if (format.language != null) { builder.append(", language=").append(format.language); } + if (format.label != null) { + builder.append(", label=").append(format.label); + } return builder.toString(); } @@ -672,38 +1693,45 @@ public final class Format implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(id); - dest.writeString(containerMimeType); - dest.writeString(sampleMimeType); - dest.writeString(codecs); - dest.writeInt(bitrate); - dest.writeInt(maxInputSize); - dest.writeInt(width); - dest.writeInt(height); - dest.writeFloat(frameRate); - dest.writeInt(rotationDegrees); - dest.writeFloat(pixelWidthHeightRatio); - dest.writeInt(projectionData != null ? 1 : 0); - if (projectionData != null) { - dest.writeByteArray(projectionData); - } - dest.writeInt(stereoMode); - dest.writeParcelable(colorInfo, flags); - dest.writeInt(channelCount); - dest.writeInt(sampleRate); - dest.writeInt(pcmEncoding); - dest.writeInt(encoderDelay); - dest.writeInt(encoderPadding); + dest.writeString(label); dest.writeInt(selectionFlags); - dest.writeString(language); - dest.writeInt(accessibilityChannel); - dest.writeLong(subsampleOffsetUs); + dest.writeInt(roleFlags); + dest.writeInt(bitrate); + dest.writeString(codecs); + dest.writeParcelable(metadata, 0); + // Container specific. + dest.writeString(containerMimeType); + // Elementary stream specific. + dest.writeString(sampleMimeType); + dest.writeInt(maxInputSize); int initializationDataSize = initializationData.size(); dest.writeInt(initializationDataSize); for (int i = 0; i < initializationDataSize; i++) { dest.writeByteArray(initializationData.get(i)); } dest.writeParcelable(drmInitData, 0); - dest.writeParcelable(metadata, 0); + dest.writeLong(subsampleOffsetUs); + // Video specific. + dest.writeInt(width); + dest.writeInt(height); + dest.writeFloat(frameRate); + dest.writeInt(rotationDegrees); + dest.writeFloat(pixelWidthHeightRatio); + Util.writeBoolean(dest, projectionData != null); + if (projectionData != null) { + dest.writeByteArray(projectionData); + } + dest.writeInt(stereoMode); + dest.writeParcelable(colorInfo, flags); + // Audio specific. + dest.writeInt(channelCount); + dest.writeInt(sampleRate); + dest.writeInt(pcmEncoding); + dest.writeInt(encoderDelay); + dest.writeInt(encoderPadding); + // Audio and text specific. + dest.writeString(language); + dest.writeInt(accessibilityChannel); } public static final Creator CREATOR = new Creator() { @@ -719,5 +1747,4 @@ public final class Format implements Parcelable { } }; - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/FormatHolder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/FormatHolder.java index 0c753588ab80..35e87f12710a 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/FormatHolder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/FormatHolder.java @@ -15,14 +15,29 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession; + /** * Holds a {@link Format}. */ public final class FormatHolder { - /** - * The held {@link Format}. - */ - public Format format; + /** Whether the {@link #format} setter also sets the {@link #drmSession} field. */ + // TODO: Remove once all Renderers and MediaSources have migrated to the new DRM model [Internal + // ref: b/129764794]. + public boolean includesDrmSession; + /** An accompanying context for decrypting samples in the format. */ + @Nullable public DrmSession drmSession; + + /** The held {@link Format}. */ + @Nullable public Format format; + + /** Clears the holder. */ + public void clear() { + includesDrmSession = false; + drmSession = null; + format = null; + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/LoadControl.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/LoadControl.java index eafb52a7a6b0..5076018d6552 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/LoadControl.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/LoadControl.java @@ -56,23 +56,58 @@ public interface LoadControl { Allocator getAllocator(); /** - * Called by the player to determine whether sufficient media is buffered for playback to be - * started or resumed. + * Returns the duration of media to retain in the buffer prior to the current playback position, + * for fast backward seeking. + *

+ * Note: If {@link #retainBackBufferFromKeyframe()} is false then seeking in the back-buffer will + * only be fast if the back-buffer contains a keyframe prior to the seek position. + *

+ * Note: Implementations should return a single value. Dynamic changes to the back-buffer are not + * currently supported. * - * @param bufferedDurationUs The duration of media that's currently buffered. - * @param rebuffering Whether the player is rebuffering. A rebuffer is defined to be caused by - * buffer depletion rather than a user action. Hence this parameter is false during initial - * buffering and when buffering as a result of a seek operation. - * @return Whether playback should be allowed to start or resume. + * @return The duration of media to retain in the buffer prior to the current playback position, + * in microseconds. */ - boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering); + long getBackBufferDurationUs(); + + /** + * Returns whether media should be retained from the keyframe before the current playback position + * minus {@link #getBackBufferDurationUs()}, rather than any sample before or at that position. + *

+ * Warning: Returning true will cause the back-buffer size to depend on the spacing of keyframes + * in the media being played. Returning true is not recommended unless you control the media and + * are comfortable with the back-buffer size exceeding {@link #getBackBufferDurationUs()} by as + * much as the maximum duration between adjacent keyframes in the media. + *

+ * Note: Implementations should return a single value. Dynamic changes to the back-buffer are not + * currently supported. + * + * @return Whether media should be retained from the keyframe before the current playback position + * minus {@link #getBackBufferDurationUs()}, rather than any sample before or at that position. + */ + boolean retainBackBufferFromKeyframe(); /** * Called by the player to determine whether it should continue to load the source. * * @param bufferedDurationUs The duration of media that's currently buffered. + * @param playbackSpeed The current playback speed. * @return Whether the loading should continue. */ - boolean shouldContinueLoading(long bufferedDurationUs); + boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed); + /** + * Called repeatedly by the player when it's loading the source, has yet to start playback, and + * has the minimum amount of data necessary for playback to be started. The value returned + * determines whether playback is actually started. The load control may opt to return {@code + * false} until some condition has been met (e.g. a certain amount of media is buffered). + * + * @param bufferedDurationUs The duration of media that's currently buffered. + * @param playbackSpeed The current playback speed. + * @param rebuffering Whether the player is rebuffering. A rebuffer is defined to be caused by + * buffer depletion rather than a user action. Hence this parameter is false during initial + * buffering and when buffering as a result of a seek operation. + * @return Whether playback should be allowed to start or resume. + */ + boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, boolean rebuffering); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodHolder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodHolder.java new file mode 100644 index 000000000000..66cb9a1fce80 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodHolder.java @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ClippingMediaPeriod; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.EmptySampleStream; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaPeriod; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleStream; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelector; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectorResult; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** Holds a {@link MediaPeriod} with information required to play it as part of a timeline. */ +/* package */ final class MediaPeriodHolder { + + private static final String TAG = "MediaPeriodHolder"; + + /** The {@link MediaPeriod} wrapped by this class. */ + public final MediaPeriod mediaPeriod; + /** The unique timeline period identifier the media period belongs to. */ + public final Object uid; + /** + * The sample streams for each renderer associated with this period. May contain null elements. + */ + public final @NullableType SampleStream[] sampleStreams; + + /** Whether the media period has finished preparing. */ + public boolean prepared; + /** Whether any of the tracks of this media period are enabled. */ + public boolean hasEnabledTracks; + /** {@link MediaPeriodInfo} about this media period. */ + public MediaPeriodInfo info; + + private final boolean[] mayRetainStreamFlags; + private final RendererCapabilities[] rendererCapabilities; + private final TrackSelector trackSelector; + private final MediaSource mediaSource; + + @Nullable private MediaPeriodHolder next; + private TrackGroupArray trackGroups; + private TrackSelectorResult trackSelectorResult; + private long rendererPositionOffsetUs; + + /** + * Creates a new holder with information required to play it as part of a timeline. + * + * @param rendererCapabilities The renderer capabilities. + * @param rendererPositionOffsetUs The renderer time of the start of the period, in microseconds. + * @param trackSelector The track selector. + * @param allocator The allocator. + * @param mediaSource The media source that produced the media period. + * @param info Information used to identify this media period in its timeline period. + * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each + * renderer. + */ + public MediaPeriodHolder( + RendererCapabilities[] rendererCapabilities, + long rendererPositionOffsetUs, + TrackSelector trackSelector, + Allocator allocator, + MediaSource mediaSource, + MediaPeriodInfo info, + TrackSelectorResult emptyTrackSelectorResult) { + this.rendererCapabilities = rendererCapabilities; + this.rendererPositionOffsetUs = rendererPositionOffsetUs; + this.trackSelector = trackSelector; + this.mediaSource = mediaSource; + this.uid = info.id.periodUid; + this.info = info; + this.trackGroups = TrackGroupArray.EMPTY; + this.trackSelectorResult = emptyTrackSelectorResult; + sampleStreams = new SampleStream[rendererCapabilities.length]; + mayRetainStreamFlags = new boolean[rendererCapabilities.length]; + mediaPeriod = + createMediaPeriod( + info.id, mediaSource, allocator, info.startPositionUs, info.endPositionUs); + } + + /** + * Converts time relative to the start of the period to the respective renderer time using {@link + * #getRendererOffset()}, in microseconds. + */ + public long toRendererTime(long periodTimeUs) { + return periodTimeUs + getRendererOffset(); + } + + /** + * Converts renderer time to the respective time relative to the start of the period using {@link + * #getRendererOffset()}, in microseconds. + */ + public long toPeriodTime(long rendererTimeUs) { + return rendererTimeUs - getRendererOffset(); + } + + /** Returns the renderer time of the start of the period, in microseconds. */ + public long getRendererOffset() { + return rendererPositionOffsetUs; + } + + /** + * Sets the renderer time of the start of the period, in microseconds. + * + * @param rendererPositionOffsetUs The new renderer position offset, in microseconds. + */ + public void setRendererOffset(long rendererPositionOffsetUs) { + this.rendererPositionOffsetUs = rendererPositionOffsetUs; + } + + /** Returns start position of period in renderer time. */ + public long getStartPositionRendererTime() { + return info.startPositionUs + rendererPositionOffsetUs; + } + + /** Returns whether the period is fully buffered. */ + public boolean isFullyBuffered() { + return prepared + && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE); + } + + /** + * Returns the buffered position in microseconds. If the period is buffered to the end, then the + * period duration is returned. + * + * @return The buffered position in microseconds. + */ + public long getBufferedPositionUs() { + if (!prepared) { + return info.startPositionUs; + } + long bufferedPositionUs = + hasEnabledTracks ? mediaPeriod.getBufferedPositionUs() : C.TIME_END_OF_SOURCE; + return bufferedPositionUs == C.TIME_END_OF_SOURCE ? info.durationUs : bufferedPositionUs; + } + + /** + * Returns the next load time relative to the start of the period, or {@link C#TIME_END_OF_SOURCE} + * if loading has finished. + */ + public long getNextLoadPositionUs() { + return !prepared ? 0 : mediaPeriod.getNextLoadPositionUs(); + } + + /** + * Handles period preparation. + * + * @param playbackSpeed The current playback speed. + * @param timeline The current {@link Timeline}. + * @throws ExoPlaybackException If an error occurs during track selection. + */ + public void handlePrepared(float playbackSpeed, Timeline timeline) throws ExoPlaybackException { + prepared = true; + trackGroups = mediaPeriod.getTrackGroups(); + TrackSelectorResult selectorResult = selectTracks(playbackSpeed, timeline); + long newStartPositionUs = + applyTrackSelection( + selectorResult, info.startPositionUs, /* forceRecreateStreams= */ false); + rendererPositionOffsetUs += info.startPositionUs - newStartPositionUs; + info = info.copyWithStartPositionUs(newStartPositionUs); + } + + /** + * Reevaluates the buffer of the media period at the given renderer position. Should only be + * called if this is the loading media period. + * + * @param rendererPositionUs The playing position in renderer time, in microseconds. + */ + public void reevaluateBuffer(long rendererPositionUs) { + Assertions.checkState(isLoadingMediaPeriod()); + if (prepared) { + mediaPeriod.reevaluateBuffer(toPeriodTime(rendererPositionUs)); + } + } + + /** + * Continues loading the media period at the given renderer position. Should only be called if + * this is the loading media period. + * + * @param rendererPositionUs The load position in renderer time, in microseconds. + */ + public void continueLoading(long rendererPositionUs) { + Assertions.checkState(isLoadingMediaPeriod()); + long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs); + mediaPeriod.continueLoading(loadingPeriodPositionUs); + } + + /** + * Selects tracks for the period. Must only be called if {@link #prepared} is {@code true}. + * + *

The new track selection needs to be applied with {@link + * #applyTrackSelection(TrackSelectorResult, long, boolean)} before taking effect. + * + * @param playbackSpeed The current playback speed. + * @param timeline The current {@link Timeline}. + * @return The {@link TrackSelectorResult}. + * @throws ExoPlaybackException If an error occurs during track selection. + */ + public TrackSelectorResult selectTracks(float playbackSpeed, Timeline timeline) + throws ExoPlaybackException { + TrackSelectorResult selectorResult = + trackSelector.selectTracks(rendererCapabilities, getTrackGroups(), info.id, timeline); + for (TrackSelection trackSelection : selectorResult.selections.getAll()) { + if (trackSelection != null) { + trackSelection.onPlaybackSpeed(playbackSpeed); + } + } + return selectorResult; + } + + /** + * Applies a {@link TrackSelectorResult} to the period. + * + * @param trackSelectorResult The {@link TrackSelectorResult} to apply. + * @param positionUs The position relative to the start of the period at which to apply the new + * track selections, in microseconds. + * @param forceRecreateStreams Whether all streams are forced to be recreated. + * @return The actual position relative to the start of the period at which the new track + * selections are applied. + */ + public long applyTrackSelection( + TrackSelectorResult trackSelectorResult, long positionUs, boolean forceRecreateStreams) { + return applyTrackSelection( + trackSelectorResult, + positionUs, + forceRecreateStreams, + new boolean[rendererCapabilities.length]); + } + + /** + * Applies a {@link TrackSelectorResult} to the period. + * + * @param newTrackSelectorResult The {@link TrackSelectorResult} to apply. + * @param positionUs The position relative to the start of the period at which to apply the new + * track selections, in microseconds. + * @param forceRecreateStreams Whether all streams are forced to be recreated. + * @param streamResetFlags Will be populated to indicate which streams have been reset or were + * newly created. + * @return The actual position relative to the start of the period at which the new track + * selections are applied. + */ + public long applyTrackSelection( + TrackSelectorResult newTrackSelectorResult, + long positionUs, + boolean forceRecreateStreams, + boolean[] streamResetFlags) { + for (int i = 0; i < newTrackSelectorResult.length; i++) { + mayRetainStreamFlags[i] = + !forceRecreateStreams && newTrackSelectorResult.isEquivalent(trackSelectorResult, i); + } + + // Undo the effect of previous call to associate no-sample renderers with empty tracks + // so the mediaPeriod receives back whatever it sent us before. + disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams); + disableTrackSelectionsInResult(); + trackSelectorResult = newTrackSelectorResult; + enableTrackSelectionsInResult(); + // Disable streams on the period and get new streams for updated/newly-enabled tracks. + TrackSelectionArray trackSelections = newTrackSelectorResult.selections; + positionUs = + mediaPeriod.selectTracks( + trackSelections.getAll(), + mayRetainStreamFlags, + sampleStreams, + streamResetFlags, + positionUs); + associateNoSampleRenderersWithEmptySampleStream(sampleStreams); + + // Update whether we have enabled tracks and sanity check the expected streams are non-null. + hasEnabledTracks = false; + for (int i = 0; i < sampleStreams.length; i++) { + if (sampleStreams[i] != null) { + Assertions.checkState(newTrackSelectorResult.isRendererEnabled(i)); + // hasEnabledTracks should be true only when non-empty streams exists. + if (rendererCapabilities[i].getTrackType() != C.TRACK_TYPE_NONE) { + hasEnabledTracks = true; + } + } else { + Assertions.checkState(trackSelections.get(i) == null); + } + } + return positionUs; + } + + /** Releases the media period. No other method should be called after the release. */ + public void release() { + disableTrackSelectionsInResult(); + releaseMediaPeriod(info.endPositionUs, mediaSource, mediaPeriod); + } + + /** + * Sets the next media period holder in the queue. + * + * @param nextMediaPeriodHolder The next holder, or null if this will be the new loading media + * period holder at the end of the queue. + */ + public void setNext(@Nullable MediaPeriodHolder nextMediaPeriodHolder) { + if (nextMediaPeriodHolder == next) { + return; + } + disableTrackSelectionsInResult(); + next = nextMediaPeriodHolder; + enableTrackSelectionsInResult(); + } + + /** + * Returns the next media period holder in the queue, or null if this is the last media period + * (and thus the loading media period). + */ + @Nullable + public MediaPeriodHolder getNext() { + return next; + } + + /** Returns the {@link TrackGroupArray} exposed by this media period. */ + public TrackGroupArray getTrackGroups() { + return trackGroups; + } + + /** Returns the {@link TrackSelectorResult} which is currently applied. */ + public TrackSelectorResult getTrackSelectorResult() { + return trackSelectorResult; + } + + private void enableTrackSelectionsInResult() { + if (!isLoadingMediaPeriod()) { + return; + } + for (int i = 0; i < trackSelectorResult.length; i++) { + boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i); + TrackSelection trackSelection = trackSelectorResult.selections.get(i); + if (rendererEnabled && trackSelection != null) { + trackSelection.enable(); + } + } + } + + private void disableTrackSelectionsInResult() { + if (!isLoadingMediaPeriod()) { + return; + } + for (int i = 0; i < trackSelectorResult.length; i++) { + boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i); + TrackSelection trackSelection = trackSelectorResult.selections.get(i); + if (rendererEnabled && trackSelection != null) { + trackSelection.disable(); + } + } + } + + /** + * For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the dummy {@link + * EmptySampleStream} that was associated with it. + */ + private void disassociateNoSampleRenderersWithEmptySampleStream( + @NullableType SampleStream[] sampleStreams) { + for (int i = 0; i < rendererCapabilities.length; i++) { + if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE) { + sampleStreams[i] = null; + } + } + } + + /** + * For each renderer of type {@link C#TRACK_TYPE_NONE} that was enabled, we will associate it with + * a dummy {@link EmptySampleStream}. + */ + private void associateNoSampleRenderersWithEmptySampleStream( + @NullableType SampleStream[] sampleStreams) { + for (int i = 0; i < rendererCapabilities.length; i++) { + if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE + && trackSelectorResult.isRendererEnabled(i)) { + sampleStreams[i] = new EmptySampleStream(); + } + } + } + + private boolean isLoadingMediaPeriod() { + return next == null; + } + + /** Returns a media period corresponding to the given {@code id}. */ + private static MediaPeriod createMediaPeriod( + MediaPeriodId id, + MediaSource mediaSource, + Allocator allocator, + long startPositionUs, + long endPositionUs) { + MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs); + if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { + mediaPeriod = + new ClippingMediaPeriod( + mediaPeriod, /* enableInitialDiscontinuity= */ true, /* startUs= */ 0, endPositionUs); + } + return mediaPeriod; + } + + /** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */ + private static void releaseMediaPeriod( + long endPositionUs, MediaSource mediaSource, MediaPeriod mediaPeriod) { + try { + if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { + mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); + } else { + mediaSource.releasePeriod(mediaPeriod); + } + } catch (RuntimeException e) { + // There's nothing we can do. + Log.e(TAG, "Period release failed.", e); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodInfo.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodInfo.java new file mode 100644 index 000000000000..b240fe0f91e5 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodInfo.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaPeriod; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** Stores the information required to load and play a {@link MediaPeriod}. */ +/* package */ final class MediaPeriodInfo { + + /** The media period's identifier. */ + public final MediaPeriodId id; + /** The start position of the media to play within the media period, in microseconds. */ + public final long startPositionUs; + /** + * If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET} + * if this is not an ad or the next content media period should be played from its default + * position. + */ + public final long contentPositionUs; + /** + * The end position to which the media period's content is clipped in order to play a following ad + * group, in microseconds, or {@link C#TIME_UNSET} if there is no following ad group or if this + * media period is an ad. The value {@link C#TIME_END_OF_SOURCE} indicates that a postroll ad + * follows at the end of this content media period. + */ + public final long endPositionUs; + /** + * The duration of the media period, like {@link #endPositionUs} but with {@link + * C#TIME_END_OF_SOURCE} and {@link C#TIME_UNSET} resolved to the timeline period duration if + * known. + */ + public final long durationUs; + /** + * Whether this is the last media period in its timeline period (e.g., a postroll ad, or a media + * period corresponding to a timeline period without ads). + */ + public final boolean isLastInTimelinePeriod; + /** + * Whether this is the last media period in the entire timeline. If true, {@link + * #isLastInTimelinePeriod} will also be true. + */ + public final boolean isFinal; + + MediaPeriodInfo( + MediaPeriodId id, + long startPositionUs, + long contentPositionUs, + long endPositionUs, + long durationUs, + boolean isLastInTimelinePeriod, + boolean isFinal) { + this.id = id; + this.startPositionUs = startPositionUs; + this.contentPositionUs = contentPositionUs; + this.endPositionUs = endPositionUs; + this.durationUs = durationUs; + this.isLastInTimelinePeriod = isLastInTimelinePeriod; + this.isFinal = isFinal; + } + + /** + * Returns a copy of this instance with the start position set to the specified value. May return + * the same instance if nothing changed. + */ + public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) { + return startPositionUs == this.startPositionUs + ? this + : new MediaPeriodInfo( + id, + startPositionUs, + contentPositionUs, + endPositionUs, + durationUs, + isLastInTimelinePeriod, + isFinal); + } + + /** + * Returns a copy of this instance with the content position set to the specified value. May + * return the same instance if nothing changed. + */ + public MediaPeriodInfo copyWithContentPositionUs(long contentPositionUs) { + return contentPositionUs == this.contentPositionUs + ? this + : new MediaPeriodInfo( + id, + startPositionUs, + contentPositionUs, + endPositionUs, + durationUs, + isLastInTimelinePeriod, + isFinal); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MediaPeriodInfo that = (MediaPeriodInfo) o; + return startPositionUs == that.startPositionUs + && contentPositionUs == that.contentPositionUs + && endPositionUs == that.endPositionUs + && durationUs == that.durationUs + && isLastInTimelinePeriod == that.isLastInTimelinePeriod + && isFinal == that.isFinal + && Util.areEqual(id, that.id); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + id.hashCode(); + result = 31 * result + (int) startPositionUs; + result = 31 * result + (int) contentPositionUs; + result = 31 * result + (int) endPositionUs; + result = 31 * result + (int) durationUs; + result = 31 * result + (isLastInTimelinePeriod ? 1 : 0); + result = 31 * result + (isFinal ? 1 : 0); + return result; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodQueue.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodQueue.java new file mode 100644 index 000000000000..941fb6184862 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodQueue.java @@ -0,0 +1,743 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import android.util.Pair; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.RepeatMode; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaPeriod; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelector; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectorResult; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; + +/** + * Holds a queue of media periods, from the currently playing media period at the front to the + * loading media period at the end of the queue, with methods for controlling loading and updating + * the queue. Also has a reference to the media period currently being read. + */ +/* package */ final class MediaPeriodQueue { + + /** + * Limits the maximum number of periods to buffer ahead of the current playing period. The + * buffering policy normally prevents buffering too far ahead, but the policy could allow too many + * small periods to be buffered if the period count were not limited. + */ + private static final int MAXIMUM_BUFFER_AHEAD_PERIODS = 100; + + private final Timeline.Period period; + private final Timeline.Window window; + + private long nextWindowSequenceNumber; + private Timeline timeline; + private @RepeatMode int repeatMode; + private boolean shuffleModeEnabled; + @Nullable private MediaPeriodHolder playing; + @Nullable private MediaPeriodHolder reading; + @Nullable private MediaPeriodHolder loading; + private int length; + @Nullable private Object oldFrontPeriodUid; + private long oldFrontPeriodWindowSequenceNumber; + + /** Creates a new media period queue. */ + public MediaPeriodQueue() { + period = new Timeline.Period(); + window = new Timeline.Window(); + timeline = Timeline.EMPTY; + } + + /** + * Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(long, long)} to update the queued + * media periods to take into account the new timeline. + */ + public void setTimeline(Timeline timeline) { + this.timeline = timeline; + } + + /** + * Sets the {@link RepeatMode} and returns whether the repeat mode change has been fully handled. + * If not, it is necessary to seek to the current playback position. + */ + public boolean updateRepeatMode(@RepeatMode int repeatMode) { + this.repeatMode = repeatMode; + return updateForPlaybackModeChange(); + } + + /** + * Sets whether shuffling is enabled and returns whether the shuffle mode change has been fully + * handled. If not, it is necessary to seek to the current playback position. + */ + public boolean updateShuffleModeEnabled(boolean shuffleModeEnabled) { + this.shuffleModeEnabled = shuffleModeEnabled; + return updateForPlaybackModeChange(); + } + + /** Returns whether {@code mediaPeriod} is the current loading media period. */ + public boolean isLoading(MediaPeriod mediaPeriod) { + return loading != null && loading.mediaPeriod == mediaPeriod; + } + + /** + * If there is a loading period, reevaluates its buffer. + * + * @param rendererPositionUs The current renderer position. + */ + public void reevaluateBuffer(long rendererPositionUs) { + if (loading != null) { + loading.reevaluateBuffer(rendererPositionUs); + } + } + + /** Returns whether a new loading media period should be enqueued, if available. */ + public boolean shouldLoadNextMediaPeriod() { + return loading == null + || (!loading.info.isFinal + && loading.isFullyBuffered() + && loading.info.durationUs != C.TIME_UNSET + && length < MAXIMUM_BUFFER_AHEAD_PERIODS); + } + + /** + * Returns the {@link MediaPeriodInfo} for the next media period to load. + * + * @param rendererPositionUs The current renderer position. + * @param playbackInfo The current playback information. + * @return The {@link MediaPeriodInfo} for the next media period to load, or {@code null} if not + * yet known. + */ + public @Nullable MediaPeriodInfo getNextMediaPeriodInfo( + long rendererPositionUs, PlaybackInfo playbackInfo) { + return loading == null + ? getFirstMediaPeriodInfo(playbackInfo) + : getFollowingMediaPeriodInfo(loading, rendererPositionUs); + } + + /** + * Enqueues a new media period holder based on the specified information as the new loading media + * period, and returns it. + * + * @param rendererCapabilities The renderer capabilities. + * @param trackSelector The track selector. + * @param allocator The allocator. + * @param mediaSource The media source that produced the media period. + * @param info Information used to identify this media period in its timeline period. + * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each + * renderer. + */ + public MediaPeriodHolder enqueueNextMediaPeriodHolder( + RendererCapabilities[] rendererCapabilities, + TrackSelector trackSelector, + Allocator allocator, + MediaSource mediaSource, + MediaPeriodInfo info, + TrackSelectorResult emptyTrackSelectorResult) { + long rendererPositionOffsetUs = + loading == null + ? (info.id.isAd() && info.contentPositionUs != C.TIME_UNSET + ? info.contentPositionUs + : 0) + : (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs); + MediaPeriodHolder newPeriodHolder = + new MediaPeriodHolder( + rendererCapabilities, + rendererPositionOffsetUs, + trackSelector, + allocator, + mediaSource, + info, + emptyTrackSelectorResult); + if (loading != null) { + loading.setNext(newPeriodHolder); + } else { + playing = newPeriodHolder; + reading = newPeriodHolder; + } + oldFrontPeriodUid = null; + loading = newPeriodHolder; + length++; + return newPeriodHolder; + } + + /** + * Returns the loading period holder which is at the end of the queue, or null if the queue is + * empty. + */ + @Nullable + public MediaPeriodHolder getLoadingPeriod() { + return loading; + } + + /** + * Returns the playing period holder which is at the front of the queue, or null if the queue is + * empty. + */ + @Nullable + public MediaPeriodHolder getPlayingPeriod() { + return playing; + } + + /** Returns the reading period holder, or null if the queue is empty. */ + @Nullable + public MediaPeriodHolder getReadingPeriod() { + return reading; + } + + /** + * Continues reading from the next period holder in the queue. + * + * @return The updated reading period holder. + */ + public MediaPeriodHolder advanceReadingPeriod() { + Assertions.checkState(reading != null && reading.getNext() != null); + reading = reading.getNext(); + return reading; + } + + /** + * Dequeues the playing period holder from the front of the queue and advances the playing period + * holder to be the next item in the queue. + * + * @return The updated playing period holder, or null if the queue is or becomes empty. + */ + @Nullable + public MediaPeriodHolder advancePlayingPeriod() { + if (playing == null) { + return null; + } + if (playing == reading) { + reading = playing.getNext(); + } + playing.release(); + length--; + if (length == 0) { + loading = null; + oldFrontPeriodUid = playing.uid; + oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber; + } + playing = playing.getNext(); + return playing; + } + + /** + * Removes all period holders after the given period holder. This process may also remove the + * currently reading period holder. If that is the case, the reading period holder is set to be + * the same as the playing period holder at the front of the queue. + * + * @param mediaPeriodHolder The media period holder that shall be the new end of the queue. + * @return Whether the reading period has been removed. + */ + public boolean removeAfter(MediaPeriodHolder mediaPeriodHolder) { + Assertions.checkState(mediaPeriodHolder != null); + boolean removedReading = false; + loading = mediaPeriodHolder; + while (mediaPeriodHolder.getNext() != null) { + mediaPeriodHolder = mediaPeriodHolder.getNext(); + if (mediaPeriodHolder == reading) { + reading = playing; + removedReading = true; + } + mediaPeriodHolder.release(); + length--; + } + loading.setNext(null); + return removedReading; + } + + /** + * Clears the queue. + * + * @param keepFrontPeriodUid Whether the queue should keep the id of the media period in the front + * of queue (typically the playing one) for later reuse. + */ + public void clear(boolean keepFrontPeriodUid) { + MediaPeriodHolder front = playing; + if (front != null) { + oldFrontPeriodUid = keepFrontPeriodUid ? front.uid : null; + oldFrontPeriodWindowSequenceNumber = front.info.id.windowSequenceNumber; + removeAfter(front); + front.release(); + } else if (!keepFrontPeriodUid) { + oldFrontPeriodUid = null; + } + playing = null; + loading = null; + reading = null; + length = 0; + } + + /** + * Updates media periods in the queue to take into account the latest timeline, and returns + * whether the timeline change has been fully handled. If not, it is necessary to seek to the + * current playback position. The method assumes that the first media period in the queue is still + * consistent with the new timeline. + * + * @param rendererPositionUs The current renderer position in microseconds. + * @param maxRendererReadPositionUs The maximum renderer position up to which renderers have read + * the current reading media period in microseconds, or {@link C#TIME_END_OF_SOURCE} if they + * have read to the end. + * @return Whether the timeline change has been handled completely. + */ + public boolean updateQueuedPeriods(long rendererPositionUs, long maxRendererReadPositionUs) { + // TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline + // is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be + // handled here. + MediaPeriodHolder previousPeriodHolder = null; + MediaPeriodHolder periodHolder = playing; + while (periodHolder != null) { + MediaPeriodInfo oldPeriodInfo = periodHolder.info; + + // Get period info based on new timeline. + MediaPeriodInfo newPeriodInfo; + if (previousPeriodHolder == null) { + // The id and start position of the first period have already been verified by + // ExoPlayerImplInternal.handleSourceInfoRefreshed. Just update duration, isLastInTimeline + // and isLastInPeriod flags. + newPeriodInfo = getUpdatedMediaPeriodInfo(oldPeriodInfo); + } else { + newPeriodInfo = getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs); + if (newPeriodInfo == null) { + // We've loaded a next media period that is not in the new timeline. + return !removeAfter(previousPeriodHolder); + } + if (!canKeepMediaPeriodHolder(oldPeriodInfo, newPeriodInfo)) { + // The new media period has a different id or start position. + return !removeAfter(previousPeriodHolder); + } + } + + // Use new period info, but keep old content position. + periodHolder.info = newPeriodInfo.copyWithContentPositionUs(oldPeriodInfo.contentPositionUs); + + if (!areDurationsCompatible(oldPeriodInfo.durationUs, newPeriodInfo.durationUs)) { + // The period duration changed. Remove all subsequent periods and check whether we read + // beyond the new duration. + long newDurationInRendererTime = + newPeriodInfo.durationUs == C.TIME_UNSET + ? Long.MAX_VALUE + : periodHolder.toRendererTime(newPeriodInfo.durationUs); + boolean isReadingAndReadBeyondNewDuration = + periodHolder == reading + && (maxRendererReadPositionUs == C.TIME_END_OF_SOURCE + || maxRendererReadPositionUs >= newDurationInRendererTime); + boolean readingPeriodRemoved = removeAfter(periodHolder); + return !readingPeriodRemoved && !isReadingAndReadBeyondNewDuration; + } + + previousPeriodHolder = periodHolder; + periodHolder = periodHolder.getNext(); + } + return true; + } + + /** + * Returns new media period info based on specified {@code mediaPeriodInfo} but taking into + * account the current timeline. This method must only be called if the period is still part of + * the current timeline. + * + * @param info Media period info for a media period based on an old timeline. + * @return The updated media period info for the current timeline. + */ + public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info) { + MediaPeriodId id = info.id; + boolean isLastInPeriod = isLastInPeriod(id); + boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); + timeline.getPeriodByUid(info.id.periodUid, period); + long durationUs = + id.isAd() + ? period.getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup) + : (info.endPositionUs == C.TIME_UNSET || info.endPositionUs == C.TIME_END_OF_SOURCE + ? period.getDurationUs() + : info.endPositionUs); + return new MediaPeriodInfo( + id, + info.startPositionUs, + info.contentPositionUs, + info.endPositionUs, + durationUs, + isLastInPeriod, + isLastInTimeline); + } + + /** + * Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be + * played, returning an identifier for an ad group if one needs to be played before the specified + * position, or an identifier for a content media period if not. + * + * @param periodUid The uid of the timeline period to play. + * @param positionUs The next content position in the period to play. + * @return The identifier for the first media period to play, taking into account unplayed ads. + */ + public MediaPeriodId resolveMediaPeriodIdForAds(Object periodUid, long positionUs) { + long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(periodUid); + return resolveMediaPeriodIdForAds(periodUid, positionUs, windowSequenceNumber); + } + + // Internal methods. + + /** + * Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be + * played, returning an identifier for an ad group if one needs to be played before the specified + * position, or an identifier for a content media period if not. + * + * @param periodUid The uid of the timeline period to play. + * @param positionUs The next content position in the period to play. + * @param windowSequenceNumber The sequence number of the window in the buffered sequence of + * windows this period is part of. + * @return The identifier for the first media period to play, taking into account unplayed ads. + */ + private MediaPeriodId resolveMediaPeriodIdForAds( + Object periodUid, long positionUs, long windowSequenceNumber) { + timeline.getPeriodByUid(periodUid, period); + int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs); + if (adGroupIndex == C.INDEX_UNSET) { + int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs); + return new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex); + } else { + int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex); + return new MediaPeriodId(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber); + } + } + + /** + * Resolves the specified period uid to a corresponding window sequence number. Either by reusing + * the window sequence number of an existing matching media period or by creating a new window + * sequence number. + * + * @param periodUid The uid of the timeline period. + * @return A window sequence number for a media period created for this timeline period. + */ + private long resolvePeriodIndexToWindowSequenceNumber(Object periodUid) { + int windowIndex = timeline.getPeriodByUid(periodUid, period).windowIndex; + if (oldFrontPeriodUid != null) { + int oldFrontPeriodIndex = timeline.getIndexOfPeriod(oldFrontPeriodUid); + if (oldFrontPeriodIndex != C.INDEX_UNSET) { + int oldFrontWindowIndex = timeline.getPeriod(oldFrontPeriodIndex, period).windowIndex; + if (oldFrontWindowIndex == windowIndex) { + // Try to match old front uid after the queue has been cleared. + return oldFrontPeriodWindowSequenceNumber; + } + } + } + MediaPeriodHolder mediaPeriodHolder = playing; + while (mediaPeriodHolder != null) { + if (mediaPeriodHolder.uid.equals(periodUid)) { + // Reuse window sequence number of first exact period match. + return mediaPeriodHolder.info.id.windowSequenceNumber; + } + mediaPeriodHolder = mediaPeriodHolder.getNext(); + } + mediaPeriodHolder = playing; + while (mediaPeriodHolder != null) { + int indexOfHolderInTimeline = timeline.getIndexOfPeriod(mediaPeriodHolder.uid); + if (indexOfHolderInTimeline != C.INDEX_UNSET) { + int holderWindowIndex = timeline.getPeriod(indexOfHolderInTimeline, period).windowIndex; + if (holderWindowIndex == windowIndex) { + // As an alternative, try to match other periods of the same window. + return mediaPeriodHolder.info.id.windowSequenceNumber; + } + } + mediaPeriodHolder = mediaPeriodHolder.getNext(); + } + // If no match is found, create new sequence number. + long windowSequenceNumber = nextWindowSequenceNumber++; + if (playing == null) { + // If the queue is empty, save it as old front uid to allow later reuse. + oldFrontPeriodUid = periodUid; + oldFrontPeriodWindowSequenceNumber = windowSequenceNumber; + } + return windowSequenceNumber; + } + + /** + * Returns whether a period described by {@code oldInfo} can be kept for playing the media period + * described by {@code newInfo}. + */ + private boolean canKeepMediaPeriodHolder(MediaPeriodInfo oldInfo, MediaPeriodInfo newInfo) { + return oldInfo.startPositionUs == newInfo.startPositionUs && oldInfo.id.equals(newInfo.id); + } + + /** + * Returns whether a duration change of a period is compatible with keeping the following periods. + */ + private boolean areDurationsCompatible(long previousDurationUs, long newDurationUs) { + return previousDurationUs == C.TIME_UNSET || previousDurationUs == newDurationUs; + } + + /** + * Updates the queue for any playback mode change, and returns whether the change was fully + * handled. If not, it is necessary to seek to the current playback position. + */ + private boolean updateForPlaybackModeChange() { + // Find the last existing period holder that matches the new period order. + MediaPeriodHolder lastValidPeriodHolder = playing; + if (lastValidPeriodHolder == null) { + return true; + } + int currentPeriodIndex = timeline.getIndexOfPeriod(lastValidPeriodHolder.uid); + while (true) { + int nextPeriodIndex = + timeline.getNextPeriodIndex( + currentPeriodIndex, period, window, repeatMode, shuffleModeEnabled); + while (lastValidPeriodHolder.getNext() != null + && !lastValidPeriodHolder.info.isLastInTimelinePeriod) { + lastValidPeriodHolder = lastValidPeriodHolder.getNext(); + } + + MediaPeriodHolder nextMediaPeriodHolder = lastValidPeriodHolder.getNext(); + if (nextPeriodIndex == C.INDEX_UNSET || nextMediaPeriodHolder == null) { + break; + } + int nextPeriodHolderPeriodIndex = timeline.getIndexOfPeriod(nextMediaPeriodHolder.uid); + if (nextPeriodHolderPeriodIndex != nextPeriodIndex) { + break; + } + lastValidPeriodHolder = nextMediaPeriodHolder; + currentPeriodIndex = nextPeriodIndex; + } + + // Release any period holders that don't match the new period order. + boolean readingPeriodRemoved = removeAfter(lastValidPeriodHolder); + + // Update the period info for the last holder, as it may now be the last period in the timeline. + lastValidPeriodHolder.info = getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info); + + // If renderers may have read from a period that's been removed, it is necessary to restart. + return !readingPeriodRemoved; + } + + /** + * Returns the first {@link MediaPeriodInfo} to play, based on the specified playback position. + */ + private MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) { + return getMediaPeriodInfo( + playbackInfo.periodId, playbackInfo.contentPositionUs, playbackInfo.startPositionUs); + } + + /** + * Returns the {@link MediaPeriodInfo} for the media period following {@code mediaPeriodHolder}'s + * media period. + * + * @param mediaPeriodHolder The media period holder. + * @param rendererPositionUs The current renderer position in microseconds. + * @return The following media period's info, or {@code null} if it is not yet possible to get the + * next media period info. + */ + private @Nullable MediaPeriodInfo getFollowingMediaPeriodInfo( + MediaPeriodHolder mediaPeriodHolder, long rendererPositionUs) { + // TODO: This method is called repeatedly from ExoPlayerImplInternal.maybeUpdateLoadingPeriod + // but if the timeline is not ready to provide the next period it can't return a non-null value + // until the timeline is updated. Store whether the next timeline period is ready when the + // timeline is updated, to avoid repeatedly checking the same timeline. + MediaPeriodInfo mediaPeriodInfo = mediaPeriodHolder.info; + // The expected delay until playback transitions to the new period is equal the duration of + // media that's currently buffered (assuming no interruptions). This is used to project forward + // the start position for transitions to new windows. + long bufferedDurationUs = + mediaPeriodHolder.getRendererOffset() + mediaPeriodInfo.durationUs - rendererPositionUs; + if (mediaPeriodInfo.isLastInTimelinePeriod) { + int currentPeriodIndex = timeline.getIndexOfPeriod(mediaPeriodInfo.id.periodUid); + int nextPeriodIndex = + timeline.getNextPeriodIndex( + currentPeriodIndex, period, window, repeatMode, shuffleModeEnabled); + if (nextPeriodIndex == C.INDEX_UNSET) { + // We can't create a next period yet. + return null; + } + + long startPositionUs; + long contentPositionUs; + int nextWindowIndex = + timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex; + Object nextPeriodUid = period.uid; + long windowSequenceNumber = mediaPeriodInfo.id.windowSequenceNumber; + if (timeline.getWindow(nextWindowIndex, window).firstPeriodIndex == nextPeriodIndex) { + // We're starting to buffer a new window. When playback transitions to this window we'll + // want it to be from its default start position, so project the default start position + // forward by the duration of the buffer, and start buffering from this point. + contentPositionUs = C.TIME_UNSET; + Pair defaultPosition = + timeline.getPeriodPosition( + window, + period, + nextWindowIndex, + /* windowPositionUs= */ C.TIME_UNSET, + /* defaultPositionProjectionUs= */ Math.max(0, bufferedDurationUs)); + if (defaultPosition == null) { + return null; + } + nextPeriodUid = defaultPosition.first; + startPositionUs = defaultPosition.second; + MediaPeriodHolder nextMediaPeriodHolder = mediaPeriodHolder.getNext(); + if (nextMediaPeriodHolder != null && nextMediaPeriodHolder.uid.equals(nextPeriodUid)) { + windowSequenceNumber = nextMediaPeriodHolder.info.id.windowSequenceNumber; + } else { + windowSequenceNumber = nextWindowSequenceNumber++; + } + } else { + // We're starting to buffer a new period within the same window. + startPositionUs = 0; + contentPositionUs = 0; + } + MediaPeriodId periodId = + resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber); + return getMediaPeriodInfo(periodId, contentPositionUs, startPositionUs); + } + + MediaPeriodId currentPeriodId = mediaPeriodInfo.id; + timeline.getPeriodByUid(currentPeriodId.periodUid, period); + if (currentPeriodId.isAd()) { + int adGroupIndex = currentPeriodId.adGroupIndex; + int adCountInCurrentAdGroup = period.getAdCountInAdGroup(adGroupIndex); + if (adCountInCurrentAdGroup == C.LENGTH_UNSET) { + return null; + } + int nextAdIndexInAdGroup = + period.getNextAdIndexToPlay(adGroupIndex, currentPeriodId.adIndexInAdGroup); + if (nextAdIndexInAdGroup < adCountInCurrentAdGroup) { + // Play the next ad in the ad group if it's available. + return !period.isAdAvailable(adGroupIndex, nextAdIndexInAdGroup) + ? null + : getMediaPeriodInfoForAd( + currentPeriodId.periodUid, + adGroupIndex, + nextAdIndexInAdGroup, + mediaPeriodInfo.contentPositionUs, + currentPeriodId.windowSequenceNumber); + } else { + // Play content from the ad group position. + long startPositionUs = mediaPeriodInfo.contentPositionUs; + if (startPositionUs == C.TIME_UNSET) { + // If we're transitioning from an ad group to content starting from its default position, + // project the start position forward as if this were a transition to a new window. + Pair defaultPosition = + timeline.getPeriodPosition( + window, + period, + period.windowIndex, + /* windowPositionUs= */ C.TIME_UNSET, + /* defaultPositionProjectionUs= */ Math.max(0, bufferedDurationUs)); + if (defaultPosition == null) { + return null; + } + startPositionUs = defaultPosition.second; + } + return getMediaPeriodInfoForContent( + currentPeriodId.periodUid, startPositionUs, currentPeriodId.windowSequenceNumber); + } + } else { + // Play the next ad group if it's available. + int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.endPositionUs); + if (nextAdGroupIndex == C.INDEX_UNSET) { + // The next ad group can't be played. Play content from the previous end position instead. + return getMediaPeriodInfoForContent( + currentPeriodId.periodUid, + /* startPositionUs= */ mediaPeriodInfo.durationUs, + currentPeriodId.windowSequenceNumber); + } + int adIndexInAdGroup = period.getFirstAdIndexToPlay(nextAdGroupIndex); + return !period.isAdAvailable(nextAdGroupIndex, adIndexInAdGroup) + ? null + : getMediaPeriodInfoForAd( + currentPeriodId.periodUid, + nextAdGroupIndex, + adIndexInAdGroup, + /* contentPositionUs= */ mediaPeriodInfo.durationUs, + currentPeriodId.windowSequenceNumber); + } + } + + private MediaPeriodInfo getMediaPeriodInfo( + MediaPeriodId id, long contentPositionUs, long startPositionUs) { + timeline.getPeriodByUid(id.periodUid, period); + if (id.isAd()) { + if (!period.isAdAvailable(id.adGroupIndex, id.adIndexInAdGroup)) { + return null; + } + return getMediaPeriodInfoForAd( + id.periodUid, + id.adGroupIndex, + id.adIndexInAdGroup, + contentPositionUs, + id.windowSequenceNumber); + } else { + return getMediaPeriodInfoForContent(id.periodUid, startPositionUs, id.windowSequenceNumber); + } + } + + private MediaPeriodInfo getMediaPeriodInfoForAd( + Object periodUid, + int adGroupIndex, + int adIndexInAdGroup, + long contentPositionUs, + long windowSequenceNumber) { + MediaPeriodId id = + new MediaPeriodId(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber); + long durationUs = + timeline + .getPeriodByUid(id.periodUid, period) + .getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup); + long startPositionUs = + adIndexInAdGroup == period.getFirstAdIndexToPlay(adGroupIndex) + ? period.getAdResumePositionUs() + : 0; + return new MediaPeriodInfo( + id, + startPositionUs, + contentPositionUs, + /* endPositionUs= */ C.TIME_UNSET, + durationUs, + /* isLastInTimelinePeriod= */ false, + /* isFinal= */ false); + } + + private MediaPeriodInfo getMediaPeriodInfoForContent( + Object periodUid, long startPositionUs, long windowSequenceNumber) { + int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); + MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex); + boolean isLastInPeriod = isLastInPeriod(id); + boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); + long endPositionUs = + nextAdGroupIndex != C.INDEX_UNSET + ? period.getAdGroupTimeUs(nextAdGroupIndex) + : C.TIME_UNSET; + long durationUs = + endPositionUs == C.TIME_UNSET || endPositionUs == C.TIME_END_OF_SOURCE + ? period.durationUs + : endPositionUs; + return new MediaPeriodInfo( + id, + startPositionUs, + /* contentPositionUs= */ C.TIME_UNSET, + endPositionUs, + durationUs, + isLastInPeriod, + isLastInTimeline); + } + + private boolean isLastInPeriod(MediaPeriodId id) { + return !id.isAd() && id.nextAdGroupIndex == C.INDEX_UNSET; + } + + private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { + int periodIndex = timeline.getIndexOfPeriod(id.periodUid); + int windowIndex = timeline.getPeriod(periodIndex, period).windowIndex; + return !timeline.getWindow(windowIndex, window).isDynamic + && timeline.isLastPeriod(periodIndex, period, window, repeatMode, shuffleModeEnabled) + && isLastMediaPeriodInPeriod; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/NoSampleRenderer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/NoSampleRenderer.java new file mode 100644 index 000000000000..c4662f1544af --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/NoSampleRenderer.java @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleStream; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MediaClock; +import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * A {@link Renderer} implementation whose track type is {@link C#TRACK_TYPE_NONE} and does not + * consume data from its {@link SampleStream}. + */ +public abstract class NoSampleRenderer implements Renderer, RendererCapabilities { + + @MonotonicNonNull private RendererConfiguration configuration; + private int index; + private int state; + @Nullable private SampleStream stream; + private boolean streamIsFinal; + + @Override + public final int getTrackType() { + return C.TRACK_TYPE_NONE; + } + + @Override + public final RendererCapabilities getCapabilities() { + return this; + } + + @Override + public final void setIndex(int index) { + this.index = index; + } + + @Override + @Nullable + public MediaClock getMediaClock() { + return null; + } + + @Override + public final int getState() { + return state; + } + + /** + * Replaces the {@link SampleStream} that will be associated with this renderer. + *

+ * This method may be called when the renderer is in the following states: + * {@link #STATE_DISABLED}. + * + * @param configuration The renderer configuration. + * @param formats The enabled formats. Should be empty. + * @param stream The {@link SampleStream} from which the renderer should consume. + * @param positionUs The player's current position. + * @param joining Whether this renderer is being enabled to join an ongoing playback. + * @param offsetUs The offset that should be subtracted from {@code positionUs} + * to get the playback position with respect to the media. + * @throws ExoPlaybackException If an error occurs. + */ + @Override + public final void enable(RendererConfiguration configuration, Format[] formats, + SampleStream stream, long positionUs, boolean joining, long offsetUs) + throws ExoPlaybackException { + Assertions.checkState(state == STATE_DISABLED); + this.configuration = configuration; + state = STATE_ENABLED; + onEnabled(joining); + replaceStream(formats, stream, offsetUs); + onPositionReset(positionUs, joining); + } + + @Override + public final void start() throws ExoPlaybackException { + Assertions.checkState(state == STATE_ENABLED); + state = STATE_STARTED; + onStarted(); + } + + /** + * Replaces the {@link SampleStream} that will be associated with this renderer. + *

+ * This method may be called when the renderer is in the following states: + * {@link #STATE_ENABLED}, {@link #STATE_STARTED}. + * + * @param formats The enabled formats. Should be empty. + * @param stream The {@link SampleStream} to be associated with this renderer. + * @param offsetUs The offset that should be subtracted from {@code positionUs} in + * {@link #render(long, long)} to get the playback position with respect to the media. + * @throws ExoPlaybackException If an error occurs. + */ + @Override + public final void replaceStream(Format[] formats, SampleStream stream, long offsetUs) + throws ExoPlaybackException { + Assertions.checkState(!streamIsFinal); + this.stream = stream; + onRendererOffsetChanged(offsetUs); + } + + @Override + @Nullable + public final SampleStream getStream() { + return stream; + } + + @Override + public final boolean hasReadStreamToEnd() { + return true; + } + + @Override + public long getReadingPositionUs() { + return C.TIME_END_OF_SOURCE; + } + + @Override + public final void setCurrentStreamFinal() { + streamIsFinal = true; + } + + @Override + public final boolean isCurrentStreamFinal() { + return streamIsFinal; + } + + @Override + public final void maybeThrowStreamError() throws IOException { + } + + @Override + public final void resetPosition(long positionUs) throws ExoPlaybackException { + streamIsFinal = false; + onPositionReset(positionUs, false); + } + + @Override + public final void stop() throws ExoPlaybackException { + Assertions.checkState(state == STATE_STARTED); + state = STATE_ENABLED; + onStopped(); + } + + @Override + public final void disable() { + Assertions.checkState(state == STATE_ENABLED); + state = STATE_DISABLED; + stream = null; + streamIsFinal = false; + onDisabled(); + } + + @Override + public final void reset() { + Assertions.checkState(state == STATE_DISABLED); + onReset(); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public boolean isEnded() { + return true; + } + + // RendererCapabilities implementation. + + @Override + @Capabilities + public int supportsFormat(Format format) throws ExoPlaybackException { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); + } + + @Override + @AdaptiveSupport + public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { + return ADAPTIVE_NOT_SUPPORTED; + } + + // PlayerMessage.Target implementation. + + @Override + public void handleMessage(int what, @Nullable Object object) throws ExoPlaybackException { + // Do nothing. + } + + // Methods to be overridden by subclasses. + + /** + * Called when the renderer is enabled. + *

+ * The default implementation is a no-op. + * + * @param joining Whether this renderer is being enabled to join an ongoing playback. + * @throws ExoPlaybackException If an error occurs. + */ + protected void onEnabled(boolean joining) throws ExoPlaybackException { + // Do nothing. + } + + /** + * Called when the renderer's offset has been changed. + *

+ * The default implementation is a no-op. + * + * @param offsetUs The offset that should be subtracted from {@code positionUs} in + * {@link #render(long, long)} to get the playback position with respect to the media. + * @throws ExoPlaybackException If an error occurs. + */ + protected void onRendererOffsetChanged(long offsetUs) throws ExoPlaybackException { + // Do nothing. + } + + /** + * Called when the position is reset. This occurs when the renderer is enabled after + * {@link #onRendererOffsetChanged(long)} has been called, and also when a position + * discontinuity is encountered. + *

+ * The default implementation is a no-op. + * + * @param positionUs The new playback position in microseconds. + * @param joining Whether this renderer is being enabled to join an ongoing playback. + * @throws ExoPlaybackException If an error occurs. + */ + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + // Do nothing. + } + + /** + * Called when the renderer is started. + *

+ * The default implementation is a no-op. + * + * @throws ExoPlaybackException If an error occurs. + */ + protected void onStarted() throws ExoPlaybackException { + // Do nothing. + } + + /** + * Called when the renderer is stopped. + *

+ * The default implementation is a no-op. + * + * @throws ExoPlaybackException If an error occurs. + */ + protected void onStopped() throws ExoPlaybackException { + // Do nothing. + } + + /** + * Called when the renderer is disabled. + *

+ * The default implementation is a no-op. + */ + protected void onDisabled() { + // Do nothing. + } + + /** + * Called when the renderer is reset. + * + *

The default implementation is a no-op. + */ + protected void onReset() { + // Do nothing. + } + + // Methods to be called by subclasses. + + /** + * Returns the configuration set when the renderer was most recently enabled, or {@code null} if + * the renderer has never been enabled. + */ + @Nullable + protected final RendererConfiguration getConfiguration() { + return configuration; + } + + /** + * Returns the index of the renderer within the player. + */ + protected final int getIndex() { + return index; + } + +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackInfo.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackInfo.java new file mode 100644 index 000000000000..c743e35661c0 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackInfo.java @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import androidx.annotation.CheckResult; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectorResult; + +/** + * Information about an ongoing playback. + */ +/* package */ final class PlaybackInfo { + + /** + * Dummy media period id used while the timeline is empty and no period id is specified. This id + * is used when playback infos are created with {@link #createDummy(long, TrackSelectorResult)}. + */ + private static final MediaPeriodId DUMMY_MEDIA_PERIOD_ID = + new MediaPeriodId(/* periodUid= */ new Object()); + + /** The current {@link Timeline}. */ + public final Timeline timeline; + /** The {@link MediaPeriodId} of the currently playing media period in the {@link #timeline}. */ + public final MediaPeriodId periodId; + /** + * The start position at which playback started in {@link #periodId} relative to the start of the + * associated period in the {@link #timeline}, in microseconds. Note that this value changes for + * each position discontinuity. + */ + public final long startPositionUs; + /** + * If {@link #periodId} refers to an ad, the position of the suspended content relative to the + * start of the associated period in the {@link #timeline}, in microseconds. {@link C#TIME_UNSET} + * if {@link #periodId} does not refer to an ad or if the suspended content should be played from + * its default position. + */ + public final long contentPositionUs; + /** The current playback state. One of the {@link Player}.STATE_ constants. */ + @Player.State public final int playbackState; + /** The current playback error, or null if this is not an error state. */ + @Nullable public final ExoPlaybackException playbackError; + /** Whether the player is currently loading. */ + public final boolean isLoading; + /** The currently available track groups. */ + public final TrackGroupArray trackGroups; + /** The result of the current track selection. */ + public final TrackSelectorResult trackSelectorResult; + /** The {@link MediaPeriodId} of the currently loading media period in the {@link #timeline}. */ + public final MediaPeriodId loadingMediaPeriodId; + + /** + * Position up to which media is buffered in {@link #loadingMediaPeriodId) relative to the start + * of the associated period in the {@link #timeline}, in microseconds. + */ + public volatile long bufferedPositionUs; + /** + * Total duration of buffered media from {@link #positionUs} to {@link #bufferedPositionUs} + * including all ads. + */ + public volatile long totalBufferedDurationUs; + /** + * Current playback position in {@link #periodId} relative to the start of the associated period + * in the {@link #timeline}, in microseconds. + */ + public volatile long positionUs; + + /** + * Creates empty dummy playback info which can be used for masking as long as no real playback + * info is available. + * + * @param startPositionUs The start position at which playback should start, in microseconds. + * @param emptyTrackSelectorResult An empty track selector result with null entries for each + * renderer. + * @return A dummy playback info. + */ + public static PlaybackInfo createDummy( + long startPositionUs, TrackSelectorResult emptyTrackSelectorResult) { + return new PlaybackInfo( + Timeline.EMPTY, + DUMMY_MEDIA_PERIOD_ID, + startPositionUs, + /* contentPositionUs= */ C.TIME_UNSET, + Player.STATE_IDLE, + /* playbackError= */ null, + /* isLoading= */ false, + TrackGroupArray.EMPTY, + emptyTrackSelectorResult, + DUMMY_MEDIA_PERIOD_ID, + startPositionUs, + /* totalBufferedDurationUs= */ 0, + startPositionUs); + } + + /** + * Create playback info. + * + * @param timeline See {@link #timeline}. + * @param periodId See {@link #periodId}. + * @param startPositionUs See {@link #startPositionUs}. + * @param contentPositionUs See {@link #contentPositionUs}. + * @param playbackState See {@link #playbackState}. + * @param isLoading See {@link #isLoading}. + * @param trackGroups See {@link #trackGroups}. + * @param trackSelectorResult See {@link #trackSelectorResult}. + * @param loadingMediaPeriodId See {@link #loadingMediaPeriodId}. + * @param bufferedPositionUs See {@link #bufferedPositionUs}. + * @param totalBufferedDurationUs See {@link #totalBufferedDurationUs}. + * @param positionUs See {@link #positionUs}. + */ + public PlaybackInfo( + Timeline timeline, + MediaPeriodId periodId, + long startPositionUs, + long contentPositionUs, + @Player.State int playbackState, + @Nullable ExoPlaybackException playbackError, + boolean isLoading, + TrackGroupArray trackGroups, + TrackSelectorResult trackSelectorResult, + MediaPeriodId loadingMediaPeriodId, + long bufferedPositionUs, + long totalBufferedDurationUs, + long positionUs) { + this.timeline = timeline; + this.periodId = periodId; + this.startPositionUs = startPositionUs; + this.contentPositionUs = contentPositionUs; + this.playbackState = playbackState; + this.playbackError = playbackError; + this.isLoading = isLoading; + this.trackGroups = trackGroups; + this.trackSelectorResult = trackSelectorResult; + this.loadingMediaPeriodId = loadingMediaPeriodId; + this.bufferedPositionUs = bufferedPositionUs; + this.totalBufferedDurationUs = totalBufferedDurationUs; + this.positionUs = positionUs; + } + + /** + * Returns dummy media period id for the first-to-be-played period of the current timeline. + * + * @param shuffleModeEnabled Whether shuffle mode is enabled. + * @param window A writable {@link Timeline.Window}. + * @param period A writable {@link Timeline.Period}. + * @return A dummy media period id for the first-to-be-played period of the current timeline. + */ + public MediaPeriodId getDummyFirstMediaPeriodId( + boolean shuffleModeEnabled, Timeline.Window window, Timeline.Period period) { + if (timeline.isEmpty()) { + return DUMMY_MEDIA_PERIOD_ID; + } + int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); + int firstPeriodIndex = timeline.getWindow(firstWindowIndex, window).firstPeriodIndex; + int currentPeriodIndex = timeline.getIndexOfPeriod(periodId.periodUid); + long windowSequenceNumber = C.INDEX_UNSET; + if (currentPeriodIndex != C.INDEX_UNSET) { + int currentWindowIndex = timeline.getPeriod(currentPeriodIndex, period).windowIndex; + if (firstWindowIndex == currentWindowIndex) { + // Keep window sequence number if the new position is still in the same window. + windowSequenceNumber = periodId.windowSequenceNumber; + } + } + return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex), windowSequenceNumber); + } + + /** + * Copies playback info with new playing position. + * + * @param periodId New playing media period. See {@link #periodId}. + * @param positionUs New position. See {@link #positionUs}. + * @param contentPositionUs New content position. See {@link #contentPositionUs}. Value is ignored + * if {@code periodId.isAd()} is true. + * @param totalBufferedDurationUs New buffered duration. See {@link #totalBufferedDurationUs}. + * @return Copied playback info with new playing position. + */ + @CheckResult + public PlaybackInfo copyWithNewPosition( + MediaPeriodId periodId, + long positionUs, + long contentPositionUs, + long totalBufferedDurationUs) { + return new PlaybackInfo( + timeline, + periodId, + positionUs, + periodId.isAd() ? contentPositionUs : C.TIME_UNSET, + playbackState, + playbackError, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); + } + + /** + * Copies playback info with the new timeline. + * + * @param timeline New timeline. See {@link #timeline}. + * @return Copied playback info with the new timeline. + */ + @CheckResult + public PlaybackInfo copyWithTimeline(Timeline timeline) { + return new PlaybackInfo( + timeline, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + playbackError, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); + } + + /** + * Copies playback info with new playback state. + * + * @param playbackState New playback state. See {@link #playbackState}. + * @return Copied playback info with new playback state. + */ + @CheckResult + public PlaybackInfo copyWithPlaybackState(int playbackState) { + return new PlaybackInfo( + timeline, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + playbackError, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); + } + + /** + * Copies playback info with a playback error. + * + * @param playbackError The error. See {@link #playbackError}. + * @return Copied playback info with the playback error. + */ + @CheckResult + public PlaybackInfo copyWithPlaybackError(@Nullable ExoPlaybackException playbackError) { + return new PlaybackInfo( + timeline, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + playbackError, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); + } + + /** + * Copies playback info with new loading state. + * + * @param isLoading New loading state. See {@link #isLoading}. + * @return Copied playback info with new loading state. + */ + @CheckResult + public PlaybackInfo copyWithIsLoading(boolean isLoading) { + return new PlaybackInfo( + timeline, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + playbackError, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); + } + + /** + * Copies playback info with new track information. + * + * @param trackGroups New track groups. See {@link #trackGroups}. + * @param trackSelectorResult New track selector result. See {@link #trackSelectorResult}. + * @return Copied playback info with new track information. + */ + @CheckResult + public PlaybackInfo copyWithTrackInfo( + TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) { + return new PlaybackInfo( + timeline, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + playbackError, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); + } + + /** + * Copies playback info with new loading media period. + * + * @param loadingMediaPeriodId New loading media period id. See {@link #loadingMediaPeriodId}. + * @return Copied playback info with new loading media period. + */ + @CheckResult + public PlaybackInfo copyWithLoadingMediaPeriodId(MediaPeriodId loadingMediaPeriodId) { + return new PlaybackInfo( + timeline, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + playbackError, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackParameters.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackParameters.java index e3d8a743f2d7..fd47117abab1 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackParameters.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackParameters.java @@ -15,53 +15,80 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; + /** * The parameters that apply to playback. */ public final class PlaybackParameters { /** - * The default playback parameters: real-time playback with no pitch modification. + * The default playback parameters: real-time playback with no pitch modification or silence + * skipping. */ - public static final PlaybackParameters DEFAULT = new PlaybackParameters(1f, 1f); + public static final PlaybackParameters DEFAULT = new PlaybackParameters(/* speed= */ 1f); - /** - * The factor by which playback will be sped up. - */ + /** The factor by which playback will be sped up. */ public final float speed; - /** - * The factor by which the audio pitch will be scaled. - */ + /** The factor by which the audio pitch will be scaled. */ public final float pitch; + /** Whether to skip silence in the input. */ + public final boolean skipSilence; + private final int scaledUsPerMs; /** - * Creates new playback parameters. + * Creates new playback parameters that set the playback speed. * - * @param speed The factor by which playback will be sped up. - * @param pitch The factor by which the audio pitch will be scaled. + * @param speed The factor by which playback will be sped up. Must be greater than zero. + */ + public PlaybackParameters(float speed) { + this(speed, /* pitch= */ 1f, /* skipSilence= */ false); + } + + /** + * Creates new playback parameters that set the playback speed and audio pitch scaling factor. + * + * @param speed The factor by which playback will be sped up. Must be greater than zero. + * @param pitch The factor by which the audio pitch will be scaled. Must be greater than zero. */ public PlaybackParameters(float speed, float pitch) { + this(speed, pitch, /* skipSilence= */ false); + } + + /** + * Creates new playback parameters that set the playback speed, audio pitch scaling factor and + * whether to skip silence in the audio stream. + * + * @param speed The factor by which playback will be sped up. Must be greater than zero. + * @param pitch The factor by which the audio pitch will be scaled. Must be greater than zero. + * @param skipSilence Whether to skip silences in the audio stream. + */ + public PlaybackParameters(float speed, float pitch, boolean skipSilence) { + Assertions.checkArgument(speed > 0); + Assertions.checkArgument(pitch > 0); this.speed = speed; this.pitch = pitch; + this.skipSilence = skipSilence; scaledUsPerMs = Math.round(speed * 1000f); } /** - * Scales the millisecond duration {@code timeMs} by the playback speed, returning the result in - * microseconds. + * Returns the media time in microseconds that will elapse in {@code timeMs} milliseconds of + * wallclock time. * * @param timeMs The time to scale, in milliseconds. * @return The scaled time, in microseconds. */ - public long getSpeedAdjustedDurationUs(long timeMs) { + public long getMediaTimeUsForPlayoutTimeMs(long timeMs) { return timeMs * scaledUsPerMs; } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -69,14 +96,17 @@ public final class PlaybackParameters { return false; } PlaybackParameters other = (PlaybackParameters) obj; - return this.speed == other.speed && this.pitch == other.pitch; + return this.speed == other.speed + && this.pitch == other.pitch + && this.skipSilence == other.skipSilence; } - + @Override public int hashCode() { int result = 17; result = 31 * result + Float.floatToRawIntBits(speed); result = 31 * result + Float.floatToRawIntBits(pitch); + result = 31 * result + (skipSilence ? 1 : 0); return result; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackPreparer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackPreparer.java new file mode 100644 index 000000000000..831a28aa47fd --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackPreparer.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +/** Called to prepare a playback. */ +public interface PlaybackPreparer { + + /** Called to prepare a playback. */ + void preparePlayback(); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Player.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Player.java new file mode 100644 index 000000000000..89059dc2eaa2 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Player.java @@ -0,0 +1,1040 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import android.os.Looper; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.TextureView; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C.VideoScalingMode; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioAttributes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AuxEffectInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.TextOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoFrameMetadataListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.spherical.CameraMotionListener; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A media player interface defining traditional high-level functionality, such as the ability to + * play, pause, seek and query properties of the currently playing media. + *

+ * Some important properties of media players that implement this interface are: + *

+ */ +public interface Player { + + /** The audio component of a {@link Player}. */ + interface AudioComponent { + + /** + * Adds a listener to receive audio events. + * + * @param listener The listener to register. + */ + void addAudioListener(AudioListener listener); + + /** + * Removes a listener of audio events. + * + * @param listener The listener to unregister. + */ + void removeAudioListener(AudioListener listener); + + /** + * Sets the attributes for audio playback, used by the underlying audio track. If not set, the + * default audio attributes will be used. They are suitable for general media playback. + * + *

Setting the audio attributes during playback may introduce a short gap in audio output as + * the audio track is recreated. A new audio session id will also be generated. + * + *

If tunneling is enabled by the track selector, the specified audio attributes will be + * ignored, but they will take effect if audio is later played without tunneling. + * + *

If the device is running a build before platform API version 21, audio attributes cannot + * be set directly on the underlying audio track. In this case, the usage will be mapped onto an + * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}. + * + * @param audioAttributes The attributes to use for audio playback. + * @deprecated Use {@link AudioComponent#setAudioAttributes(AudioAttributes, boolean)}. + */ + @Deprecated + void setAudioAttributes(AudioAttributes audioAttributes); + + /** + * Sets the attributes for audio playback, used by the underlying audio track. If not set, the + * default audio attributes will be used. They are suitable for general media playback. + * + *

Setting the audio attributes during playback may introduce a short gap in audio output as + * the audio track is recreated. A new audio session id will also be generated. + * + *

If tunneling is enabled by the track selector, the specified audio attributes will be + * ignored, but they will take effect if audio is later played without tunneling. + * + *

If the device is running a build before platform API version 21, audio attributes cannot + * be set directly on the underlying audio track. In this case, the usage will be mapped onto an + * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}. + * + *

If audio focus should be handled, the {@link AudioAttributes#usage} must be {@link + * C#USAGE_MEDIA} or {@link C#USAGE_GAME}. Other usages will throw an {@link + * IllegalArgumentException}. + * + * @param audioAttributes The attributes to use for audio playback. + * @param handleAudioFocus True if the player should handle audio focus, false otherwise. + */ + void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus); + + /** Returns the attributes for audio playback. */ + AudioAttributes getAudioAttributes(); + + /** Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set. */ + int getAudioSessionId(); + + /** Sets information on an auxiliary audio effect to attach to the underlying audio track. */ + void setAuxEffectInfo(AuxEffectInfo auxEffectInfo); + + /** Detaches any previously attached auxiliary audio effect from the underlying audio track. */ + void clearAuxEffectInfo(); + + /** + * Sets the audio volume, with 0 being silence and 1 being unity gain. + * + * @param audioVolume The audio volume. + */ + void setVolume(float audioVolume); + + /** Returns the audio volume, with 0 being silence and 1 being unity gain. */ + float getVolume(); + } + + /** The video component of a {@link Player}. */ + interface VideoComponent { + + /** + * Sets the {@link VideoScalingMode}. + * + * @param videoScalingMode The {@link VideoScalingMode}. + */ + void setVideoScalingMode(@VideoScalingMode int videoScalingMode); + + /** Returns the {@link VideoScalingMode}. */ + @VideoScalingMode + int getVideoScalingMode(); + + /** + * Adds a listener to receive video events. + * + * @param listener The listener to register. + */ + void addVideoListener(VideoListener listener); + + /** + * Removes a listener of video events. + * + * @param listener The listener to unregister. + */ + void removeVideoListener(VideoListener listener); + + /** + * Sets a listener to receive video frame metadata events. + * + *

This method is intended to be called by the same component that sets the {@link Surface} + * onto which video will be rendered. If using ExoPlayer's standard UI components, this method + * should not be called directly from application code. + * + * @param listener The listener. + */ + void setVideoFrameMetadataListener(VideoFrameMetadataListener listener); + + /** + * Clears the listener which receives video frame metadata events if it matches the one passed. + * Else does nothing. + * + * @param listener The listener to clear. + */ + void clearVideoFrameMetadataListener(VideoFrameMetadataListener listener); + + /** + * Sets a listener of camera motion events. + * + * @param listener The listener. + */ + void setCameraMotionListener(CameraMotionListener listener); + + /** + * Clears the listener which receives camera motion events if it matches the one passed. Else + * does nothing. + * + * @param listener The listener to clear. + */ + void clearCameraMotionListener(CameraMotionListener listener); + + /** + * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView} + * currently set on the player. + */ + void clearVideoSurface(); + + /** + * Clears the {@link Surface} onto which video is being rendered if it matches the one passed. + * Else does nothing. + * + * @param surface The surface to clear. + */ + void clearVideoSurface(@Nullable Surface surface); + + /** + * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for + * tracking the lifecycle of the surface, and must clear the surface by calling {@code + * setVideoSurface(null)} if the surface is destroyed. + * + *

If the surface is held by a {@link SurfaceView}, {@link TextureView} or {@link + * SurfaceHolder} then it's recommended to use {@link #setVideoSurfaceView(SurfaceView)}, {@link + * #setVideoTextureView(TextureView)} or {@link #setVideoSurfaceHolder(SurfaceHolder)} rather + * than this method, since passing the holder allows the player to track the lifecycle of the + * surface automatically. + * + * @param surface The {@link Surface}. + */ + void setVideoSurface(@Nullable Surface surface); + + /** + * Sets the {@link SurfaceHolder} that holds the {@link Surface} onto which video will be + * rendered. The player will track the lifecycle of the surface automatically. + * + * @param surfaceHolder The surface holder. + */ + void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder); + + /** + * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being + * rendered if it matches the one passed. Else does nothing. + * + * @param surfaceHolder The surface holder to clear. + */ + void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder); + + /** + * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the + * lifecycle of the surface automatically. + * + * @param surfaceView The surface view. + */ + void setVideoSurfaceView(@Nullable SurfaceView surfaceView); + + /** + * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one + * passed. Else does nothing. + * + * @param surfaceView The texture view to clear. + */ + void clearVideoSurfaceView(@Nullable SurfaceView surfaceView); + + /** + * Sets the {@link TextureView} onto which video will be rendered. The player will track the + * lifecycle of the surface automatically. + * + * @param textureView The texture view. + */ + void setVideoTextureView(@Nullable TextureView textureView); + + /** + * Clears the {@link TextureView} onto which video is being rendered if it matches the one + * passed. Else does nothing. + * + * @param textureView The texture view to clear. + */ + void clearVideoTextureView(@Nullable TextureView textureView); + + /** + * Sets the video decoder output buffer renderer. This is intended for use only with extension + * renderers that accept {@link C#MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER}. For most use + * cases, an output surface or view should be passed via {@link #setVideoSurface(Surface)} or + * {@link #setVideoSurfaceView(SurfaceView)} instead. + * + * @param videoDecoderOutputBufferRenderer The video decoder output buffer renderer, or {@code + * null} to clear the output buffer renderer. + */ + void setVideoDecoderOutputBufferRenderer( + @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer); + + /** Clears the video decoder output buffer renderer. */ + void clearVideoDecoderOutputBufferRenderer(); + + /** + * Clears the video decoder output buffer renderer if it matches the one passed. Else does + * nothing. + * + * @param videoDecoderOutputBufferRenderer The video decoder output buffer renderer to clear. + */ + void clearVideoDecoderOutputBufferRenderer( + @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer); + } + + /** The text component of a {@link Player}. */ + interface TextComponent { + + /** + * Registers an output to receive text events. + * + * @param listener The output to register. + */ + void addTextOutput(TextOutput listener); + + /** + * Removes a text output. + * + * @param listener The output to remove. + */ + void removeTextOutput(TextOutput listener); + } + + /** The metadata component of a {@link Player}. */ + interface MetadataComponent { + + /** + * Adds a {@link MetadataOutput} to receive metadata. + * + * @param output The output to register. + */ + void addMetadataOutput(MetadataOutput output); + + /** + * Removes a {@link MetadataOutput}. + * + * @param output The output to remove. + */ + void removeMetadataOutput(MetadataOutput output); + } + + /** + * Listener of changes in player state. All methods have no-op default implementations to allow + * selective overrides. + */ + interface EventListener { + + /** + * Called when the timeline has been refreshed. + * + *

Note that if the timeline has changed then a position discontinuity may also have + * occurred. For example, the current period index may have changed as a result of periods being + * added or removed from the timeline. This will not be reported via a separate call to + * {@link #onPositionDiscontinuity(int)}. + * + * @param timeline The latest timeline. Never null, but may be empty. + * @param reason The {@link TimelineChangeReason} responsible for this timeline change. + */ + @SuppressWarnings("deprecation") + default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { + Object manifest = null; + if (timeline.getWindowCount() == 1) { + // Legacy behavior was to report the manifest for single window timelines only. + Timeline.Window window = new Timeline.Window(); + manifest = timeline.getWindow(0, window).manifest; + } + // Call deprecated version. + onTimelineChanged(timeline, manifest, reason); + } + + /** + * Called when the timeline and/or manifest has been refreshed. + * + *

Note that if the timeline has changed then a position discontinuity may also have + * occurred. For example, the current period index may have changed as a result of periods being + * added or removed from the timeline. This will not be reported via a separate call to + * {@link #onPositionDiscontinuity(int)}. + * + * @param timeline The latest timeline. Never null, but may be empty. + * @param manifest The latest manifest. May be null. + * @param reason The {@link TimelineChangeReason} responsible for this timeline change. + * @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be + * accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex, + * window).manifest} for a given window index. + */ + @Deprecated + default void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {} + + /** + * Called when the available or selected tracks change. + * + * @param trackGroups The available tracks. Never null, but may be of length zero. + * @param trackSelections The track selections for each renderer. Never null and always of + * length {@link #getRendererCount()}, but may contain null elements. + */ + default void onTracksChanged( + TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {} + + /** + * Called when the player starts or stops loading the source. + * + * @param isLoading Whether the source is currently being loaded. + */ + default void onLoadingChanged(boolean isLoading) {} + + /** + * Called when the value returned from either {@link #getPlayWhenReady()} or {@link + * #getPlaybackState()} changes. + * + * @param playWhenReady Whether playback will proceed when ready. + * @param playbackState The new {@link State playback state}. + */ + default void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {} + + /** + * Called when the value returned from {@link #getPlaybackSuppressionReason()} changes. + * + * @param playbackSuppressionReason The current {@link PlaybackSuppressionReason}. + */ + default void onPlaybackSuppressionReasonChanged( + @PlaybackSuppressionReason int playbackSuppressionReason) {} + + /** + * Called when the value of {@link #isPlaying()} changes. + * + * @param isPlaying Whether the player is playing. + */ + default void onIsPlayingChanged(boolean isPlaying) {} + + /** + * Called when the value of {@link #getRepeatMode()} changes. + * + * @param repeatMode The {@link RepeatMode} used for playback. + */ + default void onRepeatModeChanged(@RepeatMode int repeatMode) {} + + /** + * Called when the value of {@link #getShuffleModeEnabled()} changes. + * + * @param shuffleModeEnabled Whether shuffling of windows is enabled. + */ + default void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {} + + /** + * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE} + * immediately after this method is called. The player instance can still be used, and {@link + * #release()} must still be called on the player should it no longer be required. + * + * @param error The error. + */ + default void onPlayerError(ExoPlaybackException error) {} + + /** + * Called when a position discontinuity occurs without a change to the timeline. A position + * discontinuity occurs when the current window or period index changes (as a result of playback + * transitioning from one period in the timeline to the next), or when the playback position + * jumps within the period currently being played (as a result of a seek being performed, or + * when the source introduces a discontinuity internally). + * + *

When a position discontinuity occurs as a result of a change to the timeline this method + * is not called. {@link #onTimelineChanged(Timeline, int)} is called in this case. + * + * @param reason The {@link DiscontinuityReason} responsible for the discontinuity. + */ + default void onPositionDiscontinuity(@DiscontinuityReason int reason) {} + + /** + * Called when the current playback parameters change. The playback parameters may change due to + * a call to {@link #setPlaybackParameters(PlaybackParameters)}, or the player itself may change + * them (for example, if audio playback switches to passthrough mode, where speed adjustment is + * no longer possible). + * + * @param playbackParameters The playback parameters. + */ + default void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {} + + /** + * Called when all pending seek requests have been processed by the player. This is guaranteed + * to happen after any necessary changes to the player state were reported to {@link + * #onPlayerStateChanged(boolean, int)}. + */ + default void onSeekProcessed() {} + } + + /** + * @deprecated Use {@link EventListener} interface directly for selective overrides as all methods + * are implemented as no-op default methods. + */ + @Deprecated + abstract class DefaultEventListener implements EventListener { + + @Override + public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { + Object manifest = null; + if (timeline.getWindowCount() == 1) { + // Legacy behavior was to report the manifest for single window timelines only. + Timeline.Window window = new Timeline.Window(); + manifest = timeline.getWindow(0, window).manifest; + } + // Call deprecated version. + onTimelineChanged(timeline, manifest, reason); + } + + @Override + @SuppressWarnings("deprecation") + public void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { + // Call deprecated version. Otherwise, do nothing. + onTimelineChanged(timeline, manifest); + } + + /** @deprecated Use {@link EventListener#onTimelineChanged(Timeline, int)} instead. */ + @Deprecated + public void onTimelineChanged(Timeline timeline, @Nullable Object manifest) { + // Do nothing. + } + } + + /** + * Playback state. One of {@link #STATE_IDLE}, {@link #STATE_BUFFERING}, {@link #STATE_READY} or + * {@link #STATE_ENDED}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED}) + @interface State {} + /** + * The player does not have any media to play. + */ + int STATE_IDLE = 1; + /** + * The player is not able to immediately play from its current position. This state typically + * occurs when more data needs to be loaded. + */ + int STATE_BUFFERING = 2; + /** + * The player is able to immediately play from its current position. The player will be playing if + * {@link #getPlayWhenReady()} is true, and paused otherwise. + */ + int STATE_READY = 3; + /** + * The player has finished playing the media. + */ + int STATE_ENDED = 4; + + /** + * Reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code true}. One + * of {@link #PLAYBACK_SUPPRESSION_REASON_NONE} or {@link + * #PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + PLAYBACK_SUPPRESSION_REASON_NONE, + PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS + }) + @interface PlaybackSuppressionReason {} + /** Playback is not suppressed. */ + int PLAYBACK_SUPPRESSION_REASON_NONE = 0; + /** Playback is suppressed due to transient audio focus loss. */ + int PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS = 1; + + /** + * Repeat modes for playback. One of {@link #REPEAT_MODE_OFF}, {@link #REPEAT_MODE_ONE} or {@link + * #REPEAT_MODE_ALL}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({REPEAT_MODE_OFF, REPEAT_MODE_ONE, REPEAT_MODE_ALL}) + @interface RepeatMode {} + /** + * Normal playback without repetition. + */ + int REPEAT_MODE_OFF = 0; + /** + * "Repeat One" mode to repeat the currently playing window infinitely. + */ + int REPEAT_MODE_ONE = 1; + /** + * "Repeat All" mode to repeat the entire timeline infinitely. + */ + int REPEAT_MODE_ALL = 2; + + /** + * Reasons for position discontinuities. One of {@link #DISCONTINUITY_REASON_PERIOD_TRANSITION}, + * {@link #DISCONTINUITY_REASON_SEEK}, {@link #DISCONTINUITY_REASON_SEEK_ADJUSTMENT}, {@link + * #DISCONTINUITY_REASON_AD_INSERTION} or {@link #DISCONTINUITY_REASON_INTERNAL}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + DISCONTINUITY_REASON_PERIOD_TRANSITION, + DISCONTINUITY_REASON_SEEK, + DISCONTINUITY_REASON_SEEK_ADJUSTMENT, + DISCONTINUITY_REASON_AD_INSERTION, + DISCONTINUITY_REASON_INTERNAL + }) + @interface DiscontinuityReason {} + /** + * Automatic playback transition from one period in the timeline to the next. The period index may + * be the same as it was before the discontinuity in case the current period is repeated. + */ + int DISCONTINUITY_REASON_PERIOD_TRANSITION = 0; + /** Seek within the current period or to another period. */ + int DISCONTINUITY_REASON_SEEK = 1; + /** + * Seek adjustment due to being unable to seek to the requested position or because the seek was + * permitted to be inexact. + */ + int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2; + /** Discontinuity to or from an ad within one period in the timeline. */ + int DISCONTINUITY_REASON_AD_INSERTION = 3; + /** Discontinuity introduced internally by the source. */ + int DISCONTINUITY_REASON_INTERNAL = 4; + + /** + * Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, {@link + * #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + TIMELINE_CHANGE_REASON_PREPARED, + TIMELINE_CHANGE_REASON_RESET, + TIMELINE_CHANGE_REASON_DYNAMIC + }) + @interface TimelineChangeReason {} + /** Timeline and manifest changed as a result of a player initialization with new media. */ + int TIMELINE_CHANGE_REASON_PREPARED = 0; + /** Timeline and manifest changed as a result of a player reset. */ + int TIMELINE_CHANGE_REASON_RESET = 1; + /** + * Timeline or manifest changed as a result of an dynamic update introduced by the played media. + */ + int TIMELINE_CHANGE_REASON_DYNAMIC = 2; + + /** Returns the component of this player for audio output, or null if audio is not supported. */ + @Nullable + AudioComponent getAudioComponent(); + + /** Returns the component of this player for video output, or null if video is not supported. */ + @Nullable + VideoComponent getVideoComponent(); + + /** Returns the component of this player for text output, or null if text is not supported. */ + @Nullable + TextComponent getTextComponent(); + + /** + * Returns the component of this player for metadata output, or null if metadata is not supported. + */ + @Nullable + MetadataComponent getMetadataComponent(); + + /** + * Returns the {@link Looper} associated with the application thread that's used to access the + * player and on which player events are received. + */ + Looper getApplicationLooper(); + + /** + * Register a listener to receive events from the player. The listener's methods will be called on + * the thread that was used to construct the player. However, if the thread used to construct the + * player does not have a {@link Looper}, then the listener will be called on the main thread. + * + * @param listener The listener to register. + */ + void addListener(EventListener listener); + + /** + * Unregister a listener. The listener will no longer receive events from the player. + * + * @param listener The listener to unregister. + */ + void removeListener(EventListener listener); + + /** + * Returns the current {@link State playback state} of the player. + * + * @return The current {@link State playback state}. + */ + @State + int getPlaybackState(); + + /** + * Returns the reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code + * true}, or {@link #PLAYBACK_SUPPRESSION_REASON_NONE} if playback is not suppressed. + * + * @return The current {@link PlaybackSuppressionReason playback suppression reason}. + */ + @PlaybackSuppressionReason + int getPlaybackSuppressionReason(); + + /** + * Returns whether the player is playing, i.e. {@link #getContentPosition()} is advancing. + * + *

If {@code false}, then at least one of the following is true: + * + *

+ * + * @return Whether the player is playing. + */ + boolean isPlaying(); + + /** + * Returns the error that caused playback to fail. This is the same error that will have been + * reported via {@link Player.EventListener#onPlayerError(ExoPlaybackException)} at the time of + * failure. It can be queried using this method until {@code stop(true)} is called or the player + * is re-prepared. + * + *

Note that this method will always return {@code null} if {@link #getPlaybackState()} is not + * {@link #STATE_IDLE}. + * + * @return The error, or {@code null}. + */ + @Nullable + ExoPlaybackException getPlaybackError(); + + /** + * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. + *

+ * If the player is already in the ready state then this method can be used to pause and resume + * playback. + * + * @param playWhenReady Whether playback should proceed when ready. + */ + void setPlayWhenReady(boolean playWhenReady); + + /** + * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. + * + * @return Whether playback will proceed when ready. + */ + boolean getPlayWhenReady(); + + /** + * Sets the {@link RepeatMode} to be used for playback. + * + * @param repeatMode The repeat mode. + */ + void setRepeatMode(@RepeatMode int repeatMode); + + /** + * Returns the current {@link RepeatMode} used for playback. + * + * @return The current repeat mode. + */ + @RepeatMode int getRepeatMode(); + + /** + * Sets whether shuffling of windows is enabled. + * + * @param shuffleModeEnabled Whether shuffling is enabled. + */ + void setShuffleModeEnabled(boolean shuffleModeEnabled); + + /** + * Returns whether shuffling of windows is enabled. + */ + boolean getShuffleModeEnabled(); + + /** + * Whether the player is currently loading the source. + * + * @return Whether the player is currently loading the source. + */ + boolean isLoading(); + + /** + * Seeks to the default position associated with the current window. The position can depend on + * the type of media being played. For live streams it will typically be the live edge of the + * window. For other streams it will typically be the start of the window. + */ + void seekToDefaultPosition(); + + /** + * Seeks to the default position associated with the specified window. The position can depend on + * the type of media being played. For live streams it will typically be the live edge of the + * window. For other streams it will typically be the start of the window. + * + * @param windowIndex The index of the window whose associated default position should be seeked + * to. + */ + void seekToDefaultPosition(int windowIndex); + + /** + * Seeks to a position specified in milliseconds in the current window. + * + * @param positionMs The seek position in the current window, or {@link C#TIME_UNSET} to seek to + * the window's default position. + */ + void seekTo(long positionMs); + + /** + * Seeks to a position specified in milliseconds in the specified window. + * + * @param windowIndex The index of the window. + * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to + * the window's default position. + * @throws IllegalSeekPositionException If the player has a non-empty timeline and the provided + * {@code windowIndex} is not within the bounds of the current timeline. + */ + void seekTo(int windowIndex, long positionMs); + + /** + * Returns whether a previous window exists, which may depend on the current repeat mode and + * whether shuffle mode is enabled. + */ + boolean hasPrevious(); + + /** + * Seeks to the default position of the previous window in the timeline, which may depend on the + * current repeat mode and whether shuffle mode is enabled. Does nothing if {@link #hasPrevious()} + * is {@code false}. + */ + void previous(); + + /** + * Returns whether a next window exists, which may depend on the current repeat mode and whether + * shuffle mode is enabled. + */ + boolean hasNext(); + + /** + * Seeks to the default position of the next window in the timeline, which may depend on the + * current repeat mode and whether shuffle mode is enabled. Does nothing if {@link #hasNext()} is + * {@code false}. + */ + void next(); + + /** + * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the + * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment. + * + *

Playback parameters changes may cause the player to buffer. {@link + * EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever the + * currently active playback parameters change. + * + * @param playbackParameters The playback parameters, or {@code null} to use the defaults. + */ + void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters); + + /** + * Returns the currently active playback parameters. + * + * @see EventListener#onPlaybackParametersChanged(PlaybackParameters) + */ + PlaybackParameters getPlaybackParameters(); + + /** + * Stops playback without resetting the player. Use {@code setPlayWhenReady(false)} rather than + * this method if the intention is to pause playback. + * + *

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

Calling this method does not reset the playback position. + */ + void stop(); + + /** + * Stops playback and optionally resets the player. Use {@code setPlayWhenReady(false)} rather + * than this method if the intention is to pause playback. + * + *

Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The + * player instance can still be used, and {@link #release()} must still be called on the player if + * it's no longer required. + * + * @param reset Whether the player should be reset. + */ + void stop(boolean reset); + + /** + * Releases the player. This method must be called when the player is no longer required. The + * player must not be used after calling this method. + */ + void release(); + + /** + * Returns the number of renderers. + */ + int getRendererCount(); + + /** + * Returns the track type that the renderer at a given index handles. + * + * @see Renderer#getTrackType() + * @param index The index of the renderer. + * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}. + */ + int getRendererType(int index); + + /** + * Returns the available track groups. + */ + TrackGroupArray getCurrentTrackGroups(); + + /** + * Returns the current track selections for each renderer. + */ + TrackSelectionArray getCurrentTrackSelections(); + + /** + * Returns the current manifest. The type depends on the type of media being played. May be null. + */ + @Nullable Object getCurrentManifest(); + + /** + * Returns the current {@link Timeline}. Never null, but may be empty. + */ + Timeline getCurrentTimeline(); + + /** + * Returns the index of the period currently being played. + */ + int getCurrentPeriodIndex(); + + /** + * Returns the index of the window currently being played. + */ + int getCurrentWindowIndex(); + + /** + * Returns the index of the next timeline window to be played, which may depend on the current + * repeat mode and whether shuffle mode is enabled. Returns {@link C#INDEX_UNSET} if the window + * currently being played is the last window. + */ + int getNextWindowIndex(); + + /** + * Returns the index of the previous timeline window to be played, which may depend on the current + * repeat mode and whether shuffle mode is enabled. Returns {@link C#INDEX_UNSET} if the window + * currently being played is the first window. + */ + int getPreviousWindowIndex(); + + /** + * Returns the tag of the currently playing window in the timeline. May be null if no tag is set + * or the timeline is not yet available. + */ + @Nullable Object getCurrentTag(); + + /** + * Returns the duration of the current content window or ad in milliseconds, or {@link + * C#TIME_UNSET} if the duration is not known. + */ + long getDuration(); + + /** Returns the playback position in the current content window or ad, in milliseconds. */ + long getCurrentPosition(); + + /** + * Returns an estimate of the position in the current content window or ad up to which data is + * buffered, in milliseconds. + */ + long getBufferedPosition(); + + /** + * Returns an estimate of the percentage in the current content window or ad up to which data is + * buffered, or 0 if no estimate is available. + */ + int getBufferedPercentage(); + + /** + * Returns an estimate of the total buffered duration from the current position, in milliseconds. + * This includes pre-buffered data for subsequent ads and windows. + */ + long getTotalBufferedDuration(); + + /** + * Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is + * empty. + * + * @see Timeline.Window#isDynamic + */ + boolean isCurrentWindowDynamic(); + + /** + * Returns whether the current window is live, or {@code false} if the {@link Timeline} is empty. + * + * @see Timeline.Window#isLive + */ + boolean isCurrentWindowLive(); + + /** + * Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is + * empty. + * + * @see Timeline.Window#isSeekable + */ + boolean isCurrentWindowSeekable(); + + /** + * Returns whether the player is currently playing an ad. + */ + boolean isPlayingAd(); + + /** + * If {@link #isPlayingAd()} returns true, returns the index of the ad group in the period + * currently being played. Returns {@link C#INDEX_UNSET} otherwise. + */ + int getCurrentAdGroupIndex(); + + /** + * If {@link #isPlayingAd()} returns true, returns the index of the ad in its ad group. Returns + * {@link C#INDEX_UNSET} otherwise. + */ + int getCurrentAdIndexInAdGroup(); + + /** + * If {@link #isPlayingAd()} returns {@code true}, returns the duration of the current content + * window in milliseconds, or {@link C#TIME_UNSET} if the duration is not known. If there is no ad + * playing, the returned duration is the same as that returned by {@link #getDuration()}. + */ + long getContentDuration(); + + /** + * If {@link #isPlayingAd()} returns {@code true}, returns the content position that will be + * played once all ads in the ad group have finished playing, in milliseconds. If there is no ad + * playing, the returned position is the same as that returned by {@link #getCurrentPosition()}. + */ + long getContentPosition(); + + /** + * If {@link #isPlayingAd()} returns {@code true}, returns an estimate of the content position in + * the current content window up to which data is buffered, in milliseconds. If there is no ad + * playing, the returned position is the same as that returned by {@link #getBufferedPosition()}. + */ + long getContentBufferedPosition(); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlayerMessage.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlayerMessage.java new file mode 100644 index 000000000000..69740220e53f --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlayerMessage.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import android.os.Handler; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; + +/** + * Defines a player message which can be sent with a {@link Sender} and received by a {@link + * Target}. + */ +public final class PlayerMessage { + + /** A target for messages. */ + public interface Target { + + /** + * Handles a message delivered to the target. + * + * @param messageType The message type. + * @param payload The message payload. + * @throws ExoPlaybackException If an error occurred whilst handling the message. Should only be + * thrown by targets that handle messages on the playback thread. + */ + void handleMessage(int messageType, @Nullable Object payload) throws ExoPlaybackException; + } + + /** A sender for messages. */ + public interface Sender { + + /** + * Sends a message. + * + * @param message The message to be sent. + */ + void sendMessage(PlayerMessage message); + } + + private final Target target; + private final Sender sender; + private final Timeline timeline; + + private int type; + @Nullable private Object payload; + private Handler handler; + private int windowIndex; + private long positionMs; + private boolean deleteAfterDelivery; + private boolean isSent; + private boolean isDelivered; + private boolean isProcessed; + private boolean isCanceled; + + /** + * Creates a new message. + * + * @param sender The {@link Sender} used to send the message. + * @param target The {@link Target} the message is sent to. + * @param timeline The timeline used when setting the position with {@link #setPosition(long)}. If + * set to {@link Timeline#EMPTY}, any position can be specified. + * @param defaultWindowIndex The default window index in the {@code timeline} when no other window + * index is specified. + * @param defaultHandler The default handler to send the message on when no other handler is + * specified. + */ + public PlayerMessage( + Sender sender, + Target target, + Timeline timeline, + int defaultWindowIndex, + Handler defaultHandler) { + this.sender = sender; + this.target = target; + this.timeline = timeline; + this.handler = defaultHandler; + this.windowIndex = defaultWindowIndex; + this.positionMs = C.TIME_UNSET; + this.deleteAfterDelivery = true; + } + + /** Returns the timeline used for setting the position with {@link #setPosition(long)}. */ + public Timeline getTimeline() { + return timeline; + } + + /** Returns the target the message is sent to. */ + public Target getTarget() { + return target; + } + + /** + * Sets the message type forwarded to {@link Target#handleMessage(int, Object)}. + * + * @param messageType The message type. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setType(int messageType) { + Assertions.checkState(!isSent); + this.type = messageType; + return this; + } + + /** Returns the message type forwarded to {@link Target#handleMessage(int, Object)}. */ + public int getType() { + return type; + } + + /** + * Sets the message payload forwarded to {@link Target#handleMessage(int, Object)}. + * + * @param payload The message payload. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setPayload(@Nullable Object payload) { + Assertions.checkState(!isSent); + this.payload = payload; + return this; + } + + /** Returns the message payload forwarded to {@link Target#handleMessage(int, Object)}. */ + @Nullable + public Object getPayload() { + return payload; + } + + /** + * Sets the handler the message is delivered on. + * + * @param handler A {@link Handler}. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setHandler(Handler handler) { + Assertions.checkState(!isSent); + this.handler = handler; + return this; + } + + /** Returns the handler the message is delivered on. */ + public Handler getHandler() { + return handler; + } + + /** + * Returns position in window at {@link #getWindowIndex()} at which the message will be delivered, + * in milliseconds. If {@link C#TIME_UNSET}, the message will be delivered immediately. + */ + public long getPositionMs() { + return positionMs; + } + + /** + * Sets a position in the current window at which the message will be delivered. + * + * @param positionMs The position in the current window at which the message will be sent, in + * milliseconds. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setPosition(long positionMs) { + Assertions.checkState(!isSent); + this.positionMs = positionMs; + return this; + } + + /** + * Sets a position in a window at which the message will be delivered. + * + * @param windowIndex The index of the window at which the message will be sent. + * @param positionMs The position in the window with index {@code windowIndex} at which the + * message will be sent, in milliseconds. + * @return This message. + * @throws IllegalSeekPositionException If the timeline returned by {@link #getTimeline()} is not + * empty and the provided window index is not within the bounds of the timeline. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setPosition(int windowIndex, long positionMs) { + Assertions.checkState(!isSent); + Assertions.checkArgument(positionMs != C.TIME_UNSET); + if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { + throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); + } + this.windowIndex = windowIndex; + this.positionMs = positionMs; + return this; + } + + /** Returns window index at which the message will be delivered. */ + public int getWindowIndex() { + return windowIndex; + } + + /** + * Sets whether the message will be deleted after delivery. If false, the message will be resent + * if playback reaches the specified position again. Only allowed to be false if a position is set + * with {@link #setPosition(long)}. + * + * @param deleteAfterDelivery Whether the message is deleted after delivery. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setDeleteAfterDelivery(boolean deleteAfterDelivery) { + Assertions.checkState(!isSent); + this.deleteAfterDelivery = deleteAfterDelivery; + return this; + } + + /** Returns whether the message will be deleted after delivery. */ + public boolean getDeleteAfterDelivery() { + return deleteAfterDelivery; + } + + /** + * Sends the message. If the target throws an {@link ExoPlaybackException} then it is propagated + * out of the player as an error using {@link + * Player.EventListener#onPlayerError(ExoPlaybackException)}. + * + * @return This message. + * @throws IllegalStateException If this message has already been sent. + */ + public PlayerMessage send() { + Assertions.checkState(!isSent); + if (positionMs == C.TIME_UNSET) { + Assertions.checkArgument(deleteAfterDelivery); + } + isSent = true; + sender.sendMessage(this); + return this; + } + + /** + * Cancels the message delivery. + * + * @return This message. + * @throws IllegalStateException If this method is called before {@link #send()}. + */ + public synchronized PlayerMessage cancel() { + Assertions.checkState(isSent); + isCanceled = true; + markAsProcessed(/* isDelivered= */ false); + return this; + } + + /** Returns whether the message delivery has been canceled. */ + public synchronized boolean isCanceled() { + return isCanceled; + } + + /** + * Blocks until after the message has been delivered or the player is no longer able to deliver + * the message. + * + *

Note that this method can't be called if the current thread is the same thread used by the + * message handler set with {@link #setHandler(Handler)} as it would cause a deadlock. + * + * @return Whether the message was delivered successfully. + * @throws IllegalStateException If this method is called before {@link #send()}. + * @throws IllegalStateException If this method is called on the same thread used by the message + * handler set with {@link #setHandler(Handler)}. + * @throws InterruptedException If the current thread is interrupted while waiting for the message + * to be delivered. + */ + public synchronized boolean blockUntilDelivered() throws InterruptedException { + Assertions.checkState(isSent); + Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread()); + while (!isProcessed) { + wait(); + } + return isDelivered; + } + + /** + * Marks the message as processed. Should only be called by a {@link Sender} and may be called + * multiple times. + * + * @param isDelivered Whether the message has been delivered to its target. The message is + * considered as being delivered when this method has been called with {@code isDelivered} set + * to true at least once. + */ + public synchronized void markAsProcessed(boolean isDelivered) { + this.isDelivered |= isDelivered; + isProcessed = true; + notifyAll(); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Renderer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Renderer.java index 152ad33af506..d06afb5d3c04 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Renderer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Renderer.java @@ -15,30 +15,47 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2; -import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleStream; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MediaClock; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Renders media read from a {@link SampleStream}. - *

- * Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is - * transitioned through various states as the overall playback state changes. The valid state - * transitions are shown below, annotated with the methods that are called during each transition. - *

- * Renderer state transitions - *

+ * + *

Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is + * transitioned through various states as the overall playback state and enabled tracks change. The + * valid state transitions are shown below, annotated with the methods that are called during each + * transition. + * + *

Renderer state
+ * transitions */ -public interface Renderer extends ExoPlayerComponent { +public interface Renderer extends PlayerMessage.Target { /** - * The renderer is disabled. + * The renderer states. One of {@link #STATE_DISABLED}, {@link #STATE_ENABLED} or {@link + * #STATE_STARTED}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_DISABLED, STATE_ENABLED, STATE_STARTED}) + @interface State {} + /** + * The renderer is disabled. A renderer in this state may hold resources that it requires for + * rendering (e.g. media decoders), for use if it's subsequently enabled. {@link #reset()} can be + * called to force the renderer to release these resources. */ int STATE_DISABLED = 0; /** - * The renderer is enabled but not started. A renderer in this state is not actively rendering - * media, but will typically hold resources that it requires for rendering (e.g. media decoders). + * The renderer is enabled but not started. A renderer in this state may render media at the + * current position (e.g. an initial video frame), but the position will not advance. A renderer + * in this state will typically hold resources that it requires for rendering (e.g. media + * decoders). */ int STATE_ENABLED = 1; /** @@ -72,18 +89,21 @@ public interface Renderer extends ExoPlayerComponent { /** * If the renderer advances its own playback position then this method returns a corresponding * {@link MediaClock}. If provided, the player will use the returned {@link MediaClock} as its - * source of time during playback. A player may have at most one renderer that returns a - * {@link MediaClock} from this method. + * source of time during playback. A player may have at most one renderer that returns a {@link + * MediaClock} from this method. * * @return The {@link MediaClock} tracking the playback position of the renderer, or null. */ + @Nullable MediaClock getMediaClock(); /** * Returns the current state of the renderer. * - * @return The current state (one of the {@code STATE_*} constants). + * @return The current state. One of {@link #STATE_DISABLED}, {@link #STATE_ENABLED} and {@link + * #STATE_STARTED}. */ + @State int getState(); /** @@ -130,9 +150,8 @@ public interface Renderer extends ExoPlayerComponent { void replaceStream(Format[] formats, SampleStream stream, long offsetUs) throws ExoPlaybackException; - /** - * Returns the {@link SampleStream} being consumed, or null if the renderer is disabled. - */ + /** Returns the {@link SampleStream} being consumed, or null if the renderer is disabled. */ + @Nullable SampleStream getStream(); /** @@ -143,6 +162,16 @@ public interface Renderer extends ExoPlayerComponent { */ boolean hasReadStreamToEnd(); + /** + * Returns the playback position up to which the renderer has read samples from the current {@link + * SampleStream}, in microseconds, or {@link C#TIME_END_OF_SOURCE} if the renderer has read the + * current {@link SampleStream} to the end. + * + *

This method may be called when the renderer is in the following states: {@link + * #STATE_ENABLED}, {@link #STATE_STARTED}. + */ + long getReadingPositionUs(); + /** * Signals to the renderer that the current {@link SampleStream} will be the final one supplied * before it is next disabled or reset. @@ -184,6 +213,18 @@ public interface Renderer extends ExoPlayerComponent { */ void resetPosition(long positionUs) throws ExoPlaybackException; + /** + * Sets the operating rate of this renderer, where 1 is the default rate, 2 is twice the default + * rate, 0.5 is half the default rate and so on. The operating rate is a hint to the renderer of + * the speed at which playback will proceed, and may be used for resource planning. + * + *

The default implementation is a no-op. + * + * @param operatingRate The operating rate. + * @throws ExoPlaybackException If an error occurs handling the operating rate. + */ + default void setOperatingRate(float operatingRate) throws ExoPlaybackException {} + /** * Incrementally renders the {@link SampleStream}. *

@@ -226,7 +267,7 @@ public interface Renderer extends ExoPlayerComponent { /** * Whether the renderer is ready for the {@link ExoPlayer} instance to transition to - * {@link ExoPlayer#STATE_ENDED}. The player will make this transition as soon as {@code true} is + * {@link Player#STATE_ENDED}. The player will make this transition as soon as {@code true} is * returned by all of its {@link Renderer}s. *

* This method may be called when the renderer is in the following states: @@ -254,4 +295,12 @@ public interface Renderer extends ExoPlayerComponent { */ void disable(); + /** + * Forces the renderer to give up any resources (e.g. media decoders) that it may be holding. If + * the renderer is not holding any resources, the call is a no-op. + * + *

This method may be called when the renderer is in the following states: {@link + * #STATE_DISABLED}. + */ + void reset(); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RendererCapabilities.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RendererCapabilities.java index 061146d9ca0c..6f34afc7b83b 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RendererCapabilities.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RendererCapabilities.java @@ -15,7 +15,12 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2; +import android.annotation.SuppressLint; +import androidx.annotation.IntDef; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Defines the capabilities of a {@link Renderer}. @@ -23,24 +28,47 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; public interface RendererCapabilities { /** - * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, - * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. + * Level of renderer support for a format. One of {@link #FORMAT_HANDLED}, {@link + * #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, {@link + * #FORMAT_UNSUPPORTED_SUBTYPE} or {@link #FORMAT_UNSUPPORTED_TYPE}. */ - int FORMAT_SUPPORT_MASK = 0b11; + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + FORMAT_HANDLED, + FORMAT_EXCEEDS_CAPABILITIES, + FORMAT_UNSUPPORTED_DRM, + FORMAT_UNSUPPORTED_SUBTYPE, + FORMAT_UNSUPPORTED_TYPE + }) + @interface FormatSupport {} + + /** A mask to apply to {@link Capabilities} to obtain the {@link FormatSupport} only. */ + int FORMAT_SUPPORT_MASK = 0b111; /** * The {@link Renderer} is capable of rendering the format. */ - int FORMAT_HANDLED = 0b11; + int FORMAT_HANDLED = 0b100; /** * The {@link Renderer} is capable of rendering formats with the same mime type, but the - * properties of the format exceed the renderer's capability. + * properties of the format exceed the renderer's capabilities. There is a chance the renderer + * will be able to play the format in practice because some renderers report their capabilities + * conservatively, but the expected outcome is that playback will fail. *

* Example: The {@link Renderer} is capable of rendering H264 and the format's mime type is * {@link MimeTypes#VIDEO_H264}, but the format's resolution exceeds the maximum limit supported * by the underlying H264 decoder. */ - int FORMAT_EXCEEDS_CAPABILITIES = 0b10; + int FORMAT_EXCEEDS_CAPABILITIES = 0b011; + /** + * The {@link Renderer} is capable of rendering formats with the same mime type, but is not + * capable of rendering the format because the format's drm protection is not supported. + *

+ * Example: The {@link Renderer} is capable of rendering H264 and the format's mime type is + * {@link MimeTypes#VIDEO_H264}, but the format indicates PlayReady drm protection where-as the + * renderer only supports Widevine. + */ + int FORMAT_UNSUPPORTED_DRM = 0b010; /** * The {@link Renderer} is a general purpose renderer for formats of the same top-level type, * but is not capable of rendering the format or any other format with the same mime type because @@ -49,7 +77,7 @@ public interface RendererCapabilities { * Example: The {@link Renderer} is a general purpose audio renderer and the format's * mime type matches audio/[subtype], but there does not exist a suitable decoder for [subtype]. */ - int FORMAT_UNSUPPORTED_SUBTYPE = 0b01; + int FORMAT_UNSUPPORTED_SUBTYPE = 0b001; /** * The {@link Renderer} is not capable of rendering the format, either because it does not * support the format's top-level type, or because it's a specialized renderer for a different @@ -58,40 +86,179 @@ public interface RendererCapabilities { * Example: The {@link Renderer} is a general purpose video renderer, but the format has an * audio mime type. */ - int FORMAT_UNSUPPORTED_TYPE = 0b00; + int FORMAT_UNSUPPORTED_TYPE = 0b000; /** - * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}. + * Level of renderer support for adaptive format switches. One of {@link #ADAPTIVE_SEAMLESS}, + * {@link #ADAPTIVE_NOT_SEAMLESS} or {@link #ADAPTIVE_NOT_SUPPORTED}. */ - int ADAPTIVE_SUPPORT_MASK = 0b1100; + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ADAPTIVE_SEAMLESS, ADAPTIVE_NOT_SEAMLESS, ADAPTIVE_NOT_SUPPORTED}) + @interface AdaptiveSupport {} + + /** A mask to apply to {@link Capabilities} to obtain the {@link AdaptiveSupport} only. */ + int ADAPTIVE_SUPPORT_MASK = 0b11000; /** * The {@link Renderer} can seamlessly adapt between formats. */ - int ADAPTIVE_SEAMLESS = 0b1000; + int ADAPTIVE_SEAMLESS = 0b10000; /** * The {@link Renderer} can adapt between formats, but may suffer a brief discontinuity * (~50-100ms) when adaptation occurs. */ - int ADAPTIVE_NOT_SEAMLESS = 0b0100; + int ADAPTIVE_NOT_SEAMLESS = 0b01000; /** * The {@link Renderer} does not support adaptation between formats. */ - int ADAPTIVE_NOT_SUPPORTED = 0b0000; + int ADAPTIVE_NOT_SUPPORTED = 0b00000; /** - * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. + * Level of renderer support for tunneling. One of {@link #TUNNELING_SUPPORTED} or {@link + * #TUNNELING_NOT_SUPPORTED}. */ - int TUNNELING_SUPPORT_MASK = 0b10000; + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({TUNNELING_SUPPORTED, TUNNELING_NOT_SUPPORTED}) + @interface TunnelingSupport {} + + /** A mask to apply to {@link Capabilities} to obtain the {@link TunnelingSupport} only. */ + int TUNNELING_SUPPORT_MASK = 0b100000; /** * The {@link Renderer} supports tunneled output. */ - int TUNNELING_SUPPORTED = 0b10000; + int TUNNELING_SUPPORTED = 0b100000; /** * The {@link Renderer} does not support tunneled output. */ - int TUNNELING_NOT_SUPPORTED = 0b00000; + int TUNNELING_NOT_SUPPORTED = 0b000000; + + /** + * Combined renderer capabilities. + * + *

This is a bitwise OR of {@link FormatSupport}, {@link AdaptiveSupport} and {@link + * TunnelingSupport}. Use {@link #getFormatSupport(int)}, {@link #getAdaptiveSupport(int)} or + * {@link #getTunnelingSupport(int)} to obtain the individual flags. And use {@link #create(int)} + * or {@link #create(int, int, int)} to create the combined capabilities. + * + *

Possible values: + * + *

+ */ + @Documented + @Retention(RetentionPolicy.SOURCE) + // Intentionally empty to prevent assignment or comparison with individual flags without masking. + @IntDef({}) + @interface Capabilities {} + + /** + * Returns {@link Capabilities} for the given {@link FormatSupport}. + * + *

The {@link AdaptiveSupport} is set to {@link #ADAPTIVE_NOT_SUPPORTED} and {{@link + * TunnelingSupport} is set to {@link #TUNNELING_NOT_SUPPORTED}. + * + * @param formatSupport The {@link FormatSupport}. + * @return The combined {@link Capabilities} of the given {@link FormatSupport}, {@link + * #ADAPTIVE_NOT_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. + */ + @Capabilities + static int create(@FormatSupport int formatSupport) { + return create(formatSupport, ADAPTIVE_NOT_SUPPORTED, TUNNELING_NOT_SUPPORTED); + } + + /** + * Returns {@link Capabilities} combining the given {@link FormatSupport}, {@link AdaptiveSupport} + * and {@link TunnelingSupport}. + * + * @param formatSupport The {@link FormatSupport}. + * @param adaptiveSupport The {@link AdaptiveSupport}. + * @param tunnelingSupport The {@link TunnelingSupport}. + * @return The combined {@link Capabilities}. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @Capabilities + static int create( + @FormatSupport int formatSupport, + @AdaptiveSupport int adaptiveSupport, + @TunnelingSupport int tunnelingSupport) { + return formatSupport | adaptiveSupport | tunnelingSupport; + } + + /** + * Returns the {@link FormatSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link FormatSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @FormatSupport + static int getFormatSupport(@Capabilities int supportFlags) { + return supportFlags & FORMAT_SUPPORT_MASK; + } + + /** + * Returns the {@link AdaptiveSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link AdaptiveSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @AdaptiveSupport + static int getAdaptiveSupport(@Capabilities int supportFlags) { + return supportFlags & ADAPTIVE_SUPPORT_MASK; + } + + /** + * Returns the {@link TunnelingSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link TunnelingSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @TunnelingSupport + static int getTunnelingSupport(@Capabilities int supportFlags) { + return supportFlags & TUNNELING_SUPPORT_MASK; + } + + /** + * Returns string representation of a {@link FormatSupport} flag. + * + * @param formatSupport A {@link FormatSupport} flag. + * @return A string representation of the flag. + */ + static String getFormatSupportString(@FormatSupport int formatSupport) { + switch (formatSupport) { + case RendererCapabilities.FORMAT_HANDLED: + return "YES"; + case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: + return "NO_EXCEEDS_CAPABILITIES"; + case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: + return "NO_UNSUPPORTED_DRM"; + case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: + return "NO_UNSUPPORTED_TYPE"; + case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: + return "NO"; + default: + throw new IllegalStateException(); + } + } /** * Returns the track type that the {@link Renderer} handles. For example, a video renderer will @@ -104,37 +271,23 @@ public interface RendererCapabilities { int getTrackType(); /** - * Returns the extent to which the {@link Renderer} supports a given format. The returned value is - * the bitwise OR of three properties: - *

- * The individual properties can be retrieved by performing a bitwise AND with - * {@link #FORMAT_SUPPORT_MASK}, {@link #ADAPTIVE_SUPPORT_MASK} and - * {@link #TUNNELING_SUPPORT_MASK} respectively. + * Returns the extent to which the {@link Renderer} supports a given format. * * @param format The format. - * @return The extent to which the renderer is capable of supporting the given format. + * @return The {@link Capabilities} for this format. * @throws ExoPlaybackException If an error occurs. */ + @Capabilities int supportsFormat(Format format) throws ExoPlaybackException; /** * Returns the extent to which the {@link Renderer} supports adapting between supported formats - * that have different mime types. + * that have different MIME types. * - * @return The extent to which the renderer supports adapting between supported formats that have - * different mime types. One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and - * {@link #ADAPTIVE_NOT_SUPPORTED}. + * @return The {@link AdaptiveSupport} for adapting between supported formats that have different + * MIME types. * @throws ExoPlaybackException If an error occurs. */ + @AdaptiveSupport int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException; - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RendererConfiguration.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RendererConfiguration.java index ccbc03cbc835..d12e2b9fb6c1 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RendererConfiguration.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RendererConfiguration.java @@ -15,6 +15,8 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2; +import androidx.annotation.Nullable; + /** * The configuration of a {@link Renderer}. */ @@ -41,7 +43,7 @@ public final class RendererConfiguration { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RenderersFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RenderersFactory.java index 9043c7a7cfc3..ed46d27fa3b9 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RenderersFactory.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RenderersFactory.java @@ -16,9 +16,12 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2; import android.os.Handler; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioRendererEventListener; -import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataRenderer; -import org.mozilla.thirdparty.com.google.android.exoplayer2.text.TextRenderer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.TextOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoRendererEventListener; /** @@ -31,14 +34,17 @@ public interface RenderersFactory { * * @param eventHandler A handler to use when invoking event listeners and outputs. * @param videoRendererEventListener An event listener for video renderers. - * @param videoRendererEventListener An event listener for audio renderers. + * @param audioRendererEventListener An event listener for audio renderers. * @param textRendererOutput An output for text renderers. * @param metadataRendererOutput An output for metadata renderers. + * @param drmSessionManager A drm session manager used by renderers. * @return The {@link Renderer instances}. */ - Renderer[] createRenderers(Handler eventHandler, + Renderer[] createRenderers( + Handler eventHandler, VideoRendererEventListener videoRendererEventListener, AudioRendererEventListener audioRendererEventListener, - TextRenderer.Output textRendererOutput, MetadataRenderer.Output metadataRendererOutput); - + TextOutput textRendererOutput, + MetadataOutput metadataRendererOutput, + @Nullable DrmSessionManager drmSessionManager); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SeekParameters.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SeekParameters.java new file mode 100644 index 000000000000..03c1d0165dfe --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SeekParameters.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; + +/** + * Parameters that apply to seeking. + * + *

The predefined {@link #EXACT}, {@link #CLOSEST_SYNC}, {@link #PREVIOUS_SYNC} and {@link + * #NEXT_SYNC} parameters are suitable for most use cases. Seeking to sync points is typically + * faster but less accurate than exact seeking. + * + *

In the general case, an instance specifies a maximum tolerance before ({@link + * #toleranceBeforeUs}) and after ({@link #toleranceAfterUs}) a requested seek position ({@code x}). + * If one or more sync points falls within the window {@code [x - toleranceBeforeUs, x + + * toleranceAfterUs]} then the seek will be performed to the sync point within the window that's + * closest to {@code x}. If no sync point falls within the window then the seek will be performed to + * {@code x - toleranceBeforeUs}. Internally the player may need to seek to an earlier sync point + * and discard media until this position is reached. + */ +public final class SeekParameters { + + /** Parameters for exact seeking. */ + public static final SeekParameters EXACT = new SeekParameters(0, 0); + /** Parameters for seeking to the closest sync point. */ + public static final SeekParameters CLOSEST_SYNC = + new SeekParameters(Long.MAX_VALUE, Long.MAX_VALUE); + /** Parameters for seeking to the sync point immediately before a requested seek position. */ + public static final SeekParameters PREVIOUS_SYNC = new SeekParameters(Long.MAX_VALUE, 0); + /** Parameters for seeking to the sync point immediately after a requested seek position. */ + public static final SeekParameters NEXT_SYNC = new SeekParameters(0, Long.MAX_VALUE); + /** Default parameters. */ + public static final SeekParameters DEFAULT = EXACT; + + /** + * The maximum time that the actual position seeked to may precede the requested seek position, in + * microseconds. + */ + public final long toleranceBeforeUs; + /** + * The maximum time that the actual position seeked to may exceed the requested seek position, in + * microseconds. + */ + public final long toleranceAfterUs; + + /** + * @param toleranceBeforeUs The maximum time that the actual position seeked to may precede the + * requested seek position, in microseconds. Must be non-negative. + * @param toleranceAfterUs The maximum time that the actual position seeked to may exceed the + * requested seek position, in microseconds. Must be non-negative. + */ + public SeekParameters(long toleranceBeforeUs, long toleranceAfterUs) { + Assertions.checkArgument(toleranceBeforeUs >= 0); + Assertions.checkArgument(toleranceAfterUs >= 0); + this.toleranceBeforeUs = toleranceBeforeUs; + this.toleranceAfterUs = toleranceAfterUs; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SeekParameters other = (SeekParameters) obj; + return toleranceBeforeUs == other.toleranceBeforeUs + && toleranceAfterUs == other.toleranceAfterUs; + } + + @Override + public int hashCode() { + return (31 * (int) toleranceBeforeUs) + (int) toleranceAfterUs; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SimpleExoPlayer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SimpleExoPlayer.java index f4ed5b28238b..7b632ed0510f 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -16,330 +16,829 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2; import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.media.MediaCodec; import android.media.PlaybackParams; import android.os.Handler; -import android.support.annotation.Nullable; -import android.util.Log; +import android.os.Looper; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsCollector; +import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioAttributes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioListener; import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioRendererEventListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AuxEffectInfo; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderCounters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; -import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataRenderer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; -import org.mozilla.thirdparty.com.google.android.exoplayer2.text.TextRenderer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.TextOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelector; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.PriorityTaskManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoFrameMetadataListener; import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoRendererEventListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.spherical.CameraMotionListener; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.concurrent.CopyOnWriteArraySet; /** * An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can - * be obtained from {@link ExoPlayerFactory}. + * be obtained from {@link SimpleExoPlayer.Builder}. */ -@TargetApi(16) -public class SimpleExoPlayer implements ExoPlayer { +public class SimpleExoPlayer extends BasePlayer + implements ExoPlayer, + Player.AudioComponent, + Player.VideoComponent, + Player.TextComponent, + Player.MetadataComponent { + + /** @deprecated Use {@link org.mozilla.thirdparty.com.google.android.exoplayer2video.VideoListener}. */ + @Deprecated + public interface VideoListener extends org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener {} /** - * A listener for video rendering information from a {@link SimpleExoPlayer}. + * A builder for {@link SimpleExoPlayer} instances. + * + *

See {@link #Builder(Context)} for the list of default values. */ - public interface VideoListener { + public static final class Builder { + + private final Context context; + private final RenderersFactory renderersFactory; + + private Clock clock; + private TrackSelector trackSelector; + private LoadControl loadControl; + private BandwidthMeter bandwidthMeter; + private AnalyticsCollector analyticsCollector; + private Looper looper; + private boolean useLazyPreparation; + private boolean buildCalled; /** - * Called each time there's a change in the size of the video being rendered. + * Creates a builder. * - * @param width The video width in pixels. - * @param height The video height in pixels. - * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise - * rotation in degrees that the application should apply for the video for it to be rendered - * in the correct orientation. This value will always be zero on API levels 21 and above, - * since the renderer will apply all necessary rotations internally. On earlier API levels - * this is not possible. Applications that use {@link android.view.TextureView} can apply - * the rotation by calling {@link android.view.TextureView#setTransform}. Applications that - * do not expect to encounter rotated videos can safely ignore this parameter. - * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case - * of square pixels this will be equal to 1.0. Different values are indicative of anamorphic - * content. + *

Use {@link #Builder(Context, RenderersFactory)} instead, if you intend to provide a custom + * {@link RenderersFactory}. This is to ensure that ProGuard or R8 can remove ExoPlayer's {@link + * DefaultRenderersFactory} from the APK. + * + *

The builder uses the following default values: + * + *

+ * + * @param context A {@link Context}. */ - void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio); + public Builder(Context context) { + this(context, new DefaultRenderersFactory(context)); + } /** - * Called when a frame is rendered for the first time since setting the surface, and when a - * frame is rendered for the first time since a video track was selected. + * Creates a builder with a custom {@link RenderersFactory}. + * + *

See {@link #Builder(Context)} for a list of default values. + * + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer Renderers} to be used by the + * player. */ - void onRenderedFirstFrame(); + public Builder(Context context, RenderersFactory renderersFactory) { + this( + context, + renderersFactory, + new DefaultTrackSelector(context), + new DefaultLoadControl(), + DefaultBandwidthMeter.getSingletonInstance(context), + Util.getLooper(), + new AnalyticsCollector(Clock.DEFAULT), + /* useLazyPreparation= */ true, + Clock.DEFAULT); + } + /** + * Creates a builder with the specified custom components. + * + *

Note that this constructor is only useful if you try to ensure that ExoPlayer's default + * components can be removed by ProGuard or R8. For most components except renderers, there is + * only a marginal benefit of doing that. + * + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer Renderers} to be used by the + * player. + * @param trackSelector A {@link TrackSelector}. + * @param loadControl A {@link LoadControl}. + * @param bandwidthMeter A {@link BandwidthMeter}. + * @param looper A {@link Looper} that must be used for all calls to the player. + * @param analyticsCollector An {@link AnalyticsCollector}. + * @param useLazyPreparation Whether media sources should be initialized lazily. + * @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}. + */ + public Builder( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + Looper looper, + AnalyticsCollector analyticsCollector, + boolean useLazyPreparation, + Clock clock) { + this.context = context; + this.renderersFactory = renderersFactory; + this.trackSelector = trackSelector; + this.loadControl = loadControl; + this.bandwidthMeter = bandwidthMeter; + this.looper = looper; + this.analyticsCollector = analyticsCollector; + this.useLazyPreparation = useLazyPreparation; + this.clock = clock; + } + + /** + * Sets the {@link TrackSelector} that will be used by the player. + * + * @param trackSelector A {@link TrackSelector}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setTrackSelector(TrackSelector trackSelector) { + Assertions.checkState(!buildCalled); + this.trackSelector = trackSelector; + return this; + } + + /** + * Sets the {@link LoadControl} that will be used by the player. + * + * @param loadControl A {@link LoadControl}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setLoadControl(LoadControl loadControl) { + Assertions.checkState(!buildCalled); + this.loadControl = loadControl; + return this; + } + + /** + * Sets the {@link BandwidthMeter} that will be used by the player. + * + * @param bandwidthMeter A {@link BandwidthMeter}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setBandwidthMeter(BandwidthMeter bandwidthMeter) { + Assertions.checkState(!buildCalled); + this.bandwidthMeter = bandwidthMeter; + return this; + } + + /** + * Sets the {@link Looper} that must be used for all calls to the player and that is used to + * call listeners on. + * + * @param looper A {@link Looper}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setLooper(Looper looper) { + Assertions.checkState(!buildCalled); + this.looper = looper; + return this; + } + + /** + * Sets the {@link AnalyticsCollector} that will collect and forward all player events. + * + * @param analyticsCollector An {@link AnalyticsCollector}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setAnalyticsCollector(AnalyticsCollector analyticsCollector) { + Assertions.checkState(!buildCalled); + this.analyticsCollector = analyticsCollector; + return this; + } + + /** + * Sets whether media sources should be initialized lazily. + * + *

If false, all initial preparation steps (e.g., manifest loads) happen immediately. If + * true, these initial preparations are triggered only when the player starts buffering the + * media. + * + * @param useLazyPreparation Whether to use lazy preparation. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setUseLazyPreparation(boolean useLazyPreparation) { + Assertions.checkState(!buildCalled); + this.useLazyPreparation = useLazyPreparation; + return this; + } + + /** + * Sets the {@link Clock} that will be used by the player. Should only be set for testing + * purposes. + * + * @param clock A {@link Clock}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + @VisibleForTesting + public Builder setClock(Clock clock) { + Assertions.checkState(!buildCalled); + this.clock = clock; + return this; + } + + /** + * Builds a {@link SimpleExoPlayer} instance. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public SimpleExoPlayer build() { + Assertions.checkState(!buildCalled); + buildCalled = true; + return new SimpleExoPlayer( + context, + renderersFactory, + trackSelector, + loadControl, + bandwidthMeter, + analyticsCollector, + clock, + looper); + } } private static final String TAG = "SimpleExoPlayer"; protected final Renderer[] renderers; - private final ExoPlayer player; + private final ExoPlayerImpl player; + private final Handler eventHandler; private final ComponentListener componentListener; - private final int videoRendererCount; - private final int audioRendererCount; + private final CopyOnWriteArraySet + videoListeners; + private final CopyOnWriteArraySet audioListeners; + private final CopyOnWriteArraySet textOutputs; + private final CopyOnWriteArraySet metadataOutputs; + private final CopyOnWriteArraySet videoDebugListeners; + private final CopyOnWriteArraySet audioDebugListeners; + private final BandwidthMeter bandwidthMeter; + private final AnalyticsCollector analyticsCollector; - private Format videoFormat; - private Format audioFormat; + private final AudioBecomingNoisyManager audioBecomingNoisyManager; + private final AudioFocusManager audioFocusManager; + private final WakeLockManager wakeLockManager; + private final WifiLockManager wifiLockManager; - private Surface surface; + @Nullable private Format videoFormat; + @Nullable private Format audioFormat; + + @Nullable private VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer; + @Nullable private Surface surface; private boolean ownsSurface; - @C.VideoScalingMode - private int videoScalingMode; - private SurfaceHolder surfaceHolder; - private TextureView textureView; - private TextRenderer.Output textOutput; - private MetadataRenderer.Output metadataOutput; - private VideoListener videoListener; - private AudioRendererEventListener audioDebugListener; - private VideoRendererEventListener videoDebugListener; - private DecoderCounters videoDecoderCounters; - private DecoderCounters audioDecoderCounters; + private @C.VideoScalingMode int videoScalingMode; + @Nullable private SurfaceHolder surfaceHolder; + @Nullable private TextureView textureView; + private int surfaceWidth; + private int surfaceHeight; + @Nullable private DecoderCounters videoDecoderCounters; + @Nullable private DecoderCounters audioDecoderCounters; private int audioSessionId; - @C.StreamType - private int audioStreamType; + private AudioAttributes audioAttributes; private float audioVolume; + @Nullable private MediaSource mediaSource; + private List currentCues; + @Nullable private VideoFrameMetadataListener videoFrameMetadataListener; + @Nullable private CameraMotionListener cameraMotionListener; + private boolean hasNotifiedFullWrongThreadWarning; + @Nullable private PriorityTaskManager priorityTaskManager; + private boolean isPriorityTaskManagerRegistered; + private boolean playerReleased; - protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, - LoadControl loadControl) { + /** + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. + * @param analyticsCollector A factory for creating the {@link AnalyticsCollector} that will + * collect and forward all player events. + * @param clock The {@link Clock} that will be used by the instance. Should always be {@link + * Clock#DEFAULT}, unless the player is being used from a test. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. + */ + @SuppressWarnings("deprecation") + protected SimpleExoPlayer( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + AnalyticsCollector analyticsCollector, + Clock clock, + Looper looper) { + this( + context, + renderersFactory, + trackSelector, + loadControl, + DrmSessionManager.getDummyDrmSessionManager(), + bandwidthMeter, + analyticsCollector, + clock, + looper); + } + + /** + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. + * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. + * @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all + * player events. + * @param clock The {@link Clock} that will be used by the instance. Should always be {@link + * Clock#DEFAULT}, unless the player is being used from a test. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. + * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl, + * BandwidthMeter, AnalyticsCollector, Clock, Looper)} instead, and pass the {@link + * DrmSessionManager} to the {@link MediaSource} factories. + */ + @Deprecated + protected SimpleExoPlayer( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, + BandwidthMeter bandwidthMeter, + AnalyticsCollector analyticsCollector, + Clock clock, + Looper looper) { + this.bandwidthMeter = bandwidthMeter; + this.analyticsCollector = analyticsCollector; componentListener = new ComponentListener(); - renderers = renderersFactory.createRenderers(new Handler(), componentListener, - componentListener, componentListener, componentListener); - - // Obtain counts of video and audio renderers. - int videoRendererCount = 0; - int audioRendererCount = 0; - for (Renderer renderer : renderers) { - switch (renderer.getTrackType()) { - case C.TRACK_TYPE_VIDEO: - videoRendererCount++; - break; - case C.TRACK_TYPE_AUDIO: - audioRendererCount++; - break; - } - } - this.videoRendererCount = videoRendererCount; - this.audioRendererCount = audioRendererCount; + videoListeners = new CopyOnWriteArraySet<>(); + audioListeners = new CopyOnWriteArraySet<>(); + textOutputs = new CopyOnWriteArraySet<>(); + metadataOutputs = new CopyOnWriteArraySet<>(); + videoDebugListeners = new CopyOnWriteArraySet<>(); + audioDebugListeners = new CopyOnWriteArraySet<>(); + eventHandler = new Handler(looper); + renderers = + renderersFactory.createRenderers( + eventHandler, + componentListener, + componentListener, + componentListener, + componentListener, + drmSessionManager); // Set initial values. audioVolume = 1; audioSessionId = C.AUDIO_SESSION_ID_UNSET; - audioStreamType = C.STREAM_TYPE_DEFAULT; + audioAttributes = AudioAttributes.DEFAULT; videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; + currentCues = Collections.emptyList(); // Build the player and associated objects. - player = new ExoPlayerImpl(renderers, trackSelector, loadControl); + player = + new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); + analyticsCollector.setPlayer(player); + player.addListener(analyticsCollector); + player.addListener(componentListener); + videoDebugListeners.add(analyticsCollector); + videoListeners.add(analyticsCollector); + audioDebugListeners.add(analyticsCollector); + audioListeners.add(analyticsCollector); + addMetadataOutput(analyticsCollector); + bandwidthMeter.addEventListener(eventHandler, analyticsCollector); + if (drmSessionManager instanceof DefaultDrmSessionManager) { + ((DefaultDrmSessionManager) drmSessionManager).addListener(eventHandler, analyticsCollector); + } + audioBecomingNoisyManager = + new AudioBecomingNoisyManager(context, eventHandler, componentListener); + audioFocusManager = new AudioFocusManager(context, eventHandler, componentListener); + wakeLockManager = new WakeLockManager(context); + wifiLockManager = new WifiLockManager(context); + } + + @Override + @Nullable + public AudioComponent getAudioComponent() { + return this; + } + + @Override + @Nullable + public VideoComponent getVideoComponent() { + return this; + } + + @Override + @Nullable + public TextComponent getTextComponent() { + return this; + } + + @Override + @Nullable + public MetadataComponent getMetadataComponent() { + return this; } /** * Sets the video scaling mode. - *

- * Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer} is - * enabled and if the output surface is owned by a {@link android.view.SurfaceView}. + * + *

Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer} + * is enabled and if the output surface is owned by a {@link android.view.SurfaceView}. * * @param videoScalingMode The video scaling mode. */ + @Override public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) { + verifyApplicationThread(); this.videoScalingMode = videoScalingMode; - ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; - int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SCALING_MODE, - videoScalingMode); + player + .createMessage(renderer) + .setType(C.MSG_SET_SCALING_MODE) + .setPayload(videoScalingMode) + .send(); } } - player.sendMessages(messages); } - /** - * Returns the video scaling mode. - */ + @Override public @C.VideoScalingMode int getVideoScalingMode() { return videoScalingMode; } - /** - * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView} - * currently set on the player. - */ + @Override public void clearVideoSurface() { - setVideoSurface(null); - } - - /** - * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for - * tracking the lifecycle of the surface, and must clear the surface by calling - * {@code setVideoSurface(null)} if the surface is destroyed. - *

- * If the surface is held by a {@link SurfaceView}, {@link TextureView} or {@link SurfaceHolder} - * then it's recommended to use {@link #setVideoSurfaceView(SurfaceView)}, - * {@link #setVideoTextureView(TextureView)} or {@link #setVideoSurfaceHolder(SurfaceHolder)} - * rather than this method, since passing the holder allows the player to track the lifecycle of - * the surface automatically. - * - * @param surface The {@link Surface}. - */ - public void setVideoSurface(Surface surface) { + verifyApplicationThread(); removeSurfaceCallbacks(); - setVideoSurfaceInternal(surface, false); + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } - /** - * Clears the {@link Surface} onto which video is being rendered if it matches the one passed. - * Else does nothing. - * - * @param surface The surface to clear. - */ - public void clearVideoSurface(Surface surface) { + @Override + public void clearVideoSurface(@Nullable Surface surface) { + verifyApplicationThread(); if (surface != null && surface == this.surface) { - setVideoSurface(null); + clearVideoSurface(); } } - /** - * Sets the {@link SurfaceHolder} that holds the {@link Surface} onto which video will be - * rendered. The player will track the lifecycle of the surface automatically. - * - * @param surfaceHolder The surface holder. - */ - public void setVideoSurfaceHolder(SurfaceHolder surfaceHolder) { + @Override + public void setVideoSurface(@Nullable Surface surface) { + verifyApplicationThread(); removeSurfaceCallbacks(); + if (surface != null) { + clearVideoDecoderOutputBufferRenderer(); + } + setVideoSurfaceInternal(surface, /* ownsSurface= */ false); + int newSurfaceSize = surface == null ? 0 : C.LENGTH_UNSET; + maybeNotifySurfaceSizeChanged(/* width= */ newSurfaceSize, /* height= */ newSurfaceSize); + } + + @Override + public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { + verifyApplicationThread(); + removeSurfaceCallbacks(); + if (surfaceHolder != null) { + clearVideoDecoderOutputBufferRenderer(); + } this.surfaceHolder = surfaceHolder; if (surfaceHolder == null) { - setVideoSurfaceInternal(null, false); + setVideoSurfaceInternal(null, /* ownsSurface= */ false); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } else { - setVideoSurfaceInternal(surfaceHolder.getSurface(), false); surfaceHolder.addCallback(componentListener); + Surface surface = surfaceHolder.getSurface(); + if (surface != null && surface.isValid()) { + setVideoSurfaceInternal(surface, /* ownsSurface= */ false); + Rect surfaceSize = surfaceHolder.getSurfaceFrame(); + maybeNotifySurfaceSizeChanged(surfaceSize.width(), surfaceSize.height()); + } else { + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + } } } - /** - * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being - * rendered if it matches the one passed. Else does nothing. - * - * @param surfaceHolder The surface holder to clear. - */ - public void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder) { + @Override + public void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { + verifyApplicationThread(); if (surfaceHolder != null && surfaceHolder == this.surfaceHolder) { setVideoSurfaceHolder(null); } } - /** - * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the - * lifecycle of the surface automatically. - * - * @param surfaceView The surface view. - */ - public void setVideoSurfaceView(SurfaceView surfaceView) { + @Override + public void setVideoSurfaceView(@Nullable SurfaceView surfaceView) { setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); } - /** - * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one passed. - * Else does nothing. - * - * @param surfaceView The texture view to clear. - */ - public void clearVideoSurfaceView(SurfaceView surfaceView) { + @Override + public void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) { clearVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); } - /** - * Sets the {@link TextureView} onto which video will be rendered. The player will track the - * lifecycle of the surface automatically. - * - * @param textureView The texture view. - */ - public void setVideoTextureView(TextureView textureView) { + @Override + public void setVideoTextureView(@Nullable TextureView textureView) { + verifyApplicationThread(); removeSurfaceCallbacks(); + if (textureView != null) { + clearVideoDecoderOutputBufferRenderer(); + } this.textureView = textureView; if (textureView == null) { - setVideoSurfaceInternal(null, true); + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } else { if (textureView.getSurfaceTextureListener() != null) { Log.w(TAG, "Replacing existing SurfaceTextureListener."); } - SurfaceTexture surfaceTexture = textureView.getSurfaceTexture(); - setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true); textureView.setSurfaceTextureListener(componentListener); + SurfaceTexture surfaceTexture = + textureView.isAvailable() ? textureView.getSurfaceTexture() : null; + if (surfaceTexture == null) { + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + } else { + setVideoSurfaceInternal(new Surface(surfaceTexture), /* ownsSurface= */ true); + maybeNotifySurfaceSizeChanged(textureView.getWidth(), textureView.getHeight()); + } } } - /** - * Clears the {@link TextureView} onto which video is being rendered if it matches the one passed. - * Else does nothing. - * - * @param textureView The texture view to clear. - */ - public void clearVideoTextureView(TextureView textureView) { + @Override + public void clearVideoTextureView(@Nullable TextureView textureView) { + verifyApplicationThread(); if (textureView != null && textureView == this.textureView) { setVideoTextureView(null); } } - /** - * Sets the stream type for audio playback (see {@link C.StreamType} and - * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}). If the stream type - * is not set, audio renderers use {@link C#STREAM_TYPE_DEFAULT}. - *

- * Note that when the stream type changes, the AudioTrack must be reinitialized, which can - * introduce a brief gap in audio output. Note also that tracks in the same audio session must - * share the same routing, so a new audio session id will be generated. - * - * @param audioStreamType The stream type for audio playback. - */ - public void setAudioStreamType(@C.StreamType int audioStreamType) { - this.audioStreamType = audioStreamType; - ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; - int count = 0; - for (Renderer renderer : renderers) { - if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_STREAM_TYPE, audioStreamType); + @Override + public void setVideoDecoderOutputBufferRenderer( + @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) { + verifyApplicationThread(); + if (videoDecoderOutputBufferRenderer != null) { + clearVideoSurface(); + } + setVideoDecoderOutputBufferRendererInternal(videoDecoderOutputBufferRenderer); + } + + @Override + public void clearVideoDecoderOutputBufferRenderer() { + verifyApplicationThread(); + setVideoDecoderOutputBufferRendererInternal(/* videoDecoderOutputBufferRenderer= */ null); + } + + @Override + public void clearVideoDecoderOutputBufferRenderer( + @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) { + verifyApplicationThread(); + if (videoDecoderOutputBufferRenderer != null + && videoDecoderOutputBufferRenderer == this.videoDecoderOutputBufferRenderer) { + clearVideoDecoderOutputBufferRenderer(); + } + } + + @Override + public void addAudioListener(AudioListener listener) { + audioListeners.add(listener); + } + + @Override + public void removeAudioListener(AudioListener listener) { + audioListeners.remove(listener); + } + + @Override + public void setAudioAttributes(AudioAttributes audioAttributes) { + setAudioAttributes(audioAttributes, /* handleAudioFocus= */ false); + } + + @Override + public void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus) { + verifyApplicationThread(); + if (playerReleased) { + return; + } + if (!Util.areEqual(this.audioAttributes, audioAttributes)) { + this.audioAttributes = audioAttributes; + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_AUDIO_ATTRIBUTES) + .setPayload(audioAttributes) + .send(); + } + } + for (AudioListener audioListener : audioListeners) { + audioListener.onAudioAttributesChanged(audioAttributes); } } - player.sendMessages(messages); + + audioFocusManager.setAudioAttributes(handleAudioFocus ? audioAttributes : null); + boolean playWhenReady = getPlayWhenReady(); + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState()); + updatePlayWhenReady(playWhenReady, playerCommand); + } + + @Override + public AudioAttributes getAudioAttributes() { + return audioAttributes; + } + + @Override + public int getAudioSessionId() { + return audioSessionId; + } + + @Override + public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) { + verifyApplicationThread(); + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_AUX_EFFECT_INFO) + .setPayload(auxEffectInfo) + .send(); + } + } + } + + @Override + public void clearAuxEffectInfo() { + setAuxEffectInfo(new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, /* sendLevel= */ 0f)); + } + + @Override + public void setVolume(float audioVolume) { + verifyApplicationThread(); + audioVolume = Util.constrainValue(audioVolume, /* min= */ 0, /* max= */ 1); + if (this.audioVolume == audioVolume) { + return; + } + this.audioVolume = audioVolume; + sendVolumeToRenderers(); + for (AudioListener audioListener : audioListeners) { + audioListener.onVolumeChanged(audioVolume); + } + } + + @Override + public float getVolume() { + return audioVolume; + } + + /** + * Sets the stream type for audio playback, used by the underlying audio track. + * + *

Setting the stream type during playback may introduce a short gap in audio output as the + * audio track is recreated. A new audio session id will also be generated. + * + *

Calling this method overwrites any attributes set previously by calling {@link + * #setAudioAttributes(AudioAttributes)}. + * + * @deprecated Use {@link #setAudioAttributes(AudioAttributes)}. + * @param streamType The stream type for audio playback. + */ + @Deprecated + public void setAudioStreamType(@C.StreamType int streamType) { + @C.AudioUsage int usage = Util.getAudioUsageForStreamType(streamType); + @C.AudioContentType int contentType = Util.getAudioContentTypeForStreamType(streamType); + AudioAttributes audioAttributes = + new AudioAttributes.Builder().setUsage(usage).setContentType(contentType).build(); + setAudioAttributes(audioAttributes); } /** * Returns the stream type for audio playback. - */ - public @C.StreamType int getAudioStreamType() { - return audioStreamType; - } - - /** - * Sets the audio volume, with 0 being silence and 1 being unity gain. * - * @param audioVolume The audio volume. + * @deprecated Use {@link #getAudioAttributes()}. */ - public void setVolume(float audioVolume) { - this.audioVolume = audioVolume; - ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; - int count = 0; - for (Renderer renderer : renderers) { - if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_VOLUME, audioVolume); - } - } - player.sendMessages(messages); + @Deprecated + public @C.StreamType int getAudioStreamType() { + return Util.getStreamTypeForAudioUsage(audioAttributes.usage); + } + + /** Returns the {@link AnalyticsCollector} used for collecting analytics events. */ + public AnalyticsCollector getAnalyticsCollector() { + return analyticsCollector; } /** - * Returns the audio volume, with 0 being silence and 1 being unity gain. + * Adds an {@link AnalyticsListener} to receive analytics events. + * + * @param listener The listener to be added. */ - public float getVolume() { - return audioVolume; + public void addAnalyticsListener(AnalyticsListener listener) { + verifyApplicationThread(); + analyticsCollector.addListener(listener); + } + + /** + * Removes an {@link AnalyticsListener}. + * + * @param listener The listener to be removed. + */ + public void removeAnalyticsListener(AnalyticsListener listener) { + verifyApplicationThread(); + analyticsCollector.removeListener(listener); + } + + /** + * Sets whether the player should pause automatically when audio is rerouted from a headset to + * device speakers. See the audio + * becoming noisy documentation for more information. + * + *

This feature is not enabled by default. + * + * @param handleAudioBecomingNoisy Whether the player should pause automatically when audio is + * rerouted from a headset to device speakers. + */ + public void setHandleAudioBecomingNoisy(boolean handleAudioBecomingNoisy) { + verifyApplicationThread(); + if (playerReleased) { + return; + } + audioBecomingNoisyManager.setEnabled(handleAudioBecomingNoisy); + } + + /** + * Sets a {@link PriorityTaskManager}, or null to clear a previously set priority task manager. + * + *

The priority {@link C#PRIORITY_PLAYBACK} will be set while the player is loading. + * + * @param priorityTaskManager The {@link PriorityTaskManager}, or null to clear a previously set + * priority task manager. + */ + public void setPriorityTaskManager(@Nullable PriorityTaskManager priorityTaskManager) { + verifyApplicationThread(); + if (Util.areEqual(this.priorityTaskManager, priorityTaskManager)) { + return; + } + if (isPriorityTaskManagerRegistered) { + Assertions.checkNotNull(this.priorityTaskManager).remove(C.PRIORITY_PLAYBACK); + } + if (priorityTaskManager != null && isLoading()) { + priorityTaskManager.add(C.PRIORITY_PLAYBACK); + isPriorityTaskManagerRegistered = true; + } else { + isPriorityTaskManagerRegistered = false; + } + this.priorityTaskManager = priorityTaskManager; } /** @@ -361,198 +860,444 @@ public class SimpleExoPlayer implements ExoPlayer { setPlaybackParameters(playbackParameters); } - /** - * Returns the video format currently being played, or null if no video is being played. - */ + /** Returns the video format currently being played, or null if no video is being played. */ + @Nullable public Format getVideoFormat() { return videoFormat; } - /** - * Returns the audio format currently being played, or null if no audio is being played. - */ + /** Returns the audio format currently being played, or null if no audio is being played. */ + @Nullable public Format getAudioFormat() { return audioFormat; } - /** - * Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set. - */ - public int getAudioSessionId() { - return audioSessionId; - } - - /** - * Returns {@link DecoderCounters} for video, or null if no video is being played. - */ + /** Returns {@link DecoderCounters} for video, or null if no video is being played. */ + @Nullable public DecoderCounters getVideoDecoderCounters() { return videoDecoderCounters; } - /** - * Returns {@link DecoderCounters} for audio, or null if no audio is being played. - */ + /** Returns {@link DecoderCounters} for audio, or null if no audio is being played. */ + @Nullable public DecoderCounters getAudioDecoderCounters() { return audioDecoderCounters; } - /** - * Sets a listener to receive video events. - * - * @param listener The listener. - */ - public void setVideoListener(VideoListener listener) { - videoListener = listener; + @Override + public void addVideoListener(org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener listener) { + videoListeners.add(listener); + } + + @Override + public void removeVideoListener(org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener listener) { + videoListeners.remove(listener); + } + + @Override + public void setVideoFrameMetadataListener(VideoFrameMetadataListener listener) { + verifyApplicationThread(); + videoFrameMetadataListener = listener; + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) + .setPayload(listener) + .send(); + } + } + } + + @Override + public void clearVideoFrameMetadataListener(VideoFrameMetadataListener listener) { + verifyApplicationThread(); + if (videoFrameMetadataListener != listener) { + return; + } + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) + .setPayload(null) + .send(); + } + } + } + + @Override + public void setCameraMotionListener(CameraMotionListener listener) { + verifyApplicationThread(); + cameraMotionListener = listener; + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_CAMERA_MOTION) { + player + .createMessage(renderer) + .setType(C.MSG_SET_CAMERA_MOTION_LISTENER) + .setPayload(listener) + .send(); + } + } + } + + @Override + public void clearCameraMotionListener(CameraMotionListener listener) { + verifyApplicationThread(); + if (cameraMotionListener != listener) { + return; + } + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_CAMERA_MOTION) { + player + .createMessage(renderer) + .setType(C.MSG_SET_CAMERA_MOTION_LISTENER) + .setPayload(null) + .send(); + } + } } /** - * Clears the listener receiving video events if it matches the one passed. Else does nothing. + * Sets a listener to receive video events, removing all existing listeners. + * + * @param listener The listener. + * @deprecated Use {@link #addVideoListener(org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener)}. + */ + @Deprecated + @SuppressWarnings("deprecation") + public void setVideoListener(VideoListener listener) { + videoListeners.clear(); + if (listener != null) { + addVideoListener(listener); + } + } + + /** + * Equivalent to {@link #removeVideoListener(org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener)}. * * @param listener The listener to clear. + * @deprecated Use {@link + * #removeVideoListener(org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener)}. */ + @Deprecated + @SuppressWarnings("deprecation") public void clearVideoListener(VideoListener listener) { - if (videoListener == listener) { - videoListener = null; + removeVideoListener(listener); + } + + @Override + public void addTextOutput(TextOutput listener) { + if (!currentCues.isEmpty()) { + listener.onCues(currentCues); } + textOutputs.add(listener); + } + + @Override + public void removeTextOutput(TextOutput listener) { + textOutputs.remove(listener); } /** - * Sets an output to receive text events. + * Sets an output to receive text events, removing all existing outputs. * * @param output The output. + * @deprecated Use {@link #addTextOutput(TextOutput)}. */ - public void setTextOutput(TextRenderer.Output output) { - textOutput = output; - } - - /** - * Clears the output receiving text events if it matches the one passed. Else does nothing. - * - * @param output The output to clear. - */ - public void clearTextOutput(TextRenderer.Output output) { - if (textOutput == output) { - textOutput = null; + @Deprecated + public void setTextOutput(TextOutput output) { + textOutputs.clear(); + if (output != null) { + addTextOutput(output); } } /** - * Sets a listener to receive metadata events. + * Equivalent to {@link #removeTextOutput(TextOutput)}. + * + * @param output The output to clear. + * @deprecated Use {@link #removeTextOutput(TextOutput)}. + */ + @Deprecated + public void clearTextOutput(TextOutput output) { + removeTextOutput(output); + } + + @Override + public void addMetadataOutput(MetadataOutput listener) { + metadataOutputs.add(listener); + } + + @Override + public void removeMetadataOutput(MetadataOutput listener) { + metadataOutputs.remove(listener); + } + + /** + * Sets an output to receive metadata events, removing all existing outputs. * * @param output The output. + * @deprecated Use {@link #addMetadataOutput(MetadataOutput)}. */ - public void setMetadataOutput(MetadataRenderer.Output output) { - metadataOutput = output; - } - - /** - * Clears the output receiving metadata events if it matches the one passed. Else does nothing. - * - * @param output The output to clear. - */ - public void clearMetadataOutput(MetadataRenderer.Output output) { - if (metadataOutput == output) { - metadataOutput = null; + @Deprecated + public void setMetadataOutput(MetadataOutput output) { + metadataOutputs.retainAll(Collections.singleton(analyticsCollector)); + if (output != null) { + addMetadataOutput(output); } } /** - * Sets a listener to receive debug events from the video renderer. + * Equivalent to {@link #removeMetadataOutput(MetadataOutput)}. * - * @param listener The listener. + * @param output The output to clear. + * @deprecated Use {@link #removeMetadataOutput(MetadataOutput)}. */ + @Deprecated + public void clearMetadataOutput(MetadataOutput output) { + removeMetadataOutput(output); + } + + /** + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug + * information. + */ + @Deprecated + @SuppressWarnings("deprecation") public void setVideoDebugListener(VideoRendererEventListener listener) { - videoDebugListener = listener; + videoDebugListeners.retainAll(Collections.singleton(analyticsCollector)); + if (listener != null) { + addVideoDebugListener(listener); + } } /** - * Sets a listener to receive debug events from the audio renderer. - * - * @param listener The listener. + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug + * information. */ + @Deprecated + public void addVideoDebugListener(VideoRendererEventListener listener) { + videoDebugListeners.add(listener); + } + + /** + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} and {@link + * #removeAnalyticsListener(AnalyticsListener)} to get more detailed debug information. + */ + @Deprecated + public void removeVideoDebugListener(VideoRendererEventListener listener) { + videoDebugListeners.remove(listener); + } + + /** + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug + * information. + */ + @Deprecated + @SuppressWarnings("deprecation") public void setAudioDebugListener(AudioRendererEventListener listener) { - audioDebugListener = listener; + audioDebugListeners.retainAll(Collections.singleton(analyticsCollector)); + if (listener != null) { + addAudioDebugListener(listener); + } + } + + /** + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug + * information. + */ + @Deprecated + public void addAudioDebugListener(AudioRendererEventListener listener) { + audioDebugListeners.add(listener); + } + + /** + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} and {@link + * #removeAnalyticsListener(AnalyticsListener)} to get more detailed debug information. + */ + @Deprecated + public void removeAudioDebugListener(AudioRendererEventListener listener) { + audioDebugListeners.remove(listener); } // ExoPlayer implementation @Override - public void addListener(EventListener listener) { + public Looper getPlaybackLooper() { + return player.getPlaybackLooper(); + } + + @Override + public Looper getApplicationLooper() { + return player.getApplicationLooper(); + } + + @Override + public void addListener(Player.EventListener listener) { + verifyApplicationThread(); player.addListener(listener); } @Override - public void removeListener(EventListener listener) { + public void removeListener(Player.EventListener listener) { + verifyApplicationThread(); player.removeListener(listener); } @Override + @State public int getPlaybackState() { + verifyApplicationThread(); return player.getPlaybackState(); } + @Override + @PlaybackSuppressionReason + public int getPlaybackSuppressionReason() { + verifyApplicationThread(); + return player.getPlaybackSuppressionReason(); + } + + @Override + @Nullable + public ExoPlaybackException getPlaybackError() { + verifyApplicationThread(); + return player.getPlaybackError(); + } + + @Override + public void retry() { + verifyApplicationThread(); + if (mediaSource != null + && (getPlaybackError() != null || getPlaybackState() == Player.STATE_IDLE)) { + prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); + } + } + @Override public void prepare(MediaSource mediaSource) { - player.prepare(mediaSource); + prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); } @Override public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + verifyApplicationThread(); + if (this.mediaSource != null) { + this.mediaSource.removeEventListener(analyticsCollector); + analyticsCollector.resetForNewMediaSource(); + } + this.mediaSource = mediaSource; + mediaSource.addEventListener(eventHandler, analyticsCollector); + boolean playWhenReady = getPlayWhenReady(); + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, Player.STATE_BUFFERING); + updatePlayWhenReady(playWhenReady, playerCommand); player.prepare(mediaSource, resetPosition, resetState); } @Override public void setPlayWhenReady(boolean playWhenReady) { - player.setPlayWhenReady(playWhenReady); + verifyApplicationThread(); + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState()); + updatePlayWhenReady(playWhenReady, playerCommand); } @Override public boolean getPlayWhenReady() { + verifyApplicationThread(); return player.getPlayWhenReady(); } + @Override + public @RepeatMode int getRepeatMode() { + verifyApplicationThread(); + return player.getRepeatMode(); + } + + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + verifyApplicationThread(); + player.setRepeatMode(repeatMode); + } + + @Override + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + verifyApplicationThread(); + player.setShuffleModeEnabled(shuffleModeEnabled); + } + + @Override + public boolean getShuffleModeEnabled() { + verifyApplicationThread(); + return player.getShuffleModeEnabled(); + } + @Override public boolean isLoading() { + verifyApplicationThread(); return player.isLoading(); } - @Override - public void seekToDefaultPosition() { - player.seekToDefaultPosition(); - } - - @Override - public void seekToDefaultPosition(int windowIndex) { - player.seekToDefaultPosition(windowIndex); - } - - @Override - public void seekTo(long positionMs) { - player.seekTo(positionMs); - } - @Override public void seekTo(int windowIndex, long positionMs) { + verifyApplicationThread(); + analyticsCollector.notifySeekStarted(); player.seekTo(windowIndex, positionMs); } @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { + verifyApplicationThread(); player.setPlaybackParameters(playbackParameters); } @Override public PlaybackParameters getPlaybackParameters() { + verifyApplicationThread(); return player.getPlaybackParameters(); } @Override - public void stop() { - player.stop(); + public void setSeekParameters(@Nullable SeekParameters seekParameters) { + verifyApplicationThread(); + player.setSeekParameters(seekParameters); + } + + @Override + public SeekParameters getSeekParameters() { + verifyApplicationThread(); + return player.getSeekParameters(); + } + + @Override + public void setForegroundMode(boolean foregroundMode) { + player.setForegroundMode(foregroundMode); + } + + @Override + public void stop(boolean reset) { + verifyApplicationThread(); + audioFocusManager.updateAudioFocus(getPlayWhenReady(), Player.STATE_IDLE); + player.stop(reset); + if (mediaSource != null) { + mediaSource.removeEventListener(analyticsCollector); + analyticsCollector.resetForNewMediaSource(); + if (reset) { + mediaSource = null; + } + } + currentCues = Collections.emptyList(); } @Override public void release() { + verifyApplicationThread(); + audioBecomingNoisyManager.setEnabled(false); + wakeLockManager.setStayAwake(false); + wifiLockManager.setStayAwake(false); + audioFocusManager.release(); player.release(); removeSurfaceCallbacks(); if (surface != null) { @@ -561,86 +1306,175 @@ public class SimpleExoPlayer implements ExoPlayer { } surface = null; } + if (mediaSource != null) { + mediaSource.removeEventListener(analyticsCollector); + mediaSource = null; + } + if (isPriorityTaskManagerRegistered) { + Assertions.checkNotNull(priorityTaskManager).remove(C.PRIORITY_PLAYBACK); + isPriorityTaskManagerRegistered = false; + } + bandwidthMeter.removeEventListener(analyticsCollector); + currentCues = Collections.emptyList(); + playerReleased = true; } @Override - public void sendMessages(ExoPlayerMessage... messages) { - player.sendMessages(messages); - } - - @Override - public void blockingSendMessages(ExoPlayerMessage... messages) { - player.blockingSendMessages(messages); + public PlayerMessage createMessage(PlayerMessage.Target target) { + verifyApplicationThread(); + return player.createMessage(target); } @Override public int getRendererCount() { + verifyApplicationThread(); return player.getRendererCount(); } @Override public int getRendererType(int index) { + verifyApplicationThread(); return player.getRendererType(index); } @Override public TrackGroupArray getCurrentTrackGroups() { + verifyApplicationThread(); return player.getCurrentTrackGroups(); } @Override public TrackSelectionArray getCurrentTrackSelections() { + verifyApplicationThread(); return player.getCurrentTrackSelections(); } @Override public Timeline getCurrentTimeline() { + verifyApplicationThread(); return player.getCurrentTimeline(); } - @Override - public Object getCurrentManifest() { - return player.getCurrentManifest(); - } - @Override public int getCurrentPeriodIndex() { + verifyApplicationThread(); return player.getCurrentPeriodIndex(); } @Override public int getCurrentWindowIndex() { + verifyApplicationThread(); return player.getCurrentWindowIndex(); } @Override public long getDuration() { + verifyApplicationThread(); return player.getDuration(); } @Override public long getCurrentPosition() { + verifyApplicationThread(); return player.getCurrentPosition(); } @Override public long getBufferedPosition() { + verifyApplicationThread(); return player.getBufferedPosition(); } @Override - public int getBufferedPercentage() { - return player.getBufferedPercentage(); + public long getTotalBufferedDuration() { + verifyApplicationThread(); + return player.getTotalBufferedDuration(); } @Override - public boolean isCurrentWindowDynamic() { - return player.isCurrentWindowDynamic(); + public boolean isPlayingAd() { + verifyApplicationThread(); + return player.isPlayingAd(); } @Override - public boolean isCurrentWindowSeekable() { - return player.isCurrentWindowSeekable(); + public int getCurrentAdGroupIndex() { + verifyApplicationThread(); + return player.getCurrentAdGroupIndex(); + } + + @Override + public int getCurrentAdIndexInAdGroup() { + verifyApplicationThread(); + return player.getCurrentAdIndexInAdGroup(); + } + + @Override + public long getContentPosition() { + verifyApplicationThread(); + return player.getContentPosition(); + } + + @Override + public long getContentBufferedPosition() { + verifyApplicationThread(); + return player.getContentBufferedPosition(); + } + + /** + * Sets whether the player should use a {@link android.os.PowerManager.WakeLock} to ensure the + * device stays awake for playback, even when the screen is off. + * + *

Enabling this feature requires the {@link android.Manifest.permission#WAKE_LOCK} permission. + * It should be used together with a foreground {@link android.app.Service} for use cases where + * playback can occur when the screen is off (e.g. background audio playback). It is not useful if + * the screen will always be on during playback (e.g. foreground video playback). + * + *

This feature is not enabled by default. If enabled, a WakeLock is held whenever the player + * is in the {@link #STATE_READY READY} or {@link #STATE_BUFFERING BUFFERING} states with {@code + * playWhenReady = true}. + * + * @param handleWakeLock Whether the player should use a {@link android.os.PowerManager.WakeLock} + * to ensure the device stays awake for playback, even when the screen is off. + * @deprecated Use {@link #setWakeMode(int)} instead. + */ + @Deprecated + public void setHandleWakeLock(boolean handleWakeLock) { + setWakeMode(handleWakeLock ? C.WAKE_MODE_LOCAL : C.WAKE_MODE_NONE); + } + + /** + * Sets how the player should keep the device awake for playback when the screen is off. + * + *

Enabling this feature requires the {@link android.Manifest.permission#WAKE_LOCK} permission. + * It should be used together with a foreground {@link android.app.Service} for use cases where + * playback occurs and the screen is off (e.g. background audio playback). It is not useful when + * the screen will be kept on during playback (e.g. foreground video playback). + * + *

When enabled, the locks ({@link android.os.PowerManager.WakeLock} / {@link + * android.net.wifi.WifiManager.WifiLock}) will be held whenever the player is in the {@link + * #STATE_READY} or {@link #STATE_BUFFERING} states with {@code playWhenReady = true}. The locks + * held depends on the specified {@link C.WakeMode}. + * + * @param wakeMode The {@link C.WakeMode} option to keep the device awake during playback. + */ + public void setWakeMode(@C.WakeMode int wakeMode) { + switch (wakeMode) { + case C.WAKE_MODE_NONE: + wakeLockManager.setEnabled(false); + wifiLockManager.setEnabled(false); + break; + case C.WAKE_MODE_LOCAL: + wakeLockManager.setEnabled(true); + wifiLockManager.setEnabled(false); + break; + case C.WAKE_MODE_NETWORK: + wakeLockManager.setEnabled(true); + wifiLockManager.setEnabled(true); + break; + default: + break; + } } // Internal methods. @@ -660,94 +1494,184 @@ public class SimpleExoPlayer implements ExoPlayer { } } - private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) { + private void setVideoSurfaceInternal(@Nullable Surface surface, boolean ownsSurface) { // Note: We don't turn this method into a no-op if the surface is being replaced with itself // so as to ensure onRenderedFirstFrame callbacks are still called in this case. - ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; - int count = 0; + List messages = new ArrayList<>(); for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface); + messages.add( + player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setPayload(surface).send()); } } if (this.surface != null && this.surface != surface) { - // If we created this surface, we are responsible for releasing it. + // We're replacing a surface. Block to ensure that it's not accessed after the method returns. + try { + for (PlayerMessage message : messages) { + message.blockUntilDelivered(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + // If we created the previous surface, we are responsible for releasing it. if (this.ownsSurface) { this.surface.release(); } - // We're replacing a surface. Block to ensure that it's not accessed after the method returns. - player.blockingSendMessages(messages); - } else { - player.sendMessages(messages); } this.surface = surface; this.ownsSurface = ownsSurface; } - private final class ComponentListener implements VideoRendererEventListener, - AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output, - SurfaceHolder.Callback, TextureView.SurfaceTextureListener { + private void setVideoDecoderOutputBufferRendererInternal( + @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) { + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) + .setPayload(videoDecoderOutputBufferRenderer) + .send(); + } + } + this.videoDecoderOutputBufferRenderer = videoDecoderOutputBufferRenderer; + } + + private void maybeNotifySurfaceSizeChanged(int width, int height) { + if (width != surfaceWidth || height != surfaceHeight) { + surfaceWidth = width; + surfaceHeight = height; + for (org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) { + videoListener.onSurfaceSizeChanged(width, height); + } + } + } + + private void sendVolumeToRenderers() { + float scaledVolume = audioVolume * audioFocusManager.getVolumeMultiplier(); + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { + player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setPayload(scaledVolume).send(); + } + } + } + + private void updatePlayWhenReady( + boolean playWhenReady, @AudioFocusManager.PlayerCommand int playerCommand) { + playWhenReady = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY; + @PlaybackSuppressionReason + int playbackSuppressionReason = + playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY + ? Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS + : Player.PLAYBACK_SUPPRESSION_REASON_NONE; + player.setPlayWhenReady(playWhenReady, playbackSuppressionReason); + } + + private void verifyApplicationThread() { + if (Looper.myLooper() != getApplicationLooper()) { + Log.w( + TAG, + "Player is accessed on the wrong thread. See " + + "https://exoplayer.dev/issues/player-accessed-on-wrong-thread", + hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException()); + hasNotifiedFullWrongThreadWarning = true; + } + } + + private void updateWakeAndWifiLock() { + @State int playbackState = getPlaybackState(); + switch (playbackState) { + case Player.STATE_READY: + case Player.STATE_BUFFERING: + wakeLockManager.setStayAwake(getPlayWhenReady()); + wifiLockManager.setStayAwake(getPlayWhenReady()); + break; + case Player.STATE_ENDED: + case Player.STATE_IDLE: + wakeLockManager.setStayAwake(false); + wifiLockManager.setStayAwake(false); + break; + default: + throw new IllegalStateException(); + } + } + + private final class ComponentListener + implements VideoRendererEventListener, + AudioRendererEventListener, + TextOutput, + MetadataOutput, + SurfaceHolder.Callback, + TextureView.SurfaceTextureListener, + AudioFocusManager.PlayerControl, + AudioBecomingNoisyManager.EventListener, + Player.EventListener { // VideoRendererEventListener implementation @Override public void onVideoEnabled(DecoderCounters counters) { videoDecoderCounters = counters; - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoEnabled(counters); } } @Override - public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, - long initializationDurationMs) { - if (videoDebugListener != null) { - videoDebugListener.onVideoDecoderInitialized(decoderName, initializedTimestampMs, - initializationDurationMs); + public void onVideoDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { + videoDebugListener.onVideoDecoderInitialized( + decoderName, initializedTimestampMs, initializationDurationMs); } } @Override public void onVideoInputFormatChanged(Format format) { videoFormat = format; - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoInputFormatChanged(format); } } @Override public void onDroppedFrames(int count, long elapsed) { - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onDroppedFrames(count, elapsed); } } @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio) { - if (videoListener != null) { - videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, - pixelWidthHeightRatio); + public void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + for (org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) { + // Prevent duplicate notification if a listener is both a VideoRendererEventListener and + // a VideoListener, as they have the same method signature. + if (!videoDebugListeners.contains(videoListener)) { + videoListener.onVideoSizeChanged( + width, height, unappliedRotationDegrees, pixelWidthHeightRatio); + } } - if (videoDebugListener != null) { - videoDebugListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, - pixelWidthHeightRatio); + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { + videoDebugListener.onVideoSizeChanged( + width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } } @Override public void onRenderedFirstFrame(Surface surface) { - if (videoListener != null && SimpleExoPlayer.this.surface == surface) { - videoListener.onRenderedFirstFrame(); + if (SimpleExoPlayer.this.surface == surface) { + for (org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) { + videoListener.onRenderedFirstFrame(); + } } - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onRenderedFirstFrame(surface); } } @Override public void onVideoDisabled(DecoderCounters counters) { - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoDisabled(counters); } videoFormat = null; @@ -759,47 +1683,57 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioEnabled(DecoderCounters counters) { audioDecoderCounters = counters; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioEnabled(counters); } } @Override public void onAudioSessionId(int sessionId) { + if (audioSessionId == sessionId) { + return; + } audioSessionId = sessionId; - if (audioDebugListener != null) { + for (AudioListener audioListener : audioListeners) { + // Prevent duplicate notification if a listener is both a AudioRendererEventListener and + // a AudioListener, as they have the same method signature. + if (!audioDebugListeners.contains(audioListener)) { + audioListener.onAudioSessionId(sessionId); + } + } + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioSessionId(sessionId); } } @Override - public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs, - long initializationDurationMs) { - if (audioDebugListener != null) { - audioDebugListener.onAudioDecoderInitialized(decoderName, initializedTimestampMs, - initializationDurationMs); + public void onAudioDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { + audioDebugListener.onAudioDecoderInitialized( + decoderName, initializedTimestampMs, initializationDurationMs); } } @Override public void onAudioInputFormatChanged(Format format) { audioFormat = format; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioInputFormatChanged(format); } } @Override - public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, - long elapsedSinceLastFeedMs) { - if (audioDebugListener != null) { - audioDebugListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + public void onAudioSinkUnderrun( + int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { + audioDebugListener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); } } @Override public void onAudioDisabled(DecoderCounters counters) { - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioDisabled(counters); } audioFormat = null; @@ -807,20 +1741,21 @@ public class SimpleExoPlayer implements ExoPlayer { audioSessionId = C.AUDIO_SESSION_ID_UNSET; } - // TextRenderer.Output implementation + // TextOutput implementation @Override public void onCues(List cues) { - if (textOutput != null) { + currentCues = cues; + for (TextOutput textOutput : textOutputs) { textOutput.onCues(cues); } } - // MetadataRenderer.Output implementation + // MetadataOutput implementation @Override public void onMetadata(Metadata metadata) { - if (metadataOutput != null) { + for (MetadataOutput metadataOutput : metadataOutputs) { metadataOutput.onMetadata(metadata); } } @@ -834,29 +1769,32 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - // Do nothing. + maybeNotifySurfaceSizeChanged(width, height); } @Override public void surfaceDestroyed(SurfaceHolder holder) { - setVideoSurfaceInternal(null, false); + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } // TextureView.SurfaceTextureListener implementation @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { - setVideoSurfaceInternal(new Surface(surfaceTexture), true); + setVideoSurfaceInternal(new Surface(surfaceTexture), /* ownsSurface= */ true); + maybeNotifySurfaceSizeChanged(width, height); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) { - // Do nothing. + maybeNotifySurfaceSizeChanged(width, height); } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { - setVideoSurfaceInternal(null, true); + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); return true; } @@ -865,6 +1803,43 @@ public class SimpleExoPlayer implements ExoPlayer { // Do nothing. } - } + // AudioFocusManager.PlayerControl implementation + @Override + public void setVolumeMultiplier(float volumeMultiplier) { + sendVolumeToRenderers(); + } + + @Override + public void executePlayerCommand(@AudioFocusManager.PlayerCommand int playerCommand) { + updatePlayWhenReady(getPlayWhenReady(), playerCommand); + } + + // AudioBecomingNoisyManager.EventListener implementation. + + @Override + public void onAudioBecomingNoisy() { + setPlayWhenReady(false); + } + + // Player.EventListener implementation. + + @Override + public void onLoadingChanged(boolean isLoading) { + if (priorityTaskManager != null) { + if (isLoading && !isPriorityTaskManagerRegistered) { + priorityTaskManager.add(C.PRIORITY_PLAYBACK); + isPriorityTaskManagerRegistered = true; + } else if (!isLoading && isPriorityTaskManagerRegistered) { + priorityTaskManager.remove(C.PRIORITY_PLAYBACK); + isPriorityTaskManagerRegistered = false; + } + } + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) { + updateWakeAndWifiLock(); + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Timeline.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Timeline.java index dffcd65fa09f..c9e3d16ff7ae 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Timeline.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Timeline.java @@ -15,220 +15,123 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2; +import android.util.Pair; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ads.AdPlaybackState; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; + /** - * A representation of media currently available for playback. - *

- * Timeline instances are immutable. For cases where the available media is changing dynamically - * (e.g. live streams) a timeline provides a snapshot of the media currently available. - *

- * A timeline consists of related {@link Period}s and {@link Window}s. A period defines a single - * logical piece of media, for example a media file. A window spans one or more periods, defining - * the region within those periods that's currently available for playback along with additional - * information such as whether seeking is supported within the window. Each window defines a default - * position, which is the position from which playback will start when the player starts playing the - * window. The following examples illustrate timelines for various use cases. + * A flexible representation of the structure of media. A timeline is able to represent the + * structure of a wide variety of media, from simple cases like a single media file through to + * complex compositions of media such as playlists and streams with inserted ads. Instances are + * immutable. For cases where media is changing dynamically (e.g. live streams), a timeline provides + * a snapshot of the current state. + * + *

A timeline consists of {@link Window Windows} and {@link Period Periods}. + * + *

+ * + *

The following examples illustrate timelines for various use cases. * *

Single media file or on-demand stream

- *

- * Example timeline for a single file - *

- * A timeline for a single media file or on-demand stream consists of a single period and window. - * The window spans the whole period, indicating that all parts of the media are available for - * playback. The window's default position is typically at the start of the period (indicated by the - * black dot in the figure above). + * + *

Example timeline for a
+ * single file A timeline for a single media file or on-demand stream consists of a single period + * and window. The window spans the whole period, indicating that all parts of the media are + * available for playback. The window's default position is typically at the start of the period + * (indicated by the black dot in the figure above). * *

Playlist of media files or on-demand streams

- *

- * Example timeline for a playlist of files - *

- * A timeline for a playlist of media files or on-demand streams consists of multiple periods, each - * with its own window. Each window spans the whole of the corresponding period, and typically has a - * default position at the start of the period. The properties of the periods and windows (e.g. - * their durations and whether the window is seekable) will often only become known when the player - * starts buffering the corresponding file or stream. + * + *

Example timeline for a
+ * playlist of files A timeline for a playlist of media files or on-demand streams consists of + * multiple periods, each with its own window. Each window spans the whole of the corresponding + * period, and typically has a default position at the start of the period. The properties of the + * periods and windows (e.g. their durations and whether the window is seekable) will often only + * become known when the player starts buffering the corresponding file or stream. * *

Live stream with limited availability

- *

- * Example timeline for a live stream with
- *       limited availability - *

- * A timeline for a live stream consists of a period whose duration is unknown, since it's - * continually extending as more content is broadcast. If content only remains available for a - * limited period of time then the window may start at a non-zero position, defining the region of - * content that can still be played. The window will have {@link Window#isDynamic} set to true if - * the stream is still live. Its default position is typically near to the live edge (indicated by - * the black dot in the figure above). + * + *

Example timeline for
+ * a live stream with limited availability A timeline for a live stream consists of a period whose + * duration is unknown, since it's continually extending as more content is broadcast. If content + * only remains available for a limited period of time then the window may start at a non-zero + * position, defining the region of content that can still be played. The window will have {@link + * Window#isLive} set to true to indicate it's a live stream and {@link Window#isDynamic} set to + * true as long as we expect changes to the live window. Its default position is typically near to + * the live edge (indicated by the black dot in the figure above). * *

Live stream with indefinite availability

- *

- * Example timeline for a live stream with
- *       indefinite availability - *

- * A timeline for a live stream with indefinite availability is similar to the - * Live stream with limited availability case, except that the window - * starts at the beginning of the period to indicate that all of the previously broadcast content - * can still be played. + * + *

Example timeline
+ * for a live stream with indefinite availability A timeline for a live stream with indefinite + * availability is similar to the Live stream with limited availability + * case, except that the window starts at the beginning of the period to indicate that all of the + * previously broadcast content can still be played. * *

Live stream with multiple periods

- *

- * Example timeline for a live stream
- *       with multiple periods - *

- * This case arises when a live stream is explicitly divided into separate periods, for example at - * content and advert boundaries. This case is similar to the Live stream - * with limited availability case, except that the window may span more than one period. - * Multiple periods are also possible in the indefinite availability case. * - *

On-demand pre-roll followed by live stream

- *

- * Example timeline for an on-demand pre-roll
- *       followed by a live stream - *

- * This case is the concatenation of the Single media file or on-demand - * stream and Live stream with multiple periods cases. When playback - * of the pre-roll ends, playback of the live stream will start from its default position near the - * live edge. + *

Example timeline
+ * for a live stream with multiple periods This case arises when a live stream is explicitly + * divided into separate periods, for example at content boundaries. This case is similar to the Live stream with limited availability case, except that the window may + * span more than one period. Multiple periods are also possible in the indefinite availability + * case. + * + *

On-demand stream followed by live stream

+ * + *

Example timeline for an
+ * on-demand stream followed by a live stream This case is the concatenation of the Single media file or on-demand stream and Live + * stream with multiple periods cases. When playback of the on-demand stream ends, playback of + * the live stream will start from its default position near the live edge. + * + *

On-demand stream with mid-roll ads

+ * + *

Example
+ * timeline for an on-demand stream with mid-roll ad groups This case includes mid-roll ad groups, + * which are defined as part of the timeline's single period. The period can be queried for + * information about the ad groups and the ads they contain. */ public abstract class Timeline { /** - * An empty timeline. - */ - public static final Timeline EMPTY = new Timeline() { - - @Override - public int getWindowCount() { - return 0; - } - - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - throw new IndexOutOfBoundsException(); - } - - @Override - public int getPeriodCount() { - return 0; - } - - @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - throw new IndexOutOfBoundsException(); - } - - @Override - public int getIndexOfPeriod(Object uid) { - return C.INDEX_UNSET; - } - - }; - - /** - * Returns whether the timeline is empty. - */ - public final boolean isEmpty() { - return getWindowCount() == 0; - } - - /** - * Returns the number of windows in the timeline. - */ - public abstract int getWindowCount(); - - /** - * Populates a {@link Window} with data for the window at the specified index. Does not populate - * {@link Window#id}. + * Holds information about a window in a {@link Timeline}. A window usually corresponds to one + * playlist item and defines a region of media currently available for playback along with + * additional information such as whether seeking is supported within the window. The figure below + * shows some of the information defined by a window, as well as how this information relates to + * corresponding {@link Period Periods} in the timeline. * - * @param windowIndex The index of the window. - * @param window The {@link Window} to populate. Must not be null. - * @return The populated {@link Window}, for convenience. - */ - public final Window getWindow(int windowIndex, Window window) { - return getWindow(windowIndex, window, false); - } - - /** - * Populates a {@link Window} with data for the window at the specified index. - * - * @param windowIndex The index of the window. - * @param window The {@link Window} to populate. Must not be null. - * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to - * null. The caller should pass false for efficiency reasons unless the field is required. - * @return The populated {@link Window}, for convenience. - */ - public Window getWindow(int windowIndex, Window window, boolean setIds) { - return getWindow(windowIndex, window, setIds, 0); - } - - /** - * Populates a {@link Window} with data for the window at the specified index. - * - * @param windowIndex The index of the window. - * @param window The {@link Window} to populate. Must not be null. - * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to - * null. The caller should pass false for efficiency reasons unless the field is required. - * @param defaultPositionProjectionUs A duration into the future that the populated window's - * default start position should be projected. - * @return The populated {@link Window}, for convenience. - */ - public abstract Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs); - - /** - * Returns the number of periods in the timeline. - */ - public abstract int getPeriodCount(); - - /** - * Populates a {@link Period} with data for the period at the specified index. Does not populate - * {@link Period#id} and {@link Period#uid}. - * - * @param periodIndex The index of the period. - * @param period The {@link Period} to populate. Must not be null. - * @return The populated {@link Period}, for convenience. - */ - public final Period getPeriod(int periodIndex, Period period) { - return getPeriod(periodIndex, period, false); - } - - /** - * Populates a {@link Period} with data for the period at the specified index. - * - * @param periodIndex The index of the period. - * @param period The {@link Period} to populate. Must not be null. - * @param setIds Whether {@link Period#id} and {@link Period#uid} should be populated. If false, - * the fields will be set to null. The caller should pass false for efficiency reasons unless - * the fields are required. - * @return The populated {@link Period}, for convenience. - */ - public abstract Period getPeriod(int periodIndex, Period period, boolean setIds); - - /** - * Returns the index of the period identified by its unique {@code id}, or {@link C#INDEX_UNSET} - * if the period is not in the timeline. - * - * @param uid A unique identifier for a period. - * @return The index of the period, or {@link C#INDEX_UNSET} if the period was not found. - */ - public abstract int getIndexOfPeriod(Object uid); - - /** - * Holds information about a window in a {@link Timeline}. A window defines a region of media - * currently available for playback along with additional information such as whether seeking is - * supported within the window. See {@link Timeline} for more details. The figure below shows some - * of the information defined by a window, as well as how this information relates to - * corresponding {@link Period}s in the timeline. - *

- * Information defined by a timeline window - *

+ *

Information defined by a
+   * timeline window */ public static final class Window { /** - * An identifier for the window. Not necessarily unique. + * A {@link #uid} for a window that must be used for single-window {@link Timeline Timelines}. */ - public Object id; + public static final Object SINGLE_WINDOW_UID = new Object(); + + /** + * A unique identifier for the window. Single-window {@link Timeline Timelines} must use {@link + * #SINGLE_WINDOW_UID}. + */ + public Object uid; + + /** A tag for the window. Not necessarily unique. */ + @Nullable public Object tag; + + /** The manifest of the window. May be {@code null}. */ + @Nullable public Object manifest; /** * The start time of the presentation to which this window belongs in milliseconds since the @@ -247,14 +150,22 @@ public abstract class Timeline { */ public boolean isSeekable; - /** - * Whether this window may change when the timeline is updated. - */ + // TODO: Split this to better describe which parts of the window might change. For example it + // should be possible to individually determine whether the start and end positions of the + // window may change relative to the underlying periods. For an example of where it's useful to + // know that the end position is fixed whilst the start position may still change, see: + // https://github.com/google/ExoPlayer/issues/4780. + /** Whether this window may change when the timeline is updated. */ public boolean isDynamic; /** - * The index of the first period that belongs to this window. + * Whether the media in this window is live. For informational purposes only. + * + *

Check {@link #isDynamic} to know whether this window may still change. */ + public boolean isLive; + + /** The index of the first period that belongs to this window. */ public int firstPeriodIndex; /** @@ -281,17 +192,34 @@ public abstract class Timeline { */ public long positionInFirstPeriodUs; - /** - * Sets the data held by this window. - */ - public Window set(Object id, long presentationStartTimeMs, long windowStartTimeMs, - boolean isSeekable, boolean isDynamic, long defaultPositionUs, long durationUs, - int firstPeriodIndex, int lastPeriodIndex, long positionInFirstPeriodUs) { - this.id = id; + /** Creates window. */ + public Window() { + uid = SINGLE_WINDOW_UID; + } + + /** Sets the data held by this window. */ + public Window set( + Object uid, + @Nullable Object tag, + @Nullable Object manifest, + long presentationStartTimeMs, + long windowStartTimeMs, + boolean isSeekable, + boolean isDynamic, + boolean isLive, + long defaultPositionUs, + long durationUs, + int firstPeriodIndex, + int lastPeriodIndex, + long positionInFirstPeriodUs) { + this.uid = uid; + this.tag = tag; + this.manifest = manifest; this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; this.isSeekable = isSeekable; this.isDynamic = isDynamic; + this.isLive = isLive; this.defaultPositionUs = defaultPositionUs; this.durationUs = durationUs; this.firstPeriodIndex = firstPeriodIndex; @@ -354,24 +282,27 @@ public abstract class Timeline { /** * Holds information about a period in a {@link Timeline}. A period defines a single logical piece - * of media, for example a a media file. See {@link Timeline} for more details. The figure below - * shows some of the information defined by a period, as well as how this information relates to a - * corresponding {@link Window} in the timeline. - *

- * Information defined by a period - *

+ * of media, for example a media file. It may also define groups of ads inserted into the media, + * along with information about whether those ads have been loaded and played. + * + *

The figure below shows some of the information defined by a period, as well as how this + * information relates to a corresponding {@link Window} in the timeline. + * + *

Information defined by a
+   * period */ public static final class Period { /** - * An identifier for the period. Not necessarily unique. + * An identifier for the period. Not necessarily unique. May be null if the ids of the period + * are not required. */ - public Object id; + @Nullable public Object id; /** - * A unique identifier for the period. + * A unique identifier for the period. May be null if the ids of the period are not required. */ - public Object uid; + @Nullable public Object uid; /** * The index of the window to which this period belongs. @@ -383,24 +314,68 @@ public abstract class Timeline { */ public long durationUs; - /** - * Whether this period contains an ad. - */ - public boolean isAd; - private long positionInWindowUs; + private AdPlaybackState adPlaybackState; + + /** Creates a new instance with no ad playback state. */ + public Period() { + adPlaybackState = AdPlaybackState.NONE; + } /** * Sets the data held by this period. + * + * @param id An identifier for the period. Not necessarily unique. May be null if the ids of the + * period are not required. + * @param uid A unique identifier for the period. May be null if the ids of the period are not + * required. + * @param windowIndex The index of the window to which this period belongs. + * @param durationUs The duration of this period in microseconds, or {@link C#TIME_UNSET} if + * unknown. + * @param positionInWindowUs The position of the start of this period relative to the start of + * the window to which it belongs, in milliseconds. May be negative if the start of the + * period is not within the window. + * @return This period, for convenience. */ - public Period set(Object id, Object uid, int windowIndex, long durationUs, - long positionInWindowUs, boolean isAd) { + public Period set( + @Nullable Object id, + @Nullable Object uid, + int windowIndex, + long durationUs, + long positionInWindowUs) { + return set(id, uid, windowIndex, durationUs, positionInWindowUs, AdPlaybackState.NONE); + } + + /** + * Sets the data held by this period. + * + * @param id An identifier for the period. Not necessarily unique. May be null if the ids of the + * period are not required. + * @param uid A unique identifier for the period. May be null if the ids of the period are not + * required. + * @param windowIndex The index of the window to which this period belongs. + * @param durationUs The duration of this period in microseconds, or {@link C#TIME_UNSET} if + * unknown. + * @param positionInWindowUs The position of the start of this period relative to the start of + * the window to which it belongs, in milliseconds. May be negative if the start of the + * period is not within the window. + * @param adPlaybackState The state of the period's ads, or {@link AdPlaybackState#NONE} if + * there are no ads. + * @return This period, for convenience. + */ + public Period set( + @Nullable Object id, + @Nullable Object uid, + int windowIndex, + long durationUs, + long positionInWindowUs, + AdPlaybackState adPlaybackState) { this.id = id; this.uid = uid; this.windowIndex = windowIndex; this.durationUs = durationUs; this.positionInWindowUs = positionInWindowUs; - this.isAd = isAd; + this.adPlaybackState = adPlaybackState; return this; } @@ -436,6 +411,427 @@ public abstract class Timeline { return positionInWindowUs; } + /** + * Returns the number of ad groups in the period. + */ + public int getAdGroupCount() { + return adPlaybackState.adGroupCount; + } + + /** + * Returns the time of the ad group at index {@code adGroupIndex} in the period, in + * microseconds. + * + * @param adGroupIndex The ad group index. + * @return The time of the ad group at the index, in microseconds, or {@link + * C#TIME_END_OF_SOURCE} for a post-roll ad group. + */ + public long getAdGroupTimeUs(int adGroupIndex) { + return adPlaybackState.adGroupTimesUs[adGroupIndex]; + } + + /** + * Returns the index of the first ad in the specified ad group that should be played, or the + * number of ads in the ad group if no ads should be played. + * + * @param adGroupIndex The ad group index. + * @return The index of the first ad that should be played, or the number of ads in the ad group + * if no ads should be played. + */ + public int getFirstAdIndexToPlay(int adGroupIndex) { + return adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); + } + + /** + * Returns the index of the next ad in the specified ad group that should be played after + * playing {@code adIndexInAdGroup}, or the number of ads in the ad group if no later ads should + * be played. + * + * @param adGroupIndex The ad group index. + * @param lastPlayedAdIndex The last played ad index in the ad group. + * @return The index of the next ad that should be played, or the number of ads in the ad group + * if the ad group does not have any ads remaining to play. + */ + public int getNextAdIndexToPlay(int adGroupIndex, int lastPlayedAdIndex) { + return adPlaybackState.adGroups[adGroupIndex].getNextAdIndexToPlay(lastPlayedAdIndex); + } + + /** + * Returns whether the ad group at index {@code adGroupIndex} has been played. + * + * @param adGroupIndex The ad group index. + * @return Whether the ad group at index {@code adGroupIndex} has been played. + */ + public boolean hasPlayedAdGroup(int adGroupIndex) { + return !adPlaybackState.adGroups[adGroupIndex].hasUnplayedAds(); + } + + /** + * Returns the index of the ad group at or before {@code positionUs}, if that ad group is + * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has + * no ads remaining to be played, or if there is no such ad group. + * + * @param positionUs The position at or before which to find an ad group, in microseconds. + * @return The index of the ad group, or {@link C#INDEX_UNSET}. + */ + public int getAdGroupIndexForPositionUs(long positionUs) { + return adPlaybackState.getAdGroupIndexForPositionUs(positionUs); + } + + /** + * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be + * played. Returns {@link C#INDEX_UNSET} if there is no such ad group. + * + * @param positionUs The position after which to find an ad group, in microseconds. + * @return The index of the ad group, or {@link C#INDEX_UNSET}. + */ + public int getAdGroupIndexAfterPositionUs(long positionUs) { + return adPlaybackState.getAdGroupIndexAfterPositionUs(positionUs, durationUs); + } + + /** + * Returns the number of ads in the ad group at index {@code adGroupIndex}, or + * {@link C#LENGTH_UNSET} if not yet known. + * + * @param adGroupIndex The ad group index. + * @return The number of ads in the ad group, or {@link C#LENGTH_UNSET} if not yet known. + */ + public int getAdCountInAdGroup(int adGroupIndex) { + return adPlaybackState.adGroups[adGroupIndex].count; + } + + /** + * Returns whether the URL for the specified ad is known. + * + * @param adGroupIndex The ad group index. + * @param adIndexInAdGroup The ad index in the ad group. + * @return Whether the URL for the specified ad is known. + */ + public boolean isAdAvailable(int adGroupIndex, int adIndexInAdGroup) { + AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; + return adGroup.count != C.LENGTH_UNSET + && adGroup.states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE; + } + + /** + * Returns the duration of the ad at index {@code adIndexInAdGroup} in the ad group at + * {@code adGroupIndex}, in microseconds, or {@link C#TIME_UNSET} if not yet known. + * + * @param adGroupIndex The ad group index. + * @param adIndexInAdGroup The ad index in the ad group. + * @return The duration of the ad, or {@link C#TIME_UNSET} if not yet known. + */ + public long getAdDurationUs(int adGroupIndex, int adIndexInAdGroup) { + AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; + return adGroup.count != C.LENGTH_UNSET ? adGroup.durationsUs[adIndexInAdGroup] : C.TIME_UNSET; + } + + /** + * Returns the position offset in the first unplayed ad at which to begin playback, in + * microseconds. + */ + public long getAdResumePositionUs() { + return adPlaybackState.adResumePositionUs; + } + } + /** An empty timeline. */ + public static final Timeline EMPTY = + new Timeline() { + + @Override + public int getWindowCount() { + return 0; + } + + @Override + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + throw new IndexOutOfBoundsException(); + } + + @Override + public int getPeriodCount() { + return 0; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + throw new IndexOutOfBoundsException(); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return C.INDEX_UNSET; + } + + @Override + public Object getUidOfPeriod(int periodIndex) { + throw new IndexOutOfBoundsException(); + } + }; + + /** + * Returns whether the timeline is empty. + */ + public final boolean isEmpty() { + return getWindowCount() == 0; + } + + /** + * Returns the number of windows in the timeline. + */ + public abstract int getWindowCount(); + + /** + * Returns the index of the window after the window at index {@code windowIndex} depending on the + * {@code repeatMode} and whether shuffling is enabled. + * + * @param windowIndex Index of a window in the timeline. + * @param repeatMode A repeat mode. + * @param shuffleModeEnabled Whether shuffling is enabled. + * @return The index of the next window, or {@link C#INDEX_UNSET} if this is the last window. + */ + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { + switch (repeatMode) { + case Player.REPEAT_MODE_OFF: + return windowIndex == getLastWindowIndex(shuffleModeEnabled) ? C.INDEX_UNSET + : windowIndex + 1; + case Player.REPEAT_MODE_ONE: + return windowIndex; + case Player.REPEAT_MODE_ALL: + return windowIndex == getLastWindowIndex(shuffleModeEnabled) + ? getFirstWindowIndex(shuffleModeEnabled) : windowIndex + 1; + default: + throw new IllegalStateException(); + } + } + + /** + * Returns the index of the window before the window at index {@code windowIndex} depending on the + * {@code repeatMode} and whether shuffling is enabled. + * + * @param windowIndex Index of a window in the timeline. + * @param repeatMode A repeat mode. + * @param shuffleModeEnabled Whether shuffling is enabled. + * @return The index of the previous window, or {@link C#INDEX_UNSET} if this is the first window. + */ + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { + switch (repeatMode) { + case Player.REPEAT_MODE_OFF: + return windowIndex == getFirstWindowIndex(shuffleModeEnabled) ? C.INDEX_UNSET + : windowIndex - 1; + case Player.REPEAT_MODE_ONE: + return windowIndex; + case Player.REPEAT_MODE_ALL: + return windowIndex == getFirstWindowIndex(shuffleModeEnabled) + ? getLastWindowIndex(shuffleModeEnabled) : windowIndex - 1; + default: + throw new IllegalStateException(); + } + } + + /** + * Returns the index of the last window in the playback order depending on whether shuffling is + * enabled. + * + * @param shuffleModeEnabled Whether shuffling is enabled. + * @return The index of the last window in the playback order, or {@link C#INDEX_UNSET} if the + * timeline is empty. + */ + public int getLastWindowIndex(boolean shuffleModeEnabled) { + return isEmpty() ? C.INDEX_UNSET : getWindowCount() - 1; + } + + /** + * Returns the index of the first window in the playback order depending on whether shuffling is + * enabled. + * + * @param shuffleModeEnabled Whether shuffling is enabled. + * @return The index of the first window in the playback order, or {@link C#INDEX_UNSET} if the + * timeline is empty. + */ + public int getFirstWindowIndex(boolean shuffleModeEnabled) { + return isEmpty() ? C.INDEX_UNSET : 0; + } + + /** + * Populates a {@link Window} with data for the window at the specified index. + * + * @param windowIndex The index of the window. + * @param window The {@link Window} to populate. Must not be null. + * @return The populated {@link Window}, for convenience. + */ + public final Window getWindow(int windowIndex, Window window) { + return getWindow(windowIndex, window, /* defaultPositionProjectionUs= */ 0); + } + + /** @deprecated Use {@link #getWindow(int, Window)} instead. Tags will always be set. */ + @Deprecated + public final Window getWindow(int windowIndex, Window window, boolean setTag) { + return getWindow(windowIndex, window, /* defaultPositionProjectionUs= */ 0); + } + + /** + * Populates a {@link Window} with data for the window at the specified index. + * + * @param windowIndex The index of the window. + * @param window The {@link Window} to populate. Must not be null. + * @param defaultPositionProjectionUs A duration into the future that the populated window's + * default start position should be projected. + * @return The populated {@link Window}, for convenience. + */ + public abstract Window getWindow( + int windowIndex, Window window, long defaultPositionProjectionUs); + + /** + * Returns the number of periods in the timeline. + */ + public abstract int getPeriodCount(); + + /** + * Returns the index of the period after the period at index {@code periodIndex} depending on the + * {@code repeatMode} and whether shuffling is enabled. + * + * @param periodIndex Index of a period in the timeline. + * @param period A {@link Period} to be used internally. Must not be null. + * @param window A {@link Window} to be used internally. Must not be null. + * @param repeatMode A repeat mode. + * @param shuffleModeEnabled Whether shuffling is enabled. + * @return The index of the next period, or {@link C#INDEX_UNSET} if this is the last period. + */ + public final int getNextPeriodIndex(int periodIndex, Period period, Window window, + @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + int windowIndex = getPeriod(periodIndex, period).windowIndex; + if (getWindow(windowIndex, window).lastPeriodIndex == periodIndex) { + int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); + if (nextWindowIndex == C.INDEX_UNSET) { + return C.INDEX_UNSET; + } + return getWindow(nextWindowIndex, window).firstPeriodIndex; + } + return periodIndex + 1; + } + + /** + * Returns whether the given period is the last period of the timeline depending on the + * {@code repeatMode} and whether shuffling is enabled. + * + * @param periodIndex A period index. + * @param period A {@link Period} to be used internally. Must not be null. + * @param window A {@link Window} to be used internally. Must not be null. + * @param repeatMode A repeat mode. + * @param shuffleModeEnabled Whether shuffling is enabled. + * @return Whether the period of the given index is the last period of the timeline. + */ + public final boolean isLastPeriod(int periodIndex, Period period, Window window, + @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + return getNextPeriodIndex(periodIndex, period, window, repeatMode, shuffleModeEnabled) + == C.INDEX_UNSET; + } + + /** + * Calls {@link #getPeriodPosition(Window, Period, int, long, long)} with a zero default position + * projection. + */ + public final Pair getPeriodPosition( + Window window, Period period, int windowIndex, long windowPositionUs) { + return Assertions.checkNotNull( + getPeriodPosition( + window, period, windowIndex, windowPositionUs, /* defaultPositionProjectionUs= */ 0)); + } + + /** + * Converts (windowIndex, windowPositionUs) to the corresponding (periodUid, periodPositionUs). + * + * @param window A {@link Window} that may be overwritten. + * @param period A {@link Period} that may be overwritten. + * @param windowIndex The window index. + * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default + * start position. + * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the + * duration into the future by which the window's position should be projected. + * @return The corresponding (periodUid, periodPositionUs), or null if {@code #windowPositionUs} + * is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's + * position could not be projected by {@code defaultPositionProjectionUs}. + */ + @Nullable + public final Pair getPeriodPosition( + Window window, + Period period, + int windowIndex, + long windowPositionUs, + long defaultPositionProjectionUs) { + Assertions.checkIndex(windowIndex, 0, getWindowCount()); + getWindow(windowIndex, window, defaultPositionProjectionUs); + if (windowPositionUs == C.TIME_UNSET) { + windowPositionUs = window.getDefaultPositionUs(); + if (windowPositionUs == C.TIME_UNSET) { + return null; + } + } + int periodIndex = window.firstPeriodIndex; + long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs; + long periodDurationUs = getPeriod(periodIndex, period, /* setIds= */ true).getDurationUs(); + while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs + && periodIndex < window.lastPeriodIndex) { + periodPositionUs -= periodDurationUs; + periodDurationUs = getPeriod(++periodIndex, period, /* setIds= */ true).getDurationUs(); + } + return Pair.create(Assertions.checkNotNull(period.uid), periodPositionUs); + } + + /** + * Populates a {@link Period} with data for the period with the specified unique identifier. + * + * @param periodUid The unique identifier of the period. + * @param period The {@link Period} to populate. Must not be null. + * @return The populated {@link Period}, for convenience. + */ + public Period getPeriodByUid(Object periodUid, Period period) { + return getPeriod(getIndexOfPeriod(periodUid), period, /* setIds= */ true); + } + + /** + * Populates a {@link Period} with data for the period at the specified index. {@link Period#id} + * and {@link Period#uid} will be set to null. + * + * @param periodIndex The index of the period. + * @param period The {@link Period} to populate. Must not be null. + * @return The populated {@link Period}, for convenience. + */ + public final Period getPeriod(int periodIndex, Period period) { + return getPeriod(periodIndex, period, false); + } + + /** + * Populates a {@link Period} with data for the period at the specified index. + * + * @param periodIndex The index of the period. + * @param period The {@link Period} to populate. Must not be null. + * @param setIds Whether {@link Period#id} and {@link Period#uid} should be populated. If false, + * the fields will be set to null. The caller should pass false for efficiency reasons unless + * the fields are required. + * @return The populated {@link Period}, for convenience. + */ + public abstract Period getPeriod(int periodIndex, Period period, boolean setIds); + + /** + * Returns the index of the period identified by its unique {@link Period#uid}, or {@link + * C#INDEX_UNSET} if the period is not in the timeline. + * + * @param uid A unique identifier for a period. + * @return The index of the period, or {@link C#INDEX_UNSET} if the period was not found. + */ + public abstract int getIndexOfPeriod(Object uid); + + /** + * Returns the unique id of the period identified by its index in the timeline. + * + * @param periodIndex The index of the period. + * @return The unique id of the period. + */ + public abstract Object getUidOfPeriod(int periodIndex); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/WakeLockManager.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/WakeLockManager.java new file mode 100644 index 000000000000..368eb8aa0d85 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/WakeLockManager.java @@ -0,0 +1,101 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; + +/** + * Handles a {@link WakeLock}. + * + *

The handling of wake locks requires the {@link android.Manifest.permission#WAKE_LOCK} + * permission. + */ +/* package */ final class WakeLockManager { + + private static final String TAG = "WakeLockManager"; + private static final String WAKE_LOCK_TAG = "ExoPlayer:WakeLockManager"; + + @Nullable private final PowerManager powerManager; + @Nullable private WakeLock wakeLock; + private boolean enabled; + private boolean stayAwake; + + public WakeLockManager(Context context) { + powerManager = + (PowerManager) context.getApplicationContext().getSystemService(Context.POWER_SERVICE); + } + + /** + * Sets whether to enable the acquiring and releasing of the {@link WakeLock}. + * + *

By default, wake lock handling is not enabled. Enabling this will acquire the wake lock if + * necessary. Disabling this will release the wake lock if it is held. + * + *

Enabling {@link WakeLock} requires the {@link android.Manifest.permission#WAKE_LOCK}. + * + * @param enabled True if the player should handle a {@link WakeLock}, false otherwise. + */ + public void setEnabled(boolean enabled) { + if (enabled) { + if (wakeLock == null) { + if (powerManager == null) { + Log.w(TAG, "PowerManager is null, therefore not creating the WakeLock."); + return; + } + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG); + wakeLock.setReferenceCounted(false); + } + } + + this.enabled = enabled; + updateWakeLock(); + } + + /** + * Sets whether to acquire or release the {@link WakeLock}. + * + *

Please note this method requires wake lock handling to be enabled through setEnabled(boolean + * enable) to actually have an impact on the {@link WakeLock}. + * + * @param stayAwake True if the player should acquire the {@link WakeLock}. False if the player + * should release. + */ + public void setStayAwake(boolean stayAwake) { + this.stayAwake = stayAwake; + updateWakeLock(); + } + + // WakelockTimeout suppressed because the time the wake lock is needed for is unknown (could be + // listening to radio with screen off for multiple hours), therefore we can not determine a + // reasonable timeout that would not affect the user. + @SuppressLint("WakelockTimeout") + private void updateWakeLock() { + if (wakeLock == null) { + return; + } + + if (enabled && stayAwake) { + wakeLock.acquire(); + } else { + wakeLock.release(); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/WifiLockManager.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/WifiLockManager.java new file mode 100644 index 000000000000..1081dd39a883 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/WifiLockManager.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mozilla.thirdparty.com.google.android.exoplayer2; + +import android.content.Context; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.WifiLock; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; + +/** + * Handles a {@link WifiLock} + * + *

The handling of wifi locks requires the {@link android.Manifest.permission#WAKE_LOCK} + * permission. + */ +/* package */ final class WifiLockManager { + + private static final String TAG = "WifiLockManager"; + private static final String WIFI_LOCK_TAG = "ExoPlayer:WifiLockManager"; + + @Nullable private final WifiManager wifiManager; + @Nullable private WifiLock wifiLock; + private boolean enabled; + private boolean stayAwake; + + public WifiLockManager(Context context) { + wifiManager = + (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + } + + /** + * Sets whether to enable the usage of a {@link WifiLock}. + * + *

By default, wifi lock handling is not enabled. Enabling will acquire the wifi lock if + * necessary. Disabling will release the wifi lock if held. + * + *

Enabling {@link WifiLock} requires the {@link android.Manifest.permission#WAKE_LOCK}. + * + * @param enabled True if the player should handle a {@link WifiLock}. + */ + public void setEnabled(boolean enabled) { + if (enabled && wifiLock == null) { + if (wifiManager == null) { + Log.w(TAG, "WifiManager is null, therefore not creating the WifiLock."); + return; + } + wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, WIFI_LOCK_TAG); + wifiLock.setReferenceCounted(false); + } + + this.enabled = enabled; + updateWifiLock(); + } + + /** + * Sets whether to acquire or release the {@link WifiLock}. + * + *

The wifi lock will not be acquired unless handling has been enabled through {@link + * #setEnabled(boolean)}. + * + * @param stayAwake True if the player should acquire the {@link WifiLock}. False if it should + * release. + */ + public void setStayAwake(boolean stayAwake) { + this.stayAwake = stayAwake; + updateWifiLock(); + } + + private void updateWifiLock() { + if (wifiLock == null) { + return; + } + + if (enabled && stayAwake) { + wifiLock.acquire(); + } else { + wifiLock.release(); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsCollector.java new file mode 100644 index 000000000000..6bdb4c77271e --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -0,0 +1,881 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.analytics; + +import android.view.Surface; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.PlaybackSuppressionReason; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline.Period; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline.Window; +import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioAttributes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioRendererEventListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderCounters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoRendererEventListener; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +/** + * Data collector which is able to forward analytics events to {@link AnalyticsListener}s by + * listening to all available ExoPlayer listeners. + */ +public class AnalyticsCollector + implements Player.EventListener, + MetadataOutput, + AudioRendererEventListener, + VideoRendererEventListener, + MediaSourceEventListener, + BandwidthMeter.EventListener, + DefaultDrmSessionEventListener, + VideoListener, + AudioListener { + + private final CopyOnWriteArraySet listeners; + private final Clock clock; + private final Window window; + private final MediaPeriodQueueTracker mediaPeriodQueueTracker; + + private @MonotonicNonNull Player player; + + /** + * Creates an analytics collector. + * + * @param clock A {@link Clock} used to generate timestamps. + */ + public AnalyticsCollector(Clock clock) { + this.clock = Assertions.checkNotNull(clock); + listeners = new CopyOnWriteArraySet<>(); + mediaPeriodQueueTracker = new MediaPeriodQueueTracker(); + window = new Window(); + } + + /** + * Adds a listener for analytics events. + * + * @param listener The listener to add. + */ + public void addListener(AnalyticsListener listener) { + listeners.add(listener); + } + + /** + * Removes a previously added analytics event listener. + * + * @param listener The listener to remove. + */ + public void removeListener(AnalyticsListener listener) { + listeners.remove(listener); + } + + /** + * Sets the player for which data will be collected. Must only be called if no player has been set + * yet or the current player is idle. + * + * @param player The {@link Player} for which data will be collected. + */ + public void setPlayer(Player player) { + Assertions.checkState( + this.player == null || mediaPeriodQueueTracker.mediaPeriodInfoQueue.isEmpty()); + this.player = Assertions.checkNotNull(player); + } + + // External events. + + /** + * Notify analytics collector that a seek operation will start. Should be called before the player + * adjusts its state and position to the seek. + */ + public final void notifySeekStarted() { + if (!mediaPeriodQueueTracker.isSeeking()) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + mediaPeriodQueueTracker.onSeekStarted(); + for (AnalyticsListener listener : listeners) { + listener.onSeekStarted(eventTime); + } + } + } + + /** + * Resets the analytics collector for a new media source. Should be called before the player is + * prepared with a new media source. + */ + public final void resetForNewMediaSource() { + // Copying the list is needed because onMediaPeriodReleased will modify the list. + List mediaPeriodInfos = + new ArrayList<>(mediaPeriodQueueTracker.mediaPeriodInfoQueue); + for (MediaPeriodInfo mediaPeriodInfo : mediaPeriodInfos) { + onMediaPeriodReleased(mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId); + } + } + + // MetadataOutput implementation. + + @Override + public final void onMetadata(Metadata metadata) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onMetadata(eventTime, metadata); + } + } + + // AudioRendererEventListener implementation. + + @Override + public final void onAudioEnabled(DecoderCounters counters) { + // The renderers are only enabled after we changed the playing media period. + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_AUDIO, counters); + } + } + + @Override + public final void onAudioDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDecoderInitialized( + eventTime, C.TRACK_TYPE_AUDIO, decoderName, initializationDurationMs); + } + } + + @Override + public final void onAudioInputFormatChanged(Format format) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format); + } + } + + @Override + public final void onAudioSinkUnderrun( + int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onAudioUnderrun(eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + } + } + + @Override + public final void onAudioDisabled(DecoderCounters counters) { + // The renderers are disabled after we changed the playing media period on the playback thread + // but before this change is reported to the app thread. + EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_AUDIO, counters); + } + } + + // AudioListener implementation. + + @Override + public final void onAudioSessionId(int audioSessionId) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onAudioSessionId(eventTime, audioSessionId); + } + } + + @Override + public void onAudioAttributesChanged(AudioAttributes audioAttributes) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onAudioAttributesChanged(eventTime, audioAttributes); + } + } + + @Override + public void onVolumeChanged(float audioVolume) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onVolumeChanged(eventTime, audioVolume); + } + } + + // VideoRendererEventListener implementation. + + @Override + public final void onVideoEnabled(DecoderCounters counters) { + // The renderers are only enabled after we changed the playing media period. + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_VIDEO, counters); + } + } + + @Override + public final void onVideoDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDecoderInitialized( + eventTime, C.TRACK_TYPE_VIDEO, decoderName, initializationDurationMs); + } + } + + @Override + public final void onVideoInputFormatChanged(Format format) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format); + } + } + + @Override + public final void onDroppedFrames(int count, long elapsedMs) { + EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDroppedVideoFrames(eventTime, count, elapsedMs); + } + } + + @Override + public final void onVideoDisabled(DecoderCounters counters) { + // The renderers are disabled after we changed the playing media period on the playback thread + // but before this change is reported to the app thread. + EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, counters); + } + } + + @Override + public final void onRenderedFirstFrame(@Nullable Surface surface) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onRenderedFirstFrame(eventTime, surface); + } + } + + // VideoListener implementation. + + @Override + public final void onRenderedFirstFrame() { + // Do nothing. Already reported in VideoRendererEventListener.onRenderedFirstFrame. + } + + @Override + public final void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onVideoSizeChanged( + eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio); + } + } + + @Override + public void onSurfaceSizeChanged(int width, int height) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onSurfaceSizeChanged(eventTime, width, height); + } + } + + // MediaSourceEventListener implementation. + + @Override + public final void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { + mediaPeriodQueueTracker.onMediaPeriodCreated(windowIndex, mediaPeriodId); + EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onMediaPeriodCreated(eventTime); + } + } + + @Override + public final void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { + EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); + if (mediaPeriodQueueTracker.onMediaPeriodReleased(mediaPeriodId)) { + for (AnalyticsListener listener : listeners) { + listener.onMediaPeriodReleased(eventTime); + } + } + } + + @Override + public final void onLoadStarted( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData) { + EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onLoadStarted(eventTime, loadEventInfo, mediaLoadData); + } + } + + @Override + public final void onLoadCompleted( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData) { + EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onLoadCompleted(eventTime, loadEventInfo, mediaLoadData); + } + } + + @Override + public final void onLoadCanceled( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData) { + EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onLoadCanceled(eventTime, loadEventInfo, mediaLoadData); + } + } + + @Override + public final void onLoadError( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onLoadError(eventTime, loadEventInfo, mediaLoadData, error, wasCanceled); + } + } + + @Override + public final void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) { + mediaPeriodQueueTracker.onReadingStarted(mediaPeriodId); + EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onReadingStarted(eventTime); + } + } + + @Override + public final void onUpstreamDiscarded( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { + EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onUpstreamDiscarded(eventTime, mediaLoadData); + } + } + + @Override + public final void onDownstreamFormatChanged( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { + EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onDownstreamFormatChanged(eventTime, mediaLoadData); + } + } + + // Player.EventListener implementation. + + // TODO: Add onFinishedReportingChanges to Player.EventListener to know when a set of simultaneous + // callbacks finished. This helps to assign exactly the same EventTime to all of them instead of + // having slightly different real times. + + @Override + public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { + mediaPeriodQueueTracker.onTimelineChanged(timeline); + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onTimelineChanged(eventTime, reason); + } + } + + @Override + public final void onTracksChanged( + TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onTracksChanged(eventTime, trackGroups, trackSelections); + } + } + + @Override + public final void onLoadingChanged(boolean isLoading) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onLoadingChanged(eventTime, isLoading); + } + } + + @Override + public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState); + } + } + + @Override + public void onPlaybackSuppressionReasonChanged( + @PlaybackSuppressionReason int playbackSuppressionReason) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onPlaybackSuppressionReasonChanged(eventTime, playbackSuppressionReason); + } + } + + @Override + public void onIsPlayingChanged(boolean isPlaying) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onIsPlayingChanged(eventTime, isPlaying); + } + } + + @Override + public final void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onRepeatModeChanged(eventTime, repeatMode); + } + } + + @Override + public final void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onShuffleModeChanged(eventTime, shuffleModeEnabled); + } + } + + @Override + public final void onPlayerError(ExoPlaybackException error) { + EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onPlayerError(eventTime, error); + } + } + + @Override + public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + mediaPeriodQueueTracker.onPositionDiscontinuity(reason); + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onPositionDiscontinuity(eventTime, reason); + } + } + + @Override + public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onPlaybackParametersChanged(eventTime, playbackParameters); + } + } + + @Override + public final void onSeekProcessed() { + if (mediaPeriodQueueTracker.isSeeking()) { + mediaPeriodQueueTracker.onSeekProcessed(); + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onSeekProcessed(eventTime); + } + } + } + + // BandwidthMeter.Listener implementation. + + @Override + public final void onBandwidthSample(int elapsedMs, long bytes, long bitrate) { + EventTime eventTime = generateLoadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onBandwidthEstimate(eventTime, elapsedMs, bytes, bitrate); + } + } + + // DefaultDrmSessionManager.EventListener implementation. + + @Override + public final void onDrmSessionAcquired() { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDrmSessionAcquired(eventTime); + } + } + + @Override + public final void onDrmKeysLoaded() { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDrmKeysLoaded(eventTime); + } + } + + @Override + public final void onDrmSessionManagerError(Exception error) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDrmSessionManagerError(eventTime, error); + } + } + + @Override + public final void onDrmKeysRestored() { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDrmKeysRestored(eventTime); + } + } + + @Override + public final void onDrmKeysRemoved() { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDrmKeysRemoved(eventTime); + } + } + + @Override + public final void onDrmSessionReleased() { + EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onDrmSessionReleased(eventTime); + } + } + + // Internal methods. + + /** Returns read-only set of registered listeners. */ + protected Set getListeners() { + return Collections.unmodifiableSet(listeners); + } + + /** Returns a new {@link EventTime} for the specified timeline, window and media period id. */ + @RequiresNonNull("player") + protected EventTime generateEventTime( + Timeline timeline, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + if (timeline.isEmpty()) { + // Ensure media period id is only reported together with a valid timeline. + mediaPeriodId = null; + } + long realtimeMs = clock.elapsedRealtime(); + long eventPositionMs; + boolean isInCurrentWindow = + timeline == player.getCurrentTimeline() && windowIndex == player.getCurrentWindowIndex(); + if (mediaPeriodId != null && mediaPeriodId.isAd()) { + boolean isCurrentAd = + isInCurrentWindow + && player.getCurrentAdGroupIndex() == mediaPeriodId.adGroupIndex + && player.getCurrentAdIndexInAdGroup() == mediaPeriodId.adIndexInAdGroup; + // Assume start position of 0 for future ads. + eventPositionMs = isCurrentAd ? player.getCurrentPosition() : 0; + } else if (isInCurrentWindow) { + eventPositionMs = player.getContentPosition(); + } else { + // Assume default start position for future content windows. If timeline is not available yet, + // assume start position of 0. + eventPositionMs = + timeline.isEmpty() ? 0 : timeline.getWindow(windowIndex, window).getDefaultPositionMs(); + } + return new EventTime( + realtimeMs, + timeline, + windowIndex, + mediaPeriodId, + eventPositionMs, + player.getCurrentPosition(), + player.getTotalBufferedDuration()); + } + + private EventTime generateEventTime(@Nullable MediaPeriodInfo mediaPeriodInfo) { + Assertions.checkNotNull(player); + if (mediaPeriodInfo == null) { + int windowIndex = player.getCurrentWindowIndex(); + mediaPeriodInfo = mediaPeriodQueueTracker.tryResolveWindowIndex(windowIndex); + if (mediaPeriodInfo == null) { + Timeline timeline = player.getCurrentTimeline(); + boolean windowIsInTimeline = windowIndex < timeline.getWindowCount(); + return generateEventTime( + windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null); + } + } + return generateEventTime( + mediaPeriodInfo.timeline, mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId); + } + + private EventTime generateLastReportedPlayingMediaPeriodEventTime() { + return generateEventTime(mediaPeriodQueueTracker.getLastReportedPlayingMediaPeriod()); + } + + private EventTime generatePlayingMediaPeriodEventTime() { + return generateEventTime(mediaPeriodQueueTracker.getPlayingMediaPeriod()); + } + + private EventTime generateReadingMediaPeriodEventTime() { + return generateEventTime(mediaPeriodQueueTracker.getReadingMediaPeriod()); + } + + private EventTime generateLoadingMediaPeriodEventTime() { + return generateEventTime(mediaPeriodQueueTracker.getLoadingMediaPeriod()); + } + + private EventTime generateMediaPeriodEventTime( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + Assertions.checkNotNull(player); + if (mediaPeriodId != null) { + MediaPeriodInfo mediaPeriodInfo = mediaPeriodQueueTracker.getMediaPeriodInfo(mediaPeriodId); + return mediaPeriodInfo != null + ? generateEventTime(mediaPeriodInfo) + : generateEventTime(Timeline.EMPTY, windowIndex, mediaPeriodId); + } + Timeline timeline = player.getCurrentTimeline(); + boolean windowIsInTimeline = windowIndex < timeline.getWindowCount(); + return generateEventTime( + windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null); + } + + /** Keeps track of the active media periods and currently playing and reading media period. */ + private static final class MediaPeriodQueueTracker { + + // TODO: Investigate reporting MediaPeriodId in renderer events and adding a listener of queue + // changes, which would hopefully remove the need to track the queue here. + + private final ArrayList mediaPeriodInfoQueue; + private final HashMap mediaPeriodIdToInfo; + private final Period period; + + @Nullable private MediaPeriodInfo lastPlayingMediaPeriod; + @Nullable private MediaPeriodInfo lastReportedPlayingMediaPeriod; + @Nullable private MediaPeriodInfo readingMediaPeriod; + private Timeline timeline; + private boolean isSeeking; + + public MediaPeriodQueueTracker() { + mediaPeriodInfoQueue = new ArrayList<>(); + mediaPeriodIdToInfo = new HashMap<>(); + period = new Period(); + timeline = Timeline.EMPTY; + } + + /** + * Returns the {@link MediaPeriodInfo} of the media period in the front of the queue. This is + * the playing media period unless the player hasn't started playing yet (in which case it is + * the loading media period or null). While the player is seeking or preparing, this method will + * always return null to reflect the uncertainty about the current playing period. May also be + * null, if the timeline is empty or no media period is active yet. + */ + @Nullable + public MediaPeriodInfo getPlayingMediaPeriod() { + return mediaPeriodInfoQueue.isEmpty() || timeline.isEmpty() || isSeeking + ? null + : mediaPeriodInfoQueue.get(0); + } + + /** + * Returns the {@link MediaPeriodInfo} of the currently playing media period. This is the + * publicly reported period which should always match {@link Player#getCurrentPeriodIndex()} + * unless the player is currently seeking or being prepared in which case the previous period is + * reported until the seek or preparation is processed. May be null, if no media period is + * active yet. + */ + @Nullable + public MediaPeriodInfo getLastReportedPlayingMediaPeriod() { + return lastReportedPlayingMediaPeriod; + } + + /** + * Returns the {@link MediaPeriodInfo} of the media period currently being read by the player. + * May be null, if the player is not reading a media period. + */ + @Nullable + public MediaPeriodInfo getReadingMediaPeriod() { + return readingMediaPeriod; + } + + /** + * Returns the {@link MediaPeriodInfo} of the media period at the end of the queue which is + * currently loading or will be the next one loading. May be null, if no media period is active + * yet. + */ + @Nullable + public MediaPeriodInfo getLoadingMediaPeriod() { + return mediaPeriodInfoQueue.isEmpty() + ? null + : mediaPeriodInfoQueue.get(mediaPeriodInfoQueue.size() - 1); + } + + /** Returns the {@link MediaPeriodInfo} for the given {@link MediaPeriodId}. */ + @Nullable + public MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId mediaPeriodId) { + return mediaPeriodIdToInfo.get(mediaPeriodId); + } + + /** Returns whether the player is currently seeking. */ + public boolean isSeeking() { + return isSeeking; + } + + /** + * Tries to find an existing media period info from the specified window index. Only returns a + * non-null media period info if there is a unique, unambiguous match. + */ + @Nullable + public MediaPeriodInfo tryResolveWindowIndex(int windowIndex) { + MediaPeriodInfo match = null; + for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) { + MediaPeriodInfo info = mediaPeriodInfoQueue.get(i); + int periodIndex = timeline.getIndexOfPeriod(info.mediaPeriodId.periodUid); + if (periodIndex != C.INDEX_UNSET + && timeline.getPeriod(periodIndex, period).windowIndex == windowIndex) { + if (match != null) { + // Ambiguous match. + return null; + } + match = info; + } + } + return match; + } + + /** Updates the queue with a reported position discontinuity . */ + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; + } + + /** Updates the queue with a reported timeline change. */ + public void onTimelineChanged(Timeline timeline) { + for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) { + MediaPeriodInfo newMediaPeriodInfo = + updateMediaPeriodInfoToNewTimeline(mediaPeriodInfoQueue.get(i), timeline); + mediaPeriodInfoQueue.set(i, newMediaPeriodInfo); + mediaPeriodIdToInfo.put(newMediaPeriodInfo.mediaPeriodId, newMediaPeriodInfo); + } + if (readingMediaPeriod != null) { + readingMediaPeriod = updateMediaPeriodInfoToNewTimeline(readingMediaPeriod, timeline); + } + this.timeline = timeline; + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; + } + + /** Updates the queue with a reported start of seek. */ + public void onSeekStarted() { + isSeeking = true; + } + + /** Updates the queue with a reported processed seek. */ + public void onSeekProcessed() { + isSeeking = false; + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; + } + + /** Updates the queue with a newly created media period. */ + public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { + int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid); + boolean isInTimeline = periodIndex != C.INDEX_UNSET; + MediaPeriodInfo mediaPeriodInfo = + new MediaPeriodInfo( + mediaPeriodId, + isInTimeline ? timeline : Timeline.EMPTY, + isInTimeline ? timeline.getPeriod(periodIndex, period).windowIndex : windowIndex); + mediaPeriodInfoQueue.add(mediaPeriodInfo); + mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo); + lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); + if (mediaPeriodInfoQueue.size() == 1 && !timeline.isEmpty()) { + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; + } + } + + /** + * Updates the queue with a released media period. Returns whether the media period was still in + * the queue. + */ + public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId) { + MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId); + if (mediaPeriodInfo == null) { + // The media period has already been removed from the queue in resetForNewMediaSource(). + return false; + } + mediaPeriodInfoQueue.remove(mediaPeriodInfo); + if (readingMediaPeriod != null && mediaPeriodId.equals(readingMediaPeriod.mediaPeriodId)) { + readingMediaPeriod = mediaPeriodInfoQueue.isEmpty() ? null : mediaPeriodInfoQueue.get(0); + } + if (!mediaPeriodInfoQueue.isEmpty()) { + lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); + } + return true; + } + + /** Update the queue with a change in the reading media period. */ + public void onReadingStarted(MediaPeriodId mediaPeriodId) { + readingMediaPeriod = mediaPeriodIdToInfo.get(mediaPeriodId); + } + + private MediaPeriodInfo updateMediaPeriodInfoToNewTimeline( + MediaPeriodInfo info, Timeline newTimeline) { + int newPeriodIndex = newTimeline.getIndexOfPeriod(info.mediaPeriodId.periodUid); + if (newPeriodIndex == C.INDEX_UNSET) { + // Media period is not yet or no longer available in the new timeline. Keep it as it is. + return info; + } + int newWindowIndex = newTimeline.getPeriod(newPeriodIndex, period).windowIndex; + return new MediaPeriodInfo(info.mediaPeriodId, newTimeline, newWindowIndex); + } + } + + /** Information about a media period and its associated timeline. */ + private static final class MediaPeriodInfo { + + /** The {@link MediaPeriodId} of the media period. */ + public final MediaPeriodId mediaPeriodId; + /** + * The {@link Timeline} in which the media period can be found. Or {@link Timeline#EMPTY} if the + * media period is not part of a known timeline yet. + */ + public final Timeline timeline; + /** + * The window index of the media period in the timeline. If the timeline is empty, this is the + * prospective window index. + */ + public final int windowIndex; + + public MediaPeriodInfo(MediaPeriodId mediaPeriodId, Timeline timeline, int windowIndex) { + this.mediaPeriodId = mediaPeriodId; + this.timeline = timeline; + this.windowIndex = windowIndex; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsListener.java new file mode 100644 index 000000000000..a265268c1980 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -0,0 +1,514 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.analytics; + +import android.view.Surface; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.DiscontinuityReason; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.PlaybackSuppressionReason; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.TimelineChangeReason; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioAttributes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioSink; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderCounters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import java.io.IOException; + +/** + * A listener for analytics events. + * + *

All events are recorded with an {@link EventTime} specifying the elapsed real time and media + * time at the time of the event. + * + *

All methods have no-op default implementations to allow selective overrides. + */ +public interface AnalyticsListener { + + /** Time information of an event. */ + final class EventTime { + + /** + * Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at the time of the + * event, in milliseconds. + */ + public final long realtimeMs; + + /** Timeline at the time of the event. */ + public final Timeline timeline; + + /** + * Window index in the {@link #timeline} this event belongs to, or the prospective window index + * if the timeline is not yet known and empty. + */ + public final int windowIndex; + + /** + * Media period identifier for the media period this event belongs to, or {@code null} if the + * event is not associated with a specific media period. + */ + @Nullable public final MediaPeriodId mediaPeriodId; + + /** + * Position in the window or ad this event belongs to at the time of the event, in milliseconds. + */ + public final long eventPlaybackPositionMs; + + /** + * Position in the current timeline window ({@link Player#getCurrentWindowIndex()}) or the + * currently playing ad at the time of the event, in milliseconds. + */ + public final long currentPlaybackPositionMs; + + /** + * Total buffered duration from {@link #currentPlaybackPositionMs} at the time of the event, in + * milliseconds. This includes pre-buffered data for subsequent ads and windows. + */ + public final long totalBufferedDurationMs; + + /** + * @param realtimeMs Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at + * the time of the event, in milliseconds. + * @param timeline Timeline at the time of the event. + * @param windowIndex Window index in the {@link #timeline} this event belongs to, or the + * prospective window index if the timeline is not yet known and empty. + * @param mediaPeriodId Media period identifier for the media period this event belongs to, or + * {@code null} if the event is not associated with a specific media period. + * @param eventPlaybackPositionMs Position in the window or ad this event belongs to at the time + * of the event, in milliseconds. + * @param currentPlaybackPositionMs Position in the current timeline window ({@link + * Player#getCurrentWindowIndex()}) or the currently playing ad at the time of the event, in + * milliseconds. + * @param totalBufferedDurationMs Total buffered duration from {@link + * #currentPlaybackPositionMs} at the time of the event, in milliseconds. This includes + * pre-buffered data for subsequent ads and windows. + */ + public EventTime( + long realtimeMs, + Timeline timeline, + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + long eventPlaybackPositionMs, + long currentPlaybackPositionMs, + long totalBufferedDurationMs) { + this.realtimeMs = realtimeMs; + this.timeline = timeline; + this.windowIndex = windowIndex; + this.mediaPeriodId = mediaPeriodId; + this.eventPlaybackPositionMs = eventPlaybackPositionMs; + this.currentPlaybackPositionMs = currentPlaybackPositionMs; + this.totalBufferedDurationMs = totalBufferedDurationMs; + } + } + + /** + * Called when the player state changed. + * + * @param eventTime The event time. + * @param playWhenReady Whether the playback will proceed when ready. + * @param playbackState The new {@link Player.State playback state}. + */ + default void onPlayerStateChanged( + EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) {} + + /** + * Called when playback suppression reason changed. + * + * @param eventTime The event time. + * @param playbackSuppressionReason The new {@link PlaybackSuppressionReason}. + */ + default void onPlaybackSuppressionReasonChanged( + EventTime eventTime, @PlaybackSuppressionReason int playbackSuppressionReason) {} + + /** + * Called when the player starts or stops playing. + * + * @param eventTime The event time. + * @param isPlaying Whether the player is playing. + */ + default void onIsPlayingChanged(EventTime eventTime, boolean isPlaying) {} + + /** + * Called when the timeline changed. + * + * @param eventTime The event time. + * @param reason The reason for the timeline change. + */ + default void onTimelineChanged(EventTime eventTime, @TimelineChangeReason int reason) {} + + /** + * Called when a position discontinuity occurred. + * + * @param eventTime The event time. + * @param reason The reason for the position discontinuity. + */ + default void onPositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason) {} + + /** + * Called when a seek operation started. + * + * @param eventTime The event time. + */ + default void onSeekStarted(EventTime eventTime) {} + + /** + * Called when a seek operation was processed. + * + * @param eventTime The event time. + */ + default void onSeekProcessed(EventTime eventTime) {} + + /** + * Called when the playback parameters changed. + * + * @param eventTime The event time. + * @param playbackParameters The new playback parameters. + */ + default void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) {} + + /** + * Called when the repeat mode changed. + * + * @param eventTime The event time. + * @param repeatMode The new repeat mode. + */ + default void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode) {} + + /** + * Called when the shuffle mode changed. + * + * @param eventTime The event time. + * @param shuffleModeEnabled Whether the shuffle mode is enabled. + */ + default void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {} + + /** + * Called when the player starts or stops loading data from a source. + * + * @param eventTime The event time. + * @param isLoading Whether the player is loading. + */ + default void onLoadingChanged(EventTime eventTime, boolean isLoading) {} + + /** + * Called when a fatal player error occurred. + * + * @param eventTime The event time. + * @param error The error. + */ + default void onPlayerError(EventTime eventTime, ExoPlaybackException error) {} + + /** + * Called when the available or selected tracks for the renderers changed. + * + * @param eventTime The event time. + * @param trackGroups The available tracks. May be empty. + * @param trackSelections The track selections for each renderer. May contain null elements. + */ + default void onTracksChanged( + EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {} + + /** + * Called when a media source started loading data. + * + * @param eventTime The event time. + * @param loadEventInfo The {@link LoadEventInfo} defining the load event. + * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. + */ + default void onLoadStarted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {} + + /** + * Called when a media source completed loading data. + * + * @param eventTime The event time. + * @param loadEventInfo The {@link LoadEventInfo} defining the load event. + * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. + */ + default void onLoadCompleted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {} + + /** + * Called when a media source canceled loading data. + * + * @param eventTime The event time. + * @param loadEventInfo The {@link LoadEventInfo} defining the load event. + * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. + */ + default void onLoadCanceled( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {} + + /** + * Called when a media source loading error occurred. These errors are just for informational + * purposes and the player may recover. + * + * @param eventTime The event time. + * @param loadEventInfo The {@link LoadEventInfo} defining the load event. + * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. + * @param error The load error. + * @param wasCanceled Whether the load was canceled as a result of the error. + */ + default void onLoadError( + EventTime eventTime, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) {} + + /** + * Called when the downstream format sent to the renderers changed. + * + * @param eventTime The event time. + * @param mediaLoadData The {@link MediaLoadData} defining the newly selected media data. + */ + default void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {} + + /** + * Called when data is removed from the back of a media buffer, typically so that it can be + * re-buffered in a different format. + * + * @param eventTime The event time. + * @param mediaLoadData The {@link MediaLoadData} defining the media being discarded. + */ + default void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) {} + + /** + * Called when a media source created a media period. + * + * @param eventTime The event time. + */ + default void onMediaPeriodCreated(EventTime eventTime) {} + + /** + * Called when a media source released a media period. + * + * @param eventTime The event time. + */ + default void onMediaPeriodReleased(EventTime eventTime) {} + + /** + * Called when the player started reading a media period. + * + * @param eventTime The event time. + */ + default void onReadingStarted(EventTime eventTime) {} + + /** + * Called when the bandwidth estimate for the current data source has been updated. + * + * @param eventTime The event time. + * @param totalLoadTimeMs The total time spend loading this update is based on, in milliseconds. + * @param totalBytesLoaded The total bytes loaded this update is based on. + * @param bitrateEstimate The bandwidth estimate, in bits per second. + */ + default void onBandwidthEstimate( + EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {} + + /** + * Called when the output surface size changed. + * + * @param eventTime The event time. + * @param width The surface width in pixels. May be {@link C#LENGTH_UNSET} if unknown, or 0 if the + * video is not rendered onto a surface. + * @param height The surface height in pixels. May be {@link C#LENGTH_UNSET} if unknown, or 0 if + * the video is not rendered onto a surface. + */ + default void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {} + + /** + * Called when there is {@link Metadata} associated with the current playback time. + * + * @param eventTime The event time. + * @param metadata The metadata. + */ + default void onMetadata(EventTime eventTime, Metadata metadata) {} + + /** + * Called when an audio or video decoder has been enabled. + * + * @param eventTime The event time. + * @param trackType The track type of the enabled decoder. Either {@link C#TRACK_TYPE_AUDIO} or + * {@link C#TRACK_TYPE_VIDEO}. + * @param decoderCounters The accumulated event counters associated with this decoder. + */ + default void onDecoderEnabled( + EventTime eventTime, int trackType, DecoderCounters decoderCounters) {} + + /** + * Called when an audio or video decoder has been initialized. + * + * @param eventTime The event time. + * @param trackType The track type of the initialized decoder. Either {@link C#TRACK_TYPE_AUDIO} + * or {@link C#TRACK_TYPE_VIDEO}. + * @param decoderName The decoder that was created. + * @param initializationDurationMs Time taken to initialize the decoder, in milliseconds. + */ + default void onDecoderInitialized( + EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) {} + + /** + * Called when an audio or video decoder input format changed. + * + * @param eventTime The event time. + * @param trackType The track type of the decoder whose format changed. Either {@link + * C#TRACK_TYPE_AUDIO} or {@link C#TRACK_TYPE_VIDEO}. + * @param format The new input format for the decoder. + */ + default void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {} + + /** + * Called when an audio or video decoder has been disabled. + * + * @param eventTime The event time. + * @param trackType The track type of the disabled decoder. Either {@link C#TRACK_TYPE_AUDIO} or + * {@link C#TRACK_TYPE_VIDEO}. + * @param decoderCounters The accumulated event counters associated with this decoder. + */ + default void onDecoderDisabled( + EventTime eventTime, int trackType, DecoderCounters decoderCounters) {} + + /** + * Called when the audio session id is set. + * + * @param eventTime The event time. + * @param audioSessionId The audio session id. + */ + default void onAudioSessionId(EventTime eventTime, int audioSessionId) {} + + /** + * Called when the audio attributes change. + * + * @param eventTime The event time. + * @param audioAttributes The audio attributes. + */ + default void onAudioAttributesChanged(EventTime eventTime, AudioAttributes audioAttributes) {} + + /** + * Called when the volume changes. + * + * @param eventTime The event time. + * @param volume The new volume, with 0 being silence and 1 being unity gain. + */ + default void onVolumeChanged(EventTime eventTime, float volume) {} + + /** + * Called when an audio underrun occurred. + * + * @param eventTime The event time. + * @param bufferSize The size of the {@link AudioSink}'s buffer, in bytes. + * @param bufferSizeMs The size of the {@link AudioSink}'s buffer, in milliseconds, if it is + * configured for PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output, + * as the buffered media can have a variable bitrate so the duration may be unknown. + * @param elapsedSinceLastFeedMs The time since the {@link AudioSink} was last fed data. + */ + default void onAudioUnderrun( + EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {} + + /** + * Called after video frames have been dropped. + * + * @param eventTime The event time. + * @param droppedFrames The number of dropped frames since the last call to this method. + * @param elapsedMs The duration in milliseconds over which the frames were dropped. This duration + * is timed from when the renderer was started or from when dropped frames were last reported + * (whichever was more recent), and not from when the first of the reported drops occurred. + */ + default void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {} + + /** + * Called before a frame is rendered for the first time since setting the surface, and each time + * there's a change in the size or pixel aspect ratio of the video being rendered. + * + * @param eventTime The event time. + * @param width The width of the video. + * @param height The height of the video. + * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise + * rotation in degrees that the application should apply for the video for it to be rendered + * in the correct orientation. This value will always be zero on API levels 21 and above, + * since the renderer will apply all necessary rotations internally. + * @param pixelWidthHeightRatio The width to height ratio of each pixel. + */ + default void onVideoSizeChanged( + EventTime eventTime, + int width, + int height, + int unappliedRotationDegrees, + float pixelWidthHeightRatio) {} + + /** + * Called when a frame is rendered for the first time since setting the surface, and when a frame + * is rendered for the first time since the renderer was reset. + * + * @param eventTime The event time. + * @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if + * the renderer renders to something that isn't a {@link Surface}. + */ + default void onRenderedFirstFrame(EventTime eventTime, @Nullable Surface surface) {} + + /** + * Called each time a drm session is acquired. + * + * @param eventTime The event time. + */ + default void onDrmSessionAcquired(EventTime eventTime) {} + + /** + * Called each time drm keys are loaded. + * + * @param eventTime The event time. + */ + default void onDrmKeysLoaded(EventTime eventTime) {} + + /** + * Called when a drm error occurs. These errors are just for informational purposes and the player + * may recover. + * + * @param eventTime The event time. + * @param error The error. + */ + default void onDrmSessionManagerError(EventTime eventTime, Exception error) {} + + /** + * Called each time offline drm keys are restored. + * + * @param eventTime The event time. + */ + default void onDrmKeysRestored(EventTime eventTime) {} + + /** + * Called each time offline drm keys are removed. + * + * @param eventTime The event time. + */ + default void onDrmKeysRemoved(EventTime eventTime) {} + + /** + * Called each time a drm session is released. + * + * @param eventTime The event time. + */ + default void onDrmSessionReleased(EventTime eventTime) {} +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java new file mode 100644 index 000000000000..f56ac3fef0d6 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.analytics; + +/** + * @deprecated Use {@link AnalyticsListener} directly for selective overrides as all methods are + * implemented as no-op default methods. + */ +@Deprecated +public abstract class DefaultAnalyticsListener implements AnalyticsListener {} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java new file mode 100644 index 000000000000..710934bd36df --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java @@ -0,0 +1,355 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.analytics; + +import android.util.Base64; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.DiscontinuityReason; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Random; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +/** + * Default {@link PlaybackSessionManager} which instantiates a new session for each window in the + * timeline and also for each ad within the windows. + * + *

Sessions are identified by Base64-encoded, URL-safe, random strings. + */ +public final class DefaultPlaybackSessionManager implements PlaybackSessionManager { + + private static final Random RANDOM = new Random(); + private static final int SESSION_ID_LENGTH = 12; + + private final Timeline.Window window; + private final Timeline.Period period; + private final HashMap sessions; + + private @MonotonicNonNull Listener listener; + private Timeline currentTimeline; + @Nullable private MediaPeriodId currentMediaPeriodId; + @Nullable private String activeSessionId; + + /** Creates session manager. */ + public DefaultPlaybackSessionManager() { + window = new Timeline.Window(); + period = new Timeline.Period(); + sessions = new HashMap<>(); + currentTimeline = Timeline.EMPTY; + } + + @Override + public void setListener(Listener listener) { + this.listener = listener; + } + + @Override + public synchronized String getSessionForMediaPeriodId( + Timeline timeline, MediaPeriodId mediaPeriodId) { + int windowIndex = timeline.getPeriodByUid(mediaPeriodId.periodUid, period).windowIndex; + return getOrAddSession(windowIndex, mediaPeriodId).sessionId; + } + + @Override + public synchronized boolean belongsToSession(EventTime eventTime, String sessionId) { + SessionDescriptor sessionDescriptor = sessions.get(sessionId); + if (sessionDescriptor == null) { + return false; + } + sessionDescriptor.maybeSetWindowSequenceNumber(eventTime.windowIndex, eventTime.mediaPeriodId); + return sessionDescriptor.belongsToSession(eventTime.windowIndex, eventTime.mediaPeriodId); + } + + @Override + public synchronized void updateSessions(EventTime eventTime) { + boolean isObviouslyFinished = + eventTime.mediaPeriodId != null + && currentMediaPeriodId != null + && eventTime.mediaPeriodId.windowSequenceNumber + < currentMediaPeriodId.windowSequenceNumber; + if (!isObviouslyFinished) { + SessionDescriptor descriptor = + getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId); + if (!descriptor.isCreated) { + descriptor.isCreated = true; + Assertions.checkNotNull(listener).onSessionCreated(eventTime, descriptor.sessionId); + if (activeSessionId == null) { + updateActiveSession(eventTime, descriptor); + } + } + } + } + + @Override + public synchronized void handleTimelineUpdate(EventTime eventTime) { + Assertions.checkNotNull(listener); + Timeline previousTimeline = currentTimeline; + currentTimeline = eventTime.timeline; + Iterator iterator = sessions.values().iterator(); + while (iterator.hasNext()) { + SessionDescriptor session = iterator.next(); + if (!session.tryResolvingToNewTimeline(previousTimeline, currentTimeline)) { + iterator.remove(); + if (session.isCreated) { + if (session.sessionId.equals(activeSessionId)) { + activeSessionId = null; + } + listener.onSessionFinished( + eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false); + } + } + } + handlePositionDiscontinuity(eventTime, Player.DISCONTINUITY_REASON_INTERNAL); + } + + @Override + public synchronized void handlePositionDiscontinuity( + EventTime eventTime, @DiscontinuityReason int reason) { + Assertions.checkNotNull(listener); + boolean hasAutomaticTransition = + reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION + || reason == Player.DISCONTINUITY_REASON_AD_INSERTION; + Iterator iterator = sessions.values().iterator(); + while (iterator.hasNext()) { + SessionDescriptor session = iterator.next(); + if (session.isFinishedAtEventTime(eventTime)) { + iterator.remove(); + if (session.isCreated) { + boolean isRemovingActiveSession = session.sessionId.equals(activeSessionId); + boolean isAutomaticTransition = hasAutomaticTransition && isRemovingActiveSession; + if (isRemovingActiveSession) { + activeSessionId = null; + } + listener.onSessionFinished(eventTime, session.sessionId, isAutomaticTransition); + } + } + } + SessionDescriptor activeSessionDescriptor = + getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId); + if (eventTime.mediaPeriodId != null + && eventTime.mediaPeriodId.isAd() + && (currentMediaPeriodId == null + || currentMediaPeriodId.windowSequenceNumber + != eventTime.mediaPeriodId.windowSequenceNumber + || currentMediaPeriodId.adGroupIndex != eventTime.mediaPeriodId.adGroupIndex + || currentMediaPeriodId.adIndexInAdGroup != eventTime.mediaPeriodId.adIndexInAdGroup)) { + // New ad playback started. Find corresponding content session and notify ad playback started. + MediaPeriodId contentMediaPeriodId = + new MediaPeriodId( + eventTime.mediaPeriodId.periodUid, eventTime.mediaPeriodId.windowSequenceNumber); + SessionDescriptor contentSession = + getOrAddSession(eventTime.windowIndex, contentMediaPeriodId); + if (contentSession.isCreated && activeSessionDescriptor.isCreated) { + listener.onAdPlaybackStarted( + eventTime, contentSession.sessionId, activeSessionDescriptor.sessionId); + } + } + updateActiveSession(eventTime, activeSessionDescriptor); + } + + private SessionDescriptor getOrAddSession( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + // There should only be one matching session if mediaPeriodId is non-null. If mediaPeriodId is + // null, there may be multiple matching sessions with different window sequence numbers or + // adMediaPeriodIds. The best match is the one with the smaller window sequence number, and for + // windows with ads, the content session is preferred over ad sessions. + SessionDescriptor bestMatch = null; + long bestMatchWindowSequenceNumber = Long.MAX_VALUE; + for (SessionDescriptor sessionDescriptor : sessions.values()) { + sessionDescriptor.maybeSetWindowSequenceNumber(windowIndex, mediaPeriodId); + if (sessionDescriptor.belongsToSession(windowIndex, mediaPeriodId)) { + long windowSequenceNumber = sessionDescriptor.windowSequenceNumber; + if (windowSequenceNumber == C.INDEX_UNSET + || windowSequenceNumber < bestMatchWindowSequenceNumber) { + bestMatch = sessionDescriptor; + bestMatchWindowSequenceNumber = windowSequenceNumber; + } else if (windowSequenceNumber == bestMatchWindowSequenceNumber + && Util.castNonNull(bestMatch).adMediaPeriodId != null + && sessionDescriptor.adMediaPeriodId != null) { + bestMatch = sessionDescriptor; + } + } + } + if (bestMatch == null) { + String sessionId = generateSessionId(); + bestMatch = new SessionDescriptor(sessionId, windowIndex, mediaPeriodId); + sessions.put(sessionId, bestMatch); + } + return bestMatch; + } + + @RequiresNonNull("listener") + private void updateActiveSession(EventTime eventTime, SessionDescriptor sessionDescriptor) { + currentMediaPeriodId = eventTime.mediaPeriodId; + if (sessionDescriptor.isCreated) { + activeSessionId = sessionDescriptor.sessionId; + if (!sessionDescriptor.isActive) { + sessionDescriptor.isActive = true; + listener.onSessionActive(eventTime, sessionDescriptor.sessionId); + } + } + } + + private static String generateSessionId() { + byte[] randomBytes = new byte[SESSION_ID_LENGTH]; + RANDOM.nextBytes(randomBytes); + return Base64.encodeToString(randomBytes, Base64.URL_SAFE | Base64.NO_WRAP); + } + + /** + * Descriptor for a session. + * + *

The session may be described in one of three ways: + * + *

+ */ + private final class SessionDescriptor { + + private final String sessionId; + + private int windowIndex; + private long windowSequenceNumber; + private @MonotonicNonNull MediaPeriodId adMediaPeriodId; + + private boolean isCreated; + private boolean isActive; + + public SessionDescriptor( + String sessionId, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + this.sessionId = sessionId; + this.windowIndex = windowIndex; + this.windowSequenceNumber = + mediaPeriodId == null ? C.INDEX_UNSET : mediaPeriodId.windowSequenceNumber; + if (mediaPeriodId != null && mediaPeriodId.isAd()) { + this.adMediaPeriodId = mediaPeriodId; + } + } + + public boolean tryResolvingToNewTimeline(Timeline oldTimeline, Timeline newTimeline) { + windowIndex = resolveWindowIndexToNewTimeline(oldTimeline, newTimeline, windowIndex); + if (windowIndex == C.INDEX_UNSET) { + return false; + } + if (adMediaPeriodId == null) { + return true; + } + int newPeriodIndex = newTimeline.getIndexOfPeriod(adMediaPeriodId.periodUid); + return newPeriodIndex != C.INDEX_UNSET; + } + + public boolean belongsToSession( + int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) { + if (eventMediaPeriodId == null) { + // Events without concrete media period id are for all sessions of the same window. + return eventWindowIndex == windowIndex; + } + if (adMediaPeriodId == null) { + // If this is a content session, only events for content with the same window sequence + // number belong to this session. + return !eventMediaPeriodId.isAd() + && eventMediaPeriodId.windowSequenceNumber == windowSequenceNumber; + } + // If this is an ad session, only events for this ad belong to the session. + return eventMediaPeriodId.windowSequenceNumber == adMediaPeriodId.windowSequenceNumber + && eventMediaPeriodId.adGroupIndex == adMediaPeriodId.adGroupIndex + && eventMediaPeriodId.adIndexInAdGroup == adMediaPeriodId.adIndexInAdGroup; + } + + public void maybeSetWindowSequenceNumber( + int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) { + if (windowSequenceNumber == C.INDEX_UNSET + && eventWindowIndex == windowIndex + && eventMediaPeriodId != null + && !eventMediaPeriodId.isAd()) { + // Set window sequence number for this session as soon as we have one. + windowSequenceNumber = eventMediaPeriodId.windowSequenceNumber; + } + } + + public boolean isFinishedAtEventTime(EventTime eventTime) { + if (windowSequenceNumber == C.INDEX_UNSET) { + // Sessions with unspecified window sequence number are kept until we know more. + return false; + } + if (eventTime.mediaPeriodId == null) { + // For event times without media period id (e.g. after seek to new window), we only keep + // sessions of this window. + return windowIndex != eventTime.windowIndex; + } + if (eventTime.mediaPeriodId.windowSequenceNumber > windowSequenceNumber) { + // All past window sequence numbers are finished. + return true; + } + if (adMediaPeriodId == null) { + // Current or future content is not finished. + return false; + } + int eventPeriodIndex = eventTime.timeline.getIndexOfPeriod(eventTime.mediaPeriodId.periodUid); + int adPeriodIndex = eventTime.timeline.getIndexOfPeriod(adMediaPeriodId.periodUid); + if (eventTime.mediaPeriodId.windowSequenceNumber < adMediaPeriodId.windowSequenceNumber + || eventPeriodIndex < adPeriodIndex) { + // Ads in future windows or periods are not finished. + return false; + } + if (eventPeriodIndex > adPeriodIndex) { + // Ads in past periods are finished. + return true; + } + if (eventTime.mediaPeriodId.isAd()) { + int eventAdGroup = eventTime.mediaPeriodId.adGroupIndex; + int eventAdIndex = eventTime.mediaPeriodId.adIndexInAdGroup; + // Finished if event is for an ad after this one in the same period. + return eventAdGroup > adMediaPeriodId.adGroupIndex + || (eventAdGroup == adMediaPeriodId.adGroupIndex + && eventAdIndex > adMediaPeriodId.adIndexInAdGroup); + } else { + // Finished if the event is for content after this ad. + return eventTime.mediaPeriodId.nextAdGroupIndex == C.INDEX_UNSET + || eventTime.mediaPeriodId.nextAdGroupIndex > adMediaPeriodId.adGroupIndex; + } + } + + private int resolveWindowIndexToNewTimeline( + Timeline oldTimeline, Timeline newTimeline, int windowIndex) { + if (windowIndex >= oldTimeline.getWindowCount()) { + return windowIndex < newTimeline.getWindowCount() ? windowIndex : C.INDEX_UNSET; + } + oldTimeline.getWindow(windowIndex, window); + for (int periodIndex = window.firstPeriodIndex; + periodIndex <= window.lastPeriodIndex; + periodIndex++) { + Object periodUid = oldTimeline.getUidOfPeriod(periodIndex); + int newPeriodIndex = newTimeline.getIndexOfPeriod(periodUid); + if (newPeriodIndex != C.INDEX_UNSET) { + return newTimeline.getPeriod(newPeriodIndex, period).windowIndex; + } + } + return C.INDEX_UNSET; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java new file mode 100644 index 000000000000..d3c6f7dd2098 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java @@ -0,0 +1,120 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.analytics; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.DiscontinuityReason; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; + +/** + * Manager for active playback sessions. + * + *

The manager keeps track of the association between window index and/or media period id to + * session identifier. + */ +public interface PlaybackSessionManager { + + /** A listener for session updates. */ + interface Listener { + + /** + * Called when a new session is created as a result of {@link #updateSessions(EventTime)}. + * + * @param eventTime The {@link EventTime} at which the session is created. + * @param sessionId The identifier of the new session. + */ + void onSessionCreated(EventTime eventTime, String sessionId); + + /** + * Called when a session becomes active, i.e. playing in the foreground. + * + * @param eventTime The {@link EventTime} at which the session becomes active. + * @param sessionId The identifier of the session. + */ + void onSessionActive(EventTime eventTime, String sessionId); + + /** + * Called when a session is interrupted by ad playback. + * + * @param eventTime The {@link EventTime} at which the ad playback starts. + * @param contentSessionId The session identifier of the content session. + * @param adSessionId The identifier of the ad session. + */ + void onAdPlaybackStarted(EventTime eventTime, String contentSessionId, String adSessionId); + + /** + * Called when a session is permanently finished. + * + * @param eventTime The {@link EventTime} at which the session finished. + * @param sessionId The identifier of the finished session. + * @param automaticTransitionToNextPlayback Whether the session finished because of an automatic + * transition to the next playback item. + */ + void onSessionFinished( + EventTime eventTime, String sessionId, boolean automaticTransitionToNextPlayback); + } + + /** + * Sets the listener to be notified of session updates. Must be called before the session manager + * is used. + * + * @param listener The {@link Listener} to be notified of session updates. + */ + void setListener(Listener listener); + + /** + * Returns the session identifier for the given media period id. + * + *

Note that this will reserve a new session identifier if it doesn't exist yet, but will not + * call any {@link Listener} callbacks. + * + * @param timeline The timeline, {@code mediaPeriodId} is part of. + * @param mediaPeriodId A {@link MediaPeriodId}. + */ + String getSessionForMediaPeriodId(Timeline timeline, MediaPeriodId mediaPeriodId); + + /** + * Returns whether an event time belong to a session. + * + * @param eventTime The {@link EventTime}. + * @param sessionId A session identifier. + * @return Whether the event belongs to the specified session. + */ + boolean belongsToSession(EventTime eventTime, String sessionId); + + /** + * Updates or creates sessions based on a player {@link EventTime}. + * + * @param eventTime The {@link EventTime}. + */ + void updateSessions(EventTime eventTime); + + /** + * Updates the session associations to a new timeline. + * + * @param eventTime The event time with the timeline change. + */ + void handleTimelineUpdate(EventTime eventTime); + + /** + * Handles a position discontinuity. + * + * @param eventTime The event time of the position discontinuity. + * @param reason The {@link DiscontinuityReason}. + */ + void handlePositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStats.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStats.java new file mode 100644 index 000000000000..eef0f6e7ceee --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStats.java @@ -0,0 +1,980 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.analytics; + +import android.os.SystemClock; +import android.util.Pair; +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** Statistics about playbacks. */ +public final class PlaybackStats { + + /** + * State of a playback. One of {@link #PLAYBACK_STATE_NOT_STARTED}, {@link + * #PLAYBACK_STATE_JOINING_FOREGROUND}, {@link #PLAYBACK_STATE_JOINING_BACKGROUND}, {@link + * #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED}, {@link #PLAYBACK_STATE_SEEKING}, + * {@link #PLAYBACK_STATE_BUFFERING}, {@link #PLAYBACK_STATE_PAUSED_BUFFERING}, {@link + * #PLAYBACK_STATE_SEEK_BUFFERING}, {@link #PLAYBACK_STATE_SUPPRESSED}, {@link + * #PLAYBACK_STATE_SUPPRESSED_BUFFERING}, {@link #PLAYBACK_STATE_ENDED}, {@link + * #PLAYBACK_STATE_STOPPED}, {@link #PLAYBACK_STATE_FAILED}, {@link + * #PLAYBACK_STATE_INTERRUPTED_BY_AD} or {@link #PLAYBACK_STATE_ABANDONED}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) + @IntDef({ + PLAYBACK_STATE_NOT_STARTED, + PLAYBACK_STATE_JOINING_BACKGROUND, + PLAYBACK_STATE_JOINING_FOREGROUND, + PLAYBACK_STATE_PLAYING, + PLAYBACK_STATE_PAUSED, + PLAYBACK_STATE_SEEKING, + PLAYBACK_STATE_BUFFERING, + PLAYBACK_STATE_PAUSED_BUFFERING, + PLAYBACK_STATE_SEEK_BUFFERING, + PLAYBACK_STATE_SUPPRESSED, + PLAYBACK_STATE_SUPPRESSED_BUFFERING, + PLAYBACK_STATE_ENDED, + PLAYBACK_STATE_STOPPED, + PLAYBACK_STATE_FAILED, + PLAYBACK_STATE_INTERRUPTED_BY_AD, + PLAYBACK_STATE_ABANDONED + }) + @interface PlaybackState {} + /** Playback has not started (initial state). */ + public static final int PLAYBACK_STATE_NOT_STARTED = 0; + /** Playback is buffering in the background for initial playback start. */ + public static final int PLAYBACK_STATE_JOINING_BACKGROUND = 1; + /** Playback is buffering in the foreground for initial playback start. */ + public static final int PLAYBACK_STATE_JOINING_FOREGROUND = 2; + /** Playback is actively playing. */ + public static final int PLAYBACK_STATE_PLAYING = 3; + /** Playback is paused but ready to play. */ + public static final int PLAYBACK_STATE_PAUSED = 4; + /** Playback is handling a seek. */ + public static final int PLAYBACK_STATE_SEEKING = 5; + /** Playback is buffering to resume active playback. */ + public static final int PLAYBACK_STATE_BUFFERING = 6; + /** Playback is buffering while paused. */ + public static final int PLAYBACK_STATE_PAUSED_BUFFERING = 7; + /** Playback is buffering after a seek. */ + public static final int PLAYBACK_STATE_SEEK_BUFFERING = 8; + /** Playback is suppressed (e.g. due to audio focus loss). */ + public static final int PLAYBACK_STATE_SUPPRESSED = 9; + /** Playback is suppressed (e.g. due to audio focus loss) while buffering to resume a playback. */ + public static final int PLAYBACK_STATE_SUPPRESSED_BUFFERING = 10; + /** Playback has reached the end of the media. */ + public static final int PLAYBACK_STATE_ENDED = 11; + /** Playback is stopped and can be restarted. */ + public static final int PLAYBACK_STATE_STOPPED = 12; + /** Playback is stopped due a fatal error and can be retried. */ + public static final int PLAYBACK_STATE_FAILED = 13; + /** Playback is interrupted by an ad. */ + public static final int PLAYBACK_STATE_INTERRUPTED_BY_AD = 14; + /** Playback is abandoned before reaching the end of the media. */ + public static final int PLAYBACK_STATE_ABANDONED = 15; + /** Total number of playback states. */ + /* package */ static final int PLAYBACK_STATE_COUNT = 16; + + /** Empty playback stats. */ + public static final PlaybackStats EMPTY = merge(/* nothing */ ); + + /** + * Returns the combined {@link PlaybackStats} for all input {@link PlaybackStats}. + * + *

Note that the full history of events is not kept as the history only makes sense in the + * context of a single playback. + * + * @param playbackStats Array of {@link PlaybackStats} to combine. + * @return The combined {@link PlaybackStats}. + */ + public static PlaybackStats merge(PlaybackStats... playbackStats) { + int playbackCount = 0; + long[] playbackStateDurationsMs = new long[PLAYBACK_STATE_COUNT]; + long firstReportedTimeMs = C.TIME_UNSET; + int foregroundPlaybackCount = 0; + int abandonedBeforeReadyCount = 0; + int endedCount = 0; + int backgroundJoiningCount = 0; + long totalValidJoinTimeMs = C.TIME_UNSET; + int validJoinTimeCount = 0; + int totalPauseCount = 0; + int totalPauseBufferCount = 0; + int totalSeekCount = 0; + int totalRebufferCount = 0; + long maxRebufferTimeMs = C.TIME_UNSET; + int adPlaybackCount = 0; + long totalVideoFormatHeightTimeMs = 0; + long totalVideoFormatHeightTimeProduct = 0; + long totalVideoFormatBitrateTimeMs = 0; + long totalVideoFormatBitrateTimeProduct = 0; + long totalAudioFormatTimeMs = 0; + long totalAudioFormatBitrateTimeProduct = 0; + int initialVideoFormatHeightCount = 0; + int initialVideoFormatBitrateCount = 0; + int totalInitialVideoFormatHeight = C.LENGTH_UNSET; + long totalInitialVideoFormatBitrate = C.LENGTH_UNSET; + int initialAudioFormatBitrateCount = 0; + long totalInitialAudioFormatBitrate = C.LENGTH_UNSET; + long totalBandwidthTimeMs = 0; + long totalBandwidthBytes = 0; + long totalDroppedFrames = 0; + long totalAudioUnderruns = 0; + int fatalErrorPlaybackCount = 0; + int fatalErrorCount = 0; + int nonFatalErrorCount = 0; + for (PlaybackStats stats : playbackStats) { + playbackCount += stats.playbackCount; + for (int i = 0; i < PLAYBACK_STATE_COUNT; i++) { + playbackStateDurationsMs[i] += stats.playbackStateDurationsMs[i]; + } + if (firstReportedTimeMs == C.TIME_UNSET) { + firstReportedTimeMs = stats.firstReportedTimeMs; + } else if (stats.firstReportedTimeMs != C.TIME_UNSET) { + firstReportedTimeMs = Math.min(firstReportedTimeMs, stats.firstReportedTimeMs); + } + foregroundPlaybackCount += stats.foregroundPlaybackCount; + abandonedBeforeReadyCount += stats.abandonedBeforeReadyCount; + endedCount += stats.endedCount; + backgroundJoiningCount += stats.backgroundJoiningCount; + if (totalValidJoinTimeMs == C.TIME_UNSET) { + totalValidJoinTimeMs = stats.totalValidJoinTimeMs; + } else if (stats.totalValidJoinTimeMs != C.TIME_UNSET) { + totalValidJoinTimeMs += stats.totalValidJoinTimeMs; + } + validJoinTimeCount += stats.validJoinTimeCount; + totalPauseCount += stats.totalPauseCount; + totalPauseBufferCount += stats.totalPauseBufferCount; + totalSeekCount += stats.totalSeekCount; + totalRebufferCount += stats.totalRebufferCount; + if (maxRebufferTimeMs == C.TIME_UNSET) { + maxRebufferTimeMs = stats.maxRebufferTimeMs; + } else if (stats.maxRebufferTimeMs != C.TIME_UNSET) { + maxRebufferTimeMs = Math.max(maxRebufferTimeMs, stats.maxRebufferTimeMs); + } + adPlaybackCount += stats.adPlaybackCount; + totalVideoFormatHeightTimeMs += stats.totalVideoFormatHeightTimeMs; + totalVideoFormatHeightTimeProduct += stats.totalVideoFormatHeightTimeProduct; + totalVideoFormatBitrateTimeMs += stats.totalVideoFormatBitrateTimeMs; + totalVideoFormatBitrateTimeProduct += stats.totalVideoFormatBitrateTimeProduct; + totalAudioFormatTimeMs += stats.totalAudioFormatTimeMs; + totalAudioFormatBitrateTimeProduct += stats.totalAudioFormatBitrateTimeProduct; + initialVideoFormatHeightCount += stats.initialVideoFormatHeightCount; + initialVideoFormatBitrateCount += stats.initialVideoFormatBitrateCount; + if (totalInitialVideoFormatHeight == C.LENGTH_UNSET) { + totalInitialVideoFormatHeight = stats.totalInitialVideoFormatHeight; + } else if (stats.totalInitialVideoFormatHeight != C.LENGTH_UNSET) { + totalInitialVideoFormatHeight += stats.totalInitialVideoFormatHeight; + } + if (totalInitialVideoFormatBitrate == C.LENGTH_UNSET) { + totalInitialVideoFormatBitrate = stats.totalInitialVideoFormatBitrate; + } else if (stats.totalInitialVideoFormatBitrate != C.LENGTH_UNSET) { + totalInitialVideoFormatBitrate += stats.totalInitialVideoFormatBitrate; + } + initialAudioFormatBitrateCount += stats.initialAudioFormatBitrateCount; + if (totalInitialAudioFormatBitrate == C.LENGTH_UNSET) { + totalInitialAudioFormatBitrate = stats.totalInitialAudioFormatBitrate; + } else if (stats.totalInitialAudioFormatBitrate != C.LENGTH_UNSET) { + totalInitialAudioFormatBitrate += stats.totalInitialAudioFormatBitrate; + } + totalBandwidthTimeMs += stats.totalBandwidthTimeMs; + totalBandwidthBytes += stats.totalBandwidthBytes; + totalDroppedFrames += stats.totalDroppedFrames; + totalAudioUnderruns += stats.totalAudioUnderruns; + fatalErrorPlaybackCount += stats.fatalErrorPlaybackCount; + fatalErrorCount += stats.fatalErrorCount; + nonFatalErrorCount += stats.nonFatalErrorCount; + } + return new PlaybackStats( + playbackCount, + playbackStateDurationsMs, + /* playbackStateHistory */ Collections.emptyList(), + /* mediaTimeHistory= */ Collections.emptyList(), + firstReportedTimeMs, + foregroundPlaybackCount, + abandonedBeforeReadyCount, + endedCount, + backgroundJoiningCount, + totalValidJoinTimeMs, + validJoinTimeCount, + totalPauseCount, + totalPauseBufferCount, + totalSeekCount, + totalRebufferCount, + maxRebufferTimeMs, + adPlaybackCount, + /* videoFormatHistory= */ Collections.emptyList(), + /* audioFormatHistory= */ Collections.emptyList(), + totalVideoFormatHeightTimeMs, + totalVideoFormatHeightTimeProduct, + totalVideoFormatBitrateTimeMs, + totalVideoFormatBitrateTimeProduct, + totalAudioFormatTimeMs, + totalAudioFormatBitrateTimeProduct, + initialVideoFormatHeightCount, + initialVideoFormatBitrateCount, + totalInitialVideoFormatHeight, + totalInitialVideoFormatBitrate, + initialAudioFormatBitrateCount, + totalInitialAudioFormatBitrate, + totalBandwidthTimeMs, + totalBandwidthBytes, + totalDroppedFrames, + totalAudioUnderruns, + fatalErrorPlaybackCount, + fatalErrorCount, + nonFatalErrorCount, + /* fatalErrorHistory= */ Collections.emptyList(), + /* nonFatalErrorHistory= */ Collections.emptyList()); + } + + /** The number of individual playbacks for which these stats were collected. */ + public final int playbackCount; + + // Playback state stats. + + /** + * The playback state history as ordered pairs of the {@link EventTime} at which a state became + * active and the {@link PlaybackState}. + */ + public final List> playbackStateHistory; + /** + * The media time history as an ordered list of long[2] arrays with [0] being the realtime as + * returned by {@code SystemClock.elapsedRealtime()} and [1] being the media time at this + * realtime, in milliseconds. + */ + public final List mediaTimeHistory; + /** + * The elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} of the first + * reported playback event, or {@link C#TIME_UNSET} if no event has been reported. + */ + public final long firstReportedTimeMs; + /** The number of playbacks which were the active foreground playback at some point. */ + public final int foregroundPlaybackCount; + /** The number of playbacks which were abandoned before they were ready to play. */ + public final int abandonedBeforeReadyCount; + /** The number of playbacks which reached the ended state at least once. */ + public final int endedCount; + /** The number of playbacks which were pre-buffered in the background. */ + public final int backgroundJoiningCount; + /** + * The total time spent joining the playback, in milliseconds, or {@link C#TIME_UNSET} if no valid + * join time could be determined. + * + *

Note that this does not include background joining time. A join time may be invalid if the + * playback never reached {@link #PLAYBACK_STATE_PLAYING} or {@link #PLAYBACK_STATE_PAUSED}, or + * joining was interrupted by a seek, stop, or error state. + */ + public final long totalValidJoinTimeMs; + /** + * The number of playbacks with a valid join time as documented in {@link #totalValidJoinTimeMs}. + */ + public final int validJoinTimeCount; + /** The total number of times a playback has been paused. */ + public final int totalPauseCount; + /** The total number of times a playback has been paused while rebuffering. */ + public final int totalPauseBufferCount; + /** + * The total number of times a seek occurred. This includes seeks happening before playback + * resumed after another seek. + */ + public final int totalSeekCount; + /** + * The total number of times a rebuffer occurred. This excludes initial joining and buffering + * after seek. + */ + public final int totalRebufferCount; + /** + * The maximum time spent during a single rebuffer, in milliseconds, or {@link C#TIME_UNSET} if no + * rebuffer occurred. + */ + public final long maxRebufferTimeMs; + /** The number of ad playbacks. */ + public final int adPlaybackCount; + + // Format stats. + + /** + * The video format history as ordered pairs of the {@link EventTime} at which a format started + * being used and the {@link Format}. The {@link Format} may be null if no video format was used. + */ + public final List> videoFormatHistory; + /** + * The audio format history as ordered pairs of the {@link EventTime} at which a format started + * being used and the {@link Format}. The {@link Format} may be null if no audio format was used. + */ + public final List> audioFormatHistory; + /** The total media time for which video format height data is available, in milliseconds. */ + public final long totalVideoFormatHeightTimeMs; + /** + * The accumulated sum of all video format heights, in pixels, times the time the format was used + * for playback, in milliseconds. + */ + public final long totalVideoFormatHeightTimeProduct; + /** The total media time for which video format bitrate data is available, in milliseconds. */ + public final long totalVideoFormatBitrateTimeMs; + /** + * The accumulated sum of all video format bitrates, in bits per second, times the time the format + * was used for playback, in milliseconds. + */ + public final long totalVideoFormatBitrateTimeProduct; + /** The total media time for which audio format data is available, in milliseconds. */ + public final long totalAudioFormatTimeMs; + /** + * The accumulated sum of all audio format bitrates, in bits per second, times the time the format + * was used for playback, in milliseconds. + */ + public final long totalAudioFormatBitrateTimeProduct; + /** The number of playbacks with initial video format height data. */ + public final int initialVideoFormatHeightCount; + /** The number of playbacks with initial video format bitrate data. */ + public final int initialVideoFormatBitrateCount; + /** + * The total initial video format height for all playbacks, in pixels, or {@link C#LENGTH_UNSET} + * if no initial video format data is available. + */ + public final int totalInitialVideoFormatHeight; + /** + * The total initial video format bitrate for all playbacks, in bits per second, or {@link + * C#LENGTH_UNSET} if no initial video format data is available. + */ + public final long totalInitialVideoFormatBitrate; + /** The number of playbacks with initial audio format bitrate data. */ + public final int initialAudioFormatBitrateCount; + /** + * The total initial audio format bitrate for all playbacks, in bits per second, or {@link + * C#LENGTH_UNSET} if no initial audio format data is available. + */ + public final long totalInitialAudioFormatBitrate; + + // Bandwidth stats. + + /** The total time for which bandwidth measurement data is available, in milliseconds. */ + public final long totalBandwidthTimeMs; + /** The total bytes transferred during {@link #totalBandwidthTimeMs}. */ + public final long totalBandwidthBytes; + + // Renderer quality stats. + + /** The total number of dropped video frames. */ + public final long totalDroppedFrames; + /** The total number of audio underruns. */ + public final long totalAudioUnderruns; + + // Error stats. + + /** + * The total number of playback with at least one fatal error. Errors are fatal if playback + * stopped due to this error. + */ + public final int fatalErrorPlaybackCount; + /** The total number of fatal errors. Errors are fatal if playback stopped due to this error. */ + public final int fatalErrorCount; + /** + * The total number of non-fatal errors. Error are non-fatal if playback can recover from the + * error without stopping. + */ + public final int nonFatalErrorCount; + /** + * The history of fatal errors as ordered pairs of the {@link EventTime} at which an error + * occurred and the error. Errors are fatal if playback stopped due to this error. + */ + public final List> fatalErrorHistory; + /** + * The history of non-fatal errors as ordered pairs of the {@link EventTime} at which an error + * occurred and the error. Error are non-fatal if playback can recover from the error without + * stopping. + */ + public final List> nonFatalErrorHistory; + + private final long[] playbackStateDurationsMs; + + /* package */ PlaybackStats( + int playbackCount, + long[] playbackStateDurationsMs, + List> playbackStateHistory, + List mediaTimeHistory, + long firstReportedTimeMs, + int foregroundPlaybackCount, + int abandonedBeforeReadyCount, + int endedCount, + int backgroundJoiningCount, + long totalValidJoinTimeMs, + int validJoinTimeCount, + int totalPauseCount, + int totalPauseBufferCount, + int totalSeekCount, + int totalRebufferCount, + long maxRebufferTimeMs, + int adPlaybackCount, + List> videoFormatHistory, + List> audioFormatHistory, + long totalVideoFormatHeightTimeMs, + long totalVideoFormatHeightTimeProduct, + long totalVideoFormatBitrateTimeMs, + long totalVideoFormatBitrateTimeProduct, + long totalAudioFormatTimeMs, + long totalAudioFormatBitrateTimeProduct, + int initialVideoFormatHeightCount, + int initialVideoFormatBitrateCount, + int totalInitialVideoFormatHeight, + long totalInitialVideoFormatBitrate, + int initialAudioFormatBitrateCount, + long totalInitialAudioFormatBitrate, + long totalBandwidthTimeMs, + long totalBandwidthBytes, + long totalDroppedFrames, + long totalAudioUnderruns, + int fatalErrorPlaybackCount, + int fatalErrorCount, + int nonFatalErrorCount, + List> fatalErrorHistory, + List> nonFatalErrorHistory) { + this.playbackCount = playbackCount; + this.playbackStateDurationsMs = playbackStateDurationsMs; + this.playbackStateHistory = Collections.unmodifiableList(playbackStateHistory); + this.mediaTimeHistory = Collections.unmodifiableList(mediaTimeHistory); + this.firstReportedTimeMs = firstReportedTimeMs; + this.foregroundPlaybackCount = foregroundPlaybackCount; + this.abandonedBeforeReadyCount = abandonedBeforeReadyCount; + this.endedCount = endedCount; + this.backgroundJoiningCount = backgroundJoiningCount; + this.totalValidJoinTimeMs = totalValidJoinTimeMs; + this.validJoinTimeCount = validJoinTimeCount; + this.totalPauseCount = totalPauseCount; + this.totalPauseBufferCount = totalPauseBufferCount; + this.totalSeekCount = totalSeekCount; + this.totalRebufferCount = totalRebufferCount; + this.maxRebufferTimeMs = maxRebufferTimeMs; + this.adPlaybackCount = adPlaybackCount; + this.videoFormatHistory = Collections.unmodifiableList(videoFormatHistory); + this.audioFormatHistory = Collections.unmodifiableList(audioFormatHistory); + this.totalVideoFormatHeightTimeMs = totalVideoFormatHeightTimeMs; + this.totalVideoFormatHeightTimeProduct = totalVideoFormatHeightTimeProduct; + this.totalVideoFormatBitrateTimeMs = totalVideoFormatBitrateTimeMs; + this.totalVideoFormatBitrateTimeProduct = totalVideoFormatBitrateTimeProduct; + this.totalAudioFormatTimeMs = totalAudioFormatTimeMs; + this.totalAudioFormatBitrateTimeProduct = totalAudioFormatBitrateTimeProduct; + this.initialVideoFormatHeightCount = initialVideoFormatHeightCount; + this.initialVideoFormatBitrateCount = initialVideoFormatBitrateCount; + this.totalInitialVideoFormatHeight = totalInitialVideoFormatHeight; + this.totalInitialVideoFormatBitrate = totalInitialVideoFormatBitrate; + this.initialAudioFormatBitrateCount = initialAudioFormatBitrateCount; + this.totalInitialAudioFormatBitrate = totalInitialAudioFormatBitrate; + this.totalBandwidthTimeMs = totalBandwidthTimeMs; + this.totalBandwidthBytes = totalBandwidthBytes; + this.totalDroppedFrames = totalDroppedFrames; + this.totalAudioUnderruns = totalAudioUnderruns; + this.fatalErrorPlaybackCount = fatalErrorPlaybackCount; + this.fatalErrorCount = fatalErrorCount; + this.nonFatalErrorCount = nonFatalErrorCount; + this.fatalErrorHistory = Collections.unmodifiableList(fatalErrorHistory); + this.nonFatalErrorHistory = Collections.unmodifiableList(nonFatalErrorHistory); + } + + /** + * Returns the total time spent in a given {@link PlaybackState}, in milliseconds. + * + * @param playbackState A {@link PlaybackState}. + * @return Total spent in the given playback state, in milliseconds + */ + public long getPlaybackStateDurationMs(@PlaybackState int playbackState) { + return playbackStateDurationsMs[playbackState]; + } + + /** + * Returns the {@link PlaybackState} at the given time. + * + * @param realtimeMs The time as returned by {@link SystemClock#elapsedRealtime()}. + * @return The {@link PlaybackState} at that time, or {@link #PLAYBACK_STATE_NOT_STARTED} if the + * given time is before the first known playback state in the history. + */ + public @PlaybackState int getPlaybackStateAtTime(long realtimeMs) { + @PlaybackState int state = PLAYBACK_STATE_NOT_STARTED; + for (Pair timeAndState : playbackStateHistory) { + if (timeAndState.first.realtimeMs > realtimeMs) { + break; + } + state = timeAndState.second; + } + return state; + } + + /** + * Returns the estimated media time at the given realtime, in milliseconds, or {@link + * C#TIME_UNSET} if the media time history is unknown. + * + * @param realtimeMs The realtime as returned by {@link SystemClock#elapsedRealtime()}. + * @return The estimated media time in milliseconds at this realtime, {@link C#TIME_UNSET} if no + * estimate can be given. + */ + public long getMediaTimeMsAtRealtimeMs(long realtimeMs) { + if (mediaTimeHistory.isEmpty()) { + return C.TIME_UNSET; + } + int nextIndex = 0; + while (nextIndex < mediaTimeHistory.size() + && mediaTimeHistory.get(nextIndex)[0] <= realtimeMs) { + nextIndex++; + } + if (nextIndex == 0) { + return mediaTimeHistory.get(0)[1]; + } + if (nextIndex == mediaTimeHistory.size()) { + return mediaTimeHistory.get(mediaTimeHistory.size() - 1)[1]; + } + long prevRealtimeMs = mediaTimeHistory.get(nextIndex - 1)[0]; + long prevMediaTimeMs = mediaTimeHistory.get(nextIndex - 1)[1]; + long nextRealtimeMs = mediaTimeHistory.get(nextIndex)[0]; + long nextMediaTimeMs = mediaTimeHistory.get(nextIndex)[1]; + long realtimeDurationMs = nextRealtimeMs - prevRealtimeMs; + if (realtimeDurationMs == 0) { + return prevMediaTimeMs; + } + float fraction = (float) (realtimeMs - prevRealtimeMs) / realtimeDurationMs; + return prevMediaTimeMs + (long) ((nextMediaTimeMs - prevMediaTimeMs) * fraction); + } + + /** + * Returns the mean time spent joining the playback, in milliseconds, or {@link C#TIME_UNSET} if + * no valid join time is available. Only includes playbacks with valid join times as documented in + * {@link #totalValidJoinTimeMs}. + */ + public long getMeanJoinTimeMs() { + return validJoinTimeCount == 0 ? C.TIME_UNSET : totalValidJoinTimeMs / validJoinTimeCount; + } + + /** + * Returns the total time spent joining the playback in foreground, in milliseconds. This does + * include invalid join times where the playback never reached {@link #PLAYBACK_STATE_PLAYING} or + * {@link #PLAYBACK_STATE_PAUSED}, or joining was interrupted by a seek, stop, or error state. + */ + public long getTotalJoinTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_JOINING_FOREGROUND); + } + + /** Returns the total time spent actively playing, in milliseconds. */ + public long getTotalPlayTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_PLAYING); + } + + /** + * Returns the mean time spent actively playing per foreground playback, in milliseconds, or + * {@link C#TIME_UNSET} if no playback has been in foreground. + */ + public long getMeanPlayTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalPlayTimeMs() / foregroundPlaybackCount; + } + + /** Returns the total time spent in a paused state, in milliseconds. */ + public long getTotalPausedTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED) + + getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED_BUFFERING); + } + + /** + * Returns the mean time spent in a paused state per foreground playback, in milliseconds, or + * {@link C#TIME_UNSET} if no playback has been in foreground. + */ + public long getMeanPausedTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalPausedTimeMs() / foregroundPlaybackCount; + } + + /** + * Returns the total time spent rebuffering, in milliseconds. This excludes initial join times, + * buffer times after a seek and buffering while paused. + */ + public long getTotalRebufferTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING); + } + + /** + * Returns the mean time spent rebuffering per foreground playback, in milliseconds, or {@link + * C#TIME_UNSET} if no playback has been in foreground. This excludes initial join times, buffer + * times after a seek and buffering while paused. + */ + public long getMeanRebufferTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalRebufferTimeMs() / foregroundPlaybackCount; + } + + /** + * Returns the mean time spent during a single rebuffer, in milliseconds, or {@link C#TIME_UNSET} + * if no rebuffer was recorded. This excludes initial join times and buffer times after a seek. + */ + public long getMeanSingleRebufferTimeMs() { + return totalRebufferCount == 0 + ? C.TIME_UNSET + : (getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING) + + getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED_BUFFERING)) + / totalRebufferCount; + } + + /** + * Returns the total time spent from the start of a seek until playback is ready again, in + * milliseconds. + */ + public long getTotalSeekTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING) + + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING); + } + + /** + * Returns the mean time spent per foreground playback from the start of a seek until playback is + * ready again, in milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground. + */ + public long getMeanSeekTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalSeekTimeMs() / foregroundPlaybackCount; + } + + /** + * Returns the mean time spent from the start of a single seek until playback is ready again, in + * milliseconds, or {@link C#TIME_UNSET} if no seek occurred. + */ + public long getMeanSingleSeekTimeMs() { + return totalSeekCount == 0 ? C.TIME_UNSET : getTotalSeekTimeMs() / totalSeekCount; + } + + /** + * Returns the total time spent actively waiting for playback, in milliseconds. This includes all + * join times, rebuffer times and seek times, but excludes times without user intention to play, + * e.g. all paused states. + */ + public long getTotalWaitTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_JOINING_FOREGROUND) + + getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING) + + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING) + + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING); + } + + /** + * Returns the mean time spent actively waiting for playback per foreground playback, in + * milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground. This includes all + * join times, rebuffer times and seek times, but excludes times without user intention to play, + * e.g. all paused states. + */ + public long getMeanWaitTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalWaitTimeMs() / foregroundPlaybackCount; + } + + /** Returns the total time spent playing or actively waiting for playback, in milliseconds. */ + public long getTotalPlayAndWaitTimeMs() { + return getTotalPlayTimeMs() + getTotalWaitTimeMs(); + } + + /** + * Returns the mean time spent playing or actively waiting for playback per foreground playback, + * in milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground. + */ + public long getMeanPlayAndWaitTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalPlayAndWaitTimeMs() / foregroundPlaybackCount; + } + + /** Returns the total time covered by any playback state, in milliseconds. */ + public long getTotalElapsedTimeMs() { + long totalTimeMs = 0; + for (int i = 0; i < PLAYBACK_STATE_COUNT; i++) { + totalTimeMs += playbackStateDurationsMs[i]; + } + return totalTimeMs; + } + + /** + * Returns the mean time covered by any playback state per playback, in milliseconds, or {@link + * C#TIME_UNSET} if no playback was recorded. + */ + public long getMeanElapsedTimeMs() { + return playbackCount == 0 ? C.TIME_UNSET : getTotalElapsedTimeMs() / playbackCount; + } + + /** + * Returns the ratio of foreground playbacks which were abandoned before they were ready to play, + * or {@code 0.0} if no playback has been in foreground. + */ + public float getAbandonedBeforeReadyRatio() { + int foregroundAbandonedBeforeReady = + abandonedBeforeReadyCount - (playbackCount - foregroundPlaybackCount); + return foregroundPlaybackCount == 0 + ? 0f + : (float) foregroundAbandonedBeforeReady / foregroundPlaybackCount; + } + + /** + * Returns the ratio of foreground playbacks which reached the ended state at least once, or + * {@code 0.0} if no playback has been in foreground. + */ + public float getEndedRatio() { + return foregroundPlaybackCount == 0 ? 0f : (float) endedCount / foregroundPlaybackCount; + } + + /** + * Returns the mean number of times a playback has been paused per foreground playback, or {@code + * 0.0} if no playback has been in foreground. + */ + public float getMeanPauseCount() { + return foregroundPlaybackCount == 0 ? 0f : (float) totalPauseCount / foregroundPlaybackCount; + } + + /** + * Returns the mean number of times a playback has been paused while rebuffering per foreground + * playback, or {@code 0.0} if no playback has been in foreground. + */ + public float getMeanPauseBufferCount() { + return foregroundPlaybackCount == 0 + ? 0f + : (float) totalPauseBufferCount / foregroundPlaybackCount; + } + + /** + * Returns the mean number of times a seek occurred per foreground playback, or {@code 0.0} if no + * playback has been in foreground. This includes seeks happening before playback resumed after + * another seek. + */ + public float getMeanSeekCount() { + return foregroundPlaybackCount == 0 ? 0f : (float) totalSeekCount / foregroundPlaybackCount; + } + + /** + * Returns the mean number of times a rebuffer occurred per foreground playback, or {@code 0.0} if + * no playback has been in foreground. This excludes initial joining and buffering after seek. + */ + public float getMeanRebufferCount() { + return foregroundPlaybackCount == 0 ? 0f : (float) totalRebufferCount / foregroundPlaybackCount; + } + + /** + * Returns the ratio of wait times to the total time spent playing and waiting, or {@code 0.0} if + * no time was spend playing or waiting. This is equivalent to {@link #getTotalWaitTimeMs()} / + * {@link #getTotalPlayAndWaitTimeMs()} and also to {@link #getJoinTimeRatio()} + {@link + * #getRebufferTimeRatio()} + {@link #getSeekTimeRatio()}. + */ + public float getWaitTimeRatio() { + long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs(); + return playAndWaitTimeMs == 0 ? 0f : (float) getTotalWaitTimeMs() / playAndWaitTimeMs; + } + + /** + * Returns the ratio of foreground join time to the total time spent playing and waiting, or + * {@code 0.0} if no time was spend playing or waiting. This is equivalent to {@link + * #getTotalJoinTimeMs()} / {@link #getTotalPlayAndWaitTimeMs()}. + */ + public float getJoinTimeRatio() { + long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs(); + return playAndWaitTimeMs == 0 ? 0f : (float) getTotalJoinTimeMs() / playAndWaitTimeMs; + } + + /** + * Returns the ratio of rebuffer time to the total time spent playing and waiting, or {@code 0.0} + * if no time was spend playing or waiting. This is equivalent to {@link + * #getTotalRebufferTimeMs()} / {@link #getTotalPlayAndWaitTimeMs()}. + */ + public float getRebufferTimeRatio() { + long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs(); + return playAndWaitTimeMs == 0 ? 0f : (float) getTotalRebufferTimeMs() / playAndWaitTimeMs; + } + + /** + * Returns the ratio of seek time to the total time spent playing and waiting, or {@code 0.0} if + * no time was spend playing or waiting. This is equivalent to {@link #getTotalSeekTimeMs()} / + * {@link #getTotalPlayAndWaitTimeMs()}. + */ + public float getSeekTimeRatio() { + long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs(); + return playAndWaitTimeMs == 0 ? 0f : (float) getTotalSeekTimeMs() / playAndWaitTimeMs; + } + + /** + * Returns the rate of rebuffer events, in rebuffers per play time second, or {@code 0.0} if no + * time was spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenRebuffers()}. + */ + public float getRebufferRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * totalRebufferCount / playTimeMs; + } + + /** + * Returns the mean play time between rebuffer events, in seconds. This is equivalent to 1.0 / + * {@link #getRebufferRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}. + */ + public float getMeanTimeBetweenRebuffers() { + return 1f / getRebufferRate(); + } + + /** + * Returns the mean initial video format height, in pixels, or {@link C#LENGTH_UNSET} if no video + * format data is available. + */ + public int getMeanInitialVideoFormatHeight() { + return initialVideoFormatHeightCount == 0 + ? C.LENGTH_UNSET + : totalInitialVideoFormatHeight / initialVideoFormatHeightCount; + } + + /** + * Returns the mean initial video format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if + * no video format data is available. + */ + public int getMeanInitialVideoFormatBitrate() { + return initialVideoFormatBitrateCount == 0 + ? C.LENGTH_UNSET + : (int) (totalInitialVideoFormatBitrate / initialVideoFormatBitrateCount); + } + + /** + * Returns the mean initial audio format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if + * no audio format data is available. + */ + public int getMeanInitialAudioFormatBitrate() { + return initialAudioFormatBitrateCount == 0 + ? C.LENGTH_UNSET + : (int) (totalInitialAudioFormatBitrate / initialAudioFormatBitrateCount); + } + + /** + * Returns the mean video format height, in pixels, or {@link C#LENGTH_UNSET} if no video format + * data is available. This is a weighted average taking the time the format was used for playback + * into account. + */ + public int getMeanVideoFormatHeight() { + return totalVideoFormatHeightTimeMs == 0 + ? C.LENGTH_UNSET + : (int) (totalVideoFormatHeightTimeProduct / totalVideoFormatHeightTimeMs); + } + + /** + * Returns the mean video format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if no + * video format data is available. This is a weighted average taking the time the format was used + * for playback into account. + */ + public int getMeanVideoFormatBitrate() { + return totalVideoFormatBitrateTimeMs == 0 + ? C.LENGTH_UNSET + : (int) (totalVideoFormatBitrateTimeProduct / totalVideoFormatBitrateTimeMs); + } + + /** + * Returns the mean audio format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if no + * audio format data is available. This is a weighted average taking the time the format was used + * for playback into account. + */ + public int getMeanAudioFormatBitrate() { + return totalAudioFormatTimeMs == 0 + ? C.LENGTH_UNSET + : (int) (totalAudioFormatBitrateTimeProduct / totalAudioFormatTimeMs); + } + + /** + * Returns the mean network bandwidth based on transfer measurements, in bits per second, or + * {@link C#LENGTH_UNSET} if no transfer data is available. + */ + public int getMeanBandwidth() { + return totalBandwidthTimeMs == 0 + ? C.LENGTH_UNSET + : (int) (totalBandwidthBytes * 8000 / totalBandwidthTimeMs); + } + + /** + * Returns the mean rate at which video frames are dropped, in dropped frames per play time + * second, or {@code 0.0} if no time was spent playing. + */ + public float getDroppedFramesRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * totalDroppedFrames / playTimeMs; + } + + /** + * Returns the mean rate at which audio underruns occurred, in underruns per play time second, or + * {@code 0.0} if no time was spent playing. + */ + public float getAudioUnderrunRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * totalAudioUnderruns / playTimeMs; + } + + /** + * Returns the ratio of foreground playbacks which experienced fatal errors, or {@code 0.0} if no + * playback has been in foreground. + */ + public float getFatalErrorRatio() { + return foregroundPlaybackCount == 0 + ? 0f + : (float) fatalErrorPlaybackCount / foregroundPlaybackCount; + } + + /** + * Returns the rate of fatal errors, in errors per play time second, or {@code 0.0} if no time was + * spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenFatalErrors()}. + */ + public float getFatalErrorRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * fatalErrorCount / playTimeMs; + } + + /** + * Returns the mean play time between fatal errors, in seconds. This is equivalent to 1.0 / {@link + * #getFatalErrorRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}. + */ + public float getMeanTimeBetweenFatalErrors() { + return 1f / getFatalErrorRate(); + } + + /** + * Returns the mean number of non-fatal errors per foreground playback, or {@code 0.0} if no + * playback has been in foreground. + */ + public float getMeanNonFatalErrorCount() { + return foregroundPlaybackCount == 0 ? 0f : (float) nonFatalErrorCount / foregroundPlaybackCount; + } + + /** + * Returns the rate of non-fatal errors, in errors per play time second, or {@code 0.0} if no time + * was spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenNonFatalErrors()}. + */ + public float getNonFatalErrorRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * nonFatalErrorCount / playTimeMs; + } + + /** + * Returns the mean play time between non-fatal errors, in seconds. This is equivalent to 1.0 / + * {@link #getNonFatalErrorRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}. + */ + public float getMeanTimeBetweenNonFatalErrors() { + return 1f / getNonFatalErrorRate(); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java new file mode 100644 index 000000000000..058a3a97c138 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -0,0 +1,1059 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.analytics; + +import android.os.SystemClock; +import android.util.Pair; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline.Period; +import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.PlaybackStats.PlaybackState; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** + * {@link AnalyticsListener} to gather {@link PlaybackStats} from the player. + * + *

For accurate measurements, the listener should be added to the player before loading media, + * i.e., {@link Player#getPlaybackState()} should be {@link Player#STATE_IDLE}. + * + *

Playback stats are gathered separately for each playback session, i.e. each window in the + * {@link Timeline} and each single ad. + */ +public final class PlaybackStatsListener + implements AnalyticsListener, PlaybackSessionManager.Listener { + + /** A listener for {@link PlaybackStats} updates. */ + public interface Callback { + + /** + * Called when a playback session ends and its {@link PlaybackStats} are ready. + * + * @param eventTime The {@link EventTime} at which the playback session started. Can be used to + * identify the playback session. + * @param playbackStats The {@link PlaybackStats} for the ended playback session. + */ + void onPlaybackStatsReady(EventTime eventTime, PlaybackStats playbackStats); + } + + private final PlaybackSessionManager sessionManager; + private final Map playbackStatsTrackers; + private final Map sessionStartEventTimes; + @Nullable private final Callback callback; + private final boolean keepHistory; + private final Period period; + + private PlaybackStats finishedPlaybackStats; + @Nullable private String activeContentPlayback; + @Nullable private String activeAdPlayback; + private boolean playWhenReady; + @Player.State private int playbackState; + private boolean isSuppressed; + private float playbackSpeed; + + /** + * Creates listener for playback stats. + * + * @param keepHistory Whether the reported {@link PlaybackStats} should keep the full history of + * events. + * @param callback An optional callback for finished {@link PlaybackStats}. + */ + public PlaybackStatsListener(boolean keepHistory, @Nullable Callback callback) { + this.callback = callback; + this.keepHistory = keepHistory; + sessionManager = new DefaultPlaybackSessionManager(); + playbackStatsTrackers = new HashMap<>(); + sessionStartEventTimes = new HashMap<>(); + finishedPlaybackStats = PlaybackStats.EMPTY; + playWhenReady = false; + playbackState = Player.STATE_IDLE; + playbackSpeed = 1f; + period = new Period(); + sessionManager.setListener(this); + } + + /** + * Returns the combined {@link PlaybackStats} for all playback sessions this listener was and is + * listening to. + * + *

Note that these {@link PlaybackStats} will not contain the full history of events. + * + * @return The combined {@link PlaybackStats} for all playback sessions. + */ + public PlaybackStats getCombinedPlaybackStats() { + PlaybackStats[] allPendingPlaybackStats = new PlaybackStats[playbackStatsTrackers.size() + 1]; + allPendingPlaybackStats[0] = finishedPlaybackStats; + int index = 1; + for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) { + allPendingPlaybackStats[index++] = tracker.build(/* isFinal= */ false); + } + return PlaybackStats.merge(allPendingPlaybackStats); + } + + /** + * Returns the {@link PlaybackStats} for the currently playback session, or null if no session is + * active. + * + * @return {@link PlaybackStats} for the current playback session. + */ + @Nullable + public PlaybackStats getPlaybackStats() { + PlaybackStatsTracker activeStatsTracker = + activeAdPlayback != null + ? playbackStatsTrackers.get(activeAdPlayback) + : activeContentPlayback != null + ? playbackStatsTrackers.get(activeContentPlayback) + : null; + return activeStatsTracker == null ? null : activeStatsTracker.build(/* isFinal= */ false); + } + + /** + * Finishes all pending playback sessions. Should be called when the listener is removed from the + * player or when the player is released. + */ + public void finishAllSessions() { + // TODO: Add AnalyticsListener.onAttachedToPlayer and onDetachedFromPlayer to auto-release with + // an actual EventTime. Should also simplify other cases where the listener needs to be released + // separately from the player. + HashMap trackerCopy = new HashMap<>(playbackStatsTrackers); + EventTime dummyEventTime = + new EventTime( + SystemClock.elapsedRealtime(), + Timeline.EMPTY, + /* windowIndex= */ 0, + /* mediaPeriodId= */ null, + /* eventPlaybackPositionMs= */ 0, + /* currentPlaybackPositionMs= */ 0, + /* totalBufferedDurationMs= */ 0); + for (String session : trackerCopy.keySet()) { + onSessionFinished(dummyEventTime, session, /* automaticTransition= */ false); + } + } + + // PlaybackSessionManager.Listener implementation. + + @Override + public void onSessionCreated(EventTime eventTime, String session) { + PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime); + tracker.onPlayerStateChanged( + eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true); + tracker.onIsSuppressedChanged(eventTime, isSuppressed, /* belongsToPlayback= */ true); + tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed); + playbackStatsTrackers.put(session, tracker); + sessionStartEventTimes.put(session, eventTime); + } + + @Override + public void onSessionActive(EventTime eventTime, String session) { + Assertions.checkNotNull(playbackStatsTrackers.get(session)).onForeground(eventTime); + if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) { + activeAdPlayback = session; + } else { + activeContentPlayback = session; + } + } + + @Override + public void onAdPlaybackStarted(EventTime eventTime, String contentSession, String adSession) { + Assertions.checkState(Assertions.checkNotNull(eventTime.mediaPeriodId).isAd()); + long contentPositionUs = + eventTime + .timeline + .getPeriodByUid(eventTime.mediaPeriodId.periodUid, period) + .getAdGroupTimeUs(eventTime.mediaPeriodId.adGroupIndex); + EventTime contentEventTime = + new EventTime( + eventTime.realtimeMs, + eventTime.timeline, + eventTime.windowIndex, + new MediaPeriodId( + eventTime.mediaPeriodId.periodUid, + eventTime.mediaPeriodId.windowSequenceNumber, + eventTime.mediaPeriodId.adGroupIndex), + /* eventPlaybackPositionMs= */ C.usToMs(contentPositionUs), + eventTime.currentPlaybackPositionMs, + eventTime.totalBufferedDurationMs); + Assertions.checkNotNull(playbackStatsTrackers.get(contentSession)) + .onInterruptedByAd(contentEventTime); + } + + @Override + public void onSessionFinished(EventTime eventTime, String session, boolean automaticTransition) { + if (session.equals(activeAdPlayback)) { + activeAdPlayback = null; + } else if (session.equals(activeContentPlayback)) { + activeContentPlayback = null; + } + PlaybackStatsTracker tracker = Assertions.checkNotNull(playbackStatsTrackers.remove(session)); + EventTime startEventTime = Assertions.checkNotNull(sessionStartEventTimes.remove(session)); + if (automaticTransition) { + // Simulate ENDED state to record natural ending of playback. + tracker.onPlayerStateChanged( + eventTime, /* playWhenReady= */ true, Player.STATE_ENDED, /* belongsToPlayback= */ false); + } + tracker.onFinished(eventTime); + PlaybackStats playbackStats = tracker.build(/* isFinal= */ true); + finishedPlaybackStats = PlaybackStats.merge(finishedPlaybackStats, playbackStats); + if (callback != null) { + callback.onPlaybackStatsReady(startEventTime, playbackStats); + } + } + + // AnalyticsListener implementation. + + @Override + public void onPlayerStateChanged( + EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) { + this.playWhenReady = playWhenReady; + this.playbackState = playbackState; + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); + playbackStatsTrackers + .get(session) + .onPlayerStateChanged(eventTime, playWhenReady, playbackState, belongsToPlayback); + } + } + + @Override + public void onPlaybackSuppressionReasonChanged( + EventTime eventTime, int playbackSuppressionReason) { + isSuppressed = playbackSuppressionReason != Player.PLAYBACK_SUPPRESSION_REASON_NONE; + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); + playbackStatsTrackers + .get(session) + .onIsSuppressedChanged(eventTime, isSuppressed, belongsToPlayback); + } + } + + @Override + public void onTimelineChanged(EventTime eventTime, int reason) { + sessionManager.handleTimelineUpdate(eventTime); + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime); + } + } + } + + @Override + public void onPositionDiscontinuity(EventTime eventTime, int reason) { + sessionManager.handlePositionDiscontinuity(eventTime, reason); + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime); + } + } + } + + @Override + public void onSeekStarted(EventTime eventTime) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onSeekStarted(eventTime); + } + } + } + + @Override + public void onSeekProcessed(EventTime eventTime) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onSeekProcessed(eventTime); + } + } + } + + @Override + public void onPlayerError(EventTime eventTime, ExoPlaybackException error) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onFatalError(eventTime, error); + } + } + } + + @Override + public void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) { + playbackSpeed = playbackParameters.speed; + sessionManager.updateSessions(eventTime); + for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) { + tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed); + } + } + + @Override + public void onTracksChanged( + EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onTracksChanged(eventTime, trackSelections); + } + } + } + + @Override + public void onLoadStarted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onLoadStarted(eventTime); + } + } + } + + @Override + public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onDownstreamFormatChanged(eventTime, mediaLoadData); + } + } + } + + @Override + public void onVideoSizeChanged( + EventTime eventTime, + int width, + int height, + int unappliedRotationDegrees, + float pixelWidthHeightRatio) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onVideoSizeChanged(eventTime, width, height); + } + } + } + + @Override + public void onBandwidthEstimate( + EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onBandwidthData(totalLoadTimeMs, totalBytesLoaded); + } + } + } + + @Override + public void onAudioUnderrun( + EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onAudioUnderrun(); + } + } + } + + @Override + public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onDroppedVideoFrames(droppedFrames); + } + } + } + + @Override + public void onLoadError( + EventTime eventTime, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onNonFatalError(eventTime, error); + } + } + } + + @Override + public void onDrmSessionManagerError(EventTime eventTime, Exception error) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onNonFatalError(eventTime, error); + } + } + } + + /** Tracker for playback stats of a single playback. */ + private static final class PlaybackStatsTracker { + + // Final stats. + private final boolean keepHistory; + private final long[] playbackStateDurationsMs; + private final List> playbackStateHistory; + private final List mediaTimeHistory; + private final List> videoFormatHistory; + private final List> audioFormatHistory; + private final List> fatalErrorHistory; + private final List> nonFatalErrorHistory; + private final boolean isAd; + + private long firstReportedTimeMs; + private boolean hasBeenReady; + private boolean hasEnded; + private boolean isJoinTimeInvalid; + private int pauseCount; + private int pauseBufferCount; + private int seekCount; + private int rebufferCount; + private long maxRebufferTimeMs; + private int initialVideoFormatHeight; + private long initialVideoFormatBitrate; + private long initialAudioFormatBitrate; + private long videoFormatHeightTimeMs; + private long videoFormatHeightTimeProduct; + private long videoFormatBitrateTimeMs; + private long videoFormatBitrateTimeProduct; + private long audioFormatTimeMs; + private long audioFormatBitrateTimeProduct; + private long bandwidthTimeMs; + private long bandwidthBytes; + private long droppedFrames; + private long audioUnderruns; + private int fatalErrorCount; + private int nonFatalErrorCount; + + // Current player state tracking. + private @PlaybackState int currentPlaybackState; + private long currentPlaybackStateStartTimeMs; + private boolean isSeeking; + private boolean isForeground; + private boolean isInterruptedByAd; + private boolean isFinished; + private boolean playWhenReady; + @Player.State private int playerPlaybackState; + private boolean isSuppressed; + private boolean hasFatalError; + private boolean startedLoading; + private long lastRebufferStartTimeMs; + @Nullable private Format currentVideoFormat; + @Nullable private Format currentAudioFormat; + private long lastVideoFormatStartTimeMs; + private long lastAudioFormatStartTimeMs; + private float currentPlaybackSpeed; + + /** + * Creates a tracker for playback stats. + * + * @param keepHistory Whether to keep a full history of events. + * @param startTime The {@link EventTime} at which the playback stats start. + */ + public PlaybackStatsTracker(boolean keepHistory, EventTime startTime) { + this.keepHistory = keepHistory; + playbackStateDurationsMs = new long[PlaybackStats.PLAYBACK_STATE_COUNT]; + playbackStateHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + mediaTimeHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + videoFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + audioFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + fatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + nonFatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + currentPlaybackState = PlaybackStats.PLAYBACK_STATE_NOT_STARTED; + currentPlaybackStateStartTimeMs = startTime.realtimeMs; + playerPlaybackState = Player.STATE_IDLE; + firstReportedTimeMs = C.TIME_UNSET; + maxRebufferTimeMs = C.TIME_UNSET; + isAd = startTime.mediaPeriodId != null && startTime.mediaPeriodId.isAd(); + initialAudioFormatBitrate = C.LENGTH_UNSET; + initialVideoFormatBitrate = C.LENGTH_UNSET; + initialVideoFormatHeight = C.LENGTH_UNSET; + currentPlaybackSpeed = 1f; + } + + /** + * Notifies the tracker of a player state change event, including all player state changes while + * the playback is not in the foreground. + * + * @param eventTime The {@link EventTime}. + * @param playWhenReady Whether the playback will proceed when ready. + * @param playbackState The current {@link Player.State}. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. + */ + public void onPlayerStateChanged( + EventTime eventTime, + boolean playWhenReady, + @Player.State int playbackState, + boolean belongsToPlayback) { + this.playWhenReady = playWhenReady; + playerPlaybackState = playbackState; + if (playbackState != Player.STATE_IDLE) { + hasFatalError = false; + } + if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) { + isInterruptedByAd = false; + } + maybeUpdatePlaybackState(eventTime, belongsToPlayback); + } + + /** + * Notifies the tracker of a change to the playback suppression (e.g. due to audio focus loss), + * including all updates while the playback is not in the foreground. + * + * @param eventTime The {@link EventTime}. + * @param isSuppressed Whether playback is suppressed. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. + */ + public void onIsSuppressedChanged( + EventTime eventTime, boolean isSuppressed, boolean belongsToPlayback) { + this.isSuppressed = isSuppressed; + maybeUpdatePlaybackState(eventTime, belongsToPlayback); + } + + /** + * Notifies the tracker of a position discontinuity or timeline update for the current playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onPositionDiscontinuity(EventTime eventTime) { + isInterruptedByAd = false; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker of the start of a seek in the current playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onSeekStarted(EventTime eventTime) { + isSeeking = true; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker of a seek has been processed in the current playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onSeekProcessed(EventTime eventTime) { + isSeeking = false; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker of fatal player error in the current playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onFatalError(EventTime eventTime, Exception error) { + fatalErrorCount++; + if (keepHistory) { + fatalErrorHistory.add(Pair.create(eventTime, error)); + } + hasFatalError = true; + isInterruptedByAd = false; + isSeeking = false; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker that a load for the current playback has started. + * + * @param eventTime The {@link EventTime}. + */ + public void onLoadStarted(EventTime eventTime) { + startedLoading = true; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker that the current playback became the active foreground playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onForeground(EventTime eventTime) { + isForeground = true; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker that the current playback has been interrupted for ad playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onInterruptedByAd(EventTime eventTime) { + isInterruptedByAd = true; + isSeeking = false; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker that the current playback has finished. + * + * @param eventTime The {@link EventTime}. Not guaranteed to belong to the current playback. + */ + public void onFinished(EventTime eventTime) { + isFinished = true; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ false); + } + + /** + * Notifies the tracker that the track selection for the current playback changed. + * + * @param eventTime The {@link EventTime}. + * @param trackSelections The new {@link TrackSelectionArray}. + */ + public void onTracksChanged(EventTime eventTime, TrackSelectionArray trackSelections) { + boolean videoEnabled = false; + boolean audioEnabled = false; + for (TrackSelection trackSelection : trackSelections.getAll()) { + if (trackSelection != null && trackSelection.length() > 0) { + int trackType = MimeTypes.getTrackType(trackSelection.getFormat(0).sampleMimeType); + if (trackType == C.TRACK_TYPE_VIDEO) { + videoEnabled = true; + } else if (trackType == C.TRACK_TYPE_AUDIO) { + audioEnabled = true; + } + } + } + if (!videoEnabled) { + maybeUpdateVideoFormat(eventTime, /* newFormat= */ null); + } + if (!audioEnabled) { + maybeUpdateAudioFormat(eventTime, /* newFormat= */ null); + } + } + + /** + * Notifies the tracker that a format being read by the renderers for the current playback + * changed. + * + * @param eventTime The {@link EventTime}. + * @param mediaLoadData The {@link MediaLoadData} describing the format change. + */ + public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { + if (mediaLoadData.trackType == C.TRACK_TYPE_VIDEO + || mediaLoadData.trackType == C.TRACK_TYPE_DEFAULT) { + maybeUpdateVideoFormat(eventTime, mediaLoadData.trackFormat); + } else if (mediaLoadData.trackType == C.TRACK_TYPE_AUDIO) { + maybeUpdateAudioFormat(eventTime, mediaLoadData.trackFormat); + } + } + + /** + * Notifies the tracker that the video size for the current playback changed. + * + * @param eventTime The {@link EventTime}. + * @param width The video width in pixels. + * @param height The video height in pixels. + */ + public void onVideoSizeChanged(EventTime eventTime, int width, int height) { + if (currentVideoFormat != null && currentVideoFormat.height == Format.NO_VALUE) { + Format formatWithHeight = currentVideoFormat.copyWithVideoSize(width, height); + maybeUpdateVideoFormat(eventTime, formatWithHeight); + } + } + + /** + * Notifies the tracker of a playback speed change, including all playback speed changes while + * the playback is not in the foreground. + * + * @param eventTime The {@link EventTime}. + * @param playbackSpeed The new playback speed. + */ + public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) { + maybeUpdateMediaTimeHistory(eventTime.realtimeMs, eventTime.eventPlaybackPositionMs); + maybeRecordVideoFormatTime(eventTime.realtimeMs); + maybeRecordAudioFormatTime(eventTime.realtimeMs); + currentPlaybackSpeed = playbackSpeed; + } + + /** Notifies the builder of an audio underrun for the current playback. */ + public void onAudioUnderrun() { + audioUnderruns++; + } + + /** + * Notifies the tracker of dropped video frames for the current playback. + * + * @param droppedFrames The number of dropped video frames. + */ + public void onDroppedVideoFrames(int droppedFrames) { + this.droppedFrames += droppedFrames; + } + + /** + * Notifies the tracker of bandwidth measurement data for the current playback. + * + * @param timeMs The time for which bandwidth measurement data is available, in milliseconds. + * @param bytes The bytes transferred during {@code timeMs}. + */ + public void onBandwidthData(long timeMs, long bytes) { + bandwidthTimeMs += timeMs; + bandwidthBytes += bytes; + } + + /** + * Notifies the tracker of a non-fatal error in the current playback. + * + * @param eventTime The {@link EventTime}. + * @param error The error. + */ + public void onNonFatalError(EventTime eventTime, Exception error) { + nonFatalErrorCount++; + if (keepHistory) { + nonFatalErrorHistory.add(Pair.create(eventTime, error)); + } + } + + /** + * Builds the playback stats. + * + * @param isFinal Whether this is the final build and no further events are expected. + */ + public PlaybackStats build(boolean isFinal) { + long[] playbackStateDurationsMs = this.playbackStateDurationsMs; + List mediaTimeHistory = this.mediaTimeHistory; + if (!isFinal) { + long buildTimeMs = SystemClock.elapsedRealtime(); + playbackStateDurationsMs = + Arrays.copyOf(this.playbackStateDurationsMs, PlaybackStats.PLAYBACK_STATE_COUNT); + long lastStateDurationMs = Math.max(0, buildTimeMs - currentPlaybackStateStartTimeMs); + playbackStateDurationsMs[currentPlaybackState] += lastStateDurationMs; + maybeUpdateMaxRebufferTimeMs(buildTimeMs); + maybeRecordVideoFormatTime(buildTimeMs); + maybeRecordAudioFormatTime(buildTimeMs); + mediaTimeHistory = new ArrayList<>(this.mediaTimeHistory); + if (keepHistory && currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING) { + mediaTimeHistory.add(guessMediaTimeBasedOnElapsedRealtime(buildTimeMs)); + } + } + boolean isJoinTimeInvalid = this.isJoinTimeInvalid || !hasBeenReady; + long validJoinTimeMs = + isJoinTimeInvalid + ? C.TIME_UNSET + : playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND]; + boolean hasBackgroundJoin = + playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND] > 0; + List> videoHistory = + isFinal ? videoFormatHistory : new ArrayList<>(videoFormatHistory); + List> audioHistory = + isFinal ? audioFormatHistory : new ArrayList<>(audioFormatHistory); + return new PlaybackStats( + /* playbackCount= */ 1, + playbackStateDurationsMs, + isFinal ? playbackStateHistory : new ArrayList<>(playbackStateHistory), + mediaTimeHistory, + firstReportedTimeMs, + /* foregroundPlaybackCount= */ isForeground ? 1 : 0, + /* abandonedBeforeReadyCount= */ hasBeenReady ? 0 : 1, + /* endedCount= */ hasEnded ? 1 : 0, + /* backgroundJoiningCount= */ hasBackgroundJoin ? 1 : 0, + validJoinTimeMs, + /* validJoinTimeCount= */ isJoinTimeInvalid ? 0 : 1, + pauseCount, + pauseBufferCount, + seekCount, + rebufferCount, + maxRebufferTimeMs, + /* adPlaybackCount= */ isAd ? 1 : 0, + videoHistory, + audioHistory, + videoFormatHeightTimeMs, + videoFormatHeightTimeProduct, + videoFormatBitrateTimeMs, + videoFormatBitrateTimeProduct, + audioFormatTimeMs, + audioFormatBitrateTimeProduct, + /* initialVideoFormatHeightCount= */ initialVideoFormatHeight == C.LENGTH_UNSET ? 0 : 1, + /* initialVideoFormatBitrateCount= */ initialVideoFormatBitrate == C.LENGTH_UNSET ? 0 : 1, + initialVideoFormatHeight, + initialVideoFormatBitrate, + /* initialAudioFormatBitrateCount= */ initialAudioFormatBitrate == C.LENGTH_UNSET ? 0 : 1, + initialAudioFormatBitrate, + bandwidthTimeMs, + bandwidthBytes, + droppedFrames, + audioUnderruns, + /* fatalErrorPlaybackCount= */ fatalErrorCount > 0 ? 1 : 0, + fatalErrorCount, + nonFatalErrorCount, + fatalErrorHistory, + nonFatalErrorHistory); + } + + private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlayback) { + @PlaybackState int newPlaybackState = resolveNewPlaybackState(); + if (newPlaybackState == currentPlaybackState) { + return; + } + Assertions.checkArgument(eventTime.realtimeMs >= currentPlaybackStateStartTimeMs); + + long stateDurationMs = eventTime.realtimeMs - currentPlaybackStateStartTimeMs; + playbackStateDurationsMs[currentPlaybackState] += stateDurationMs; + if (firstReportedTimeMs == C.TIME_UNSET) { + firstReportedTimeMs = eventTime.realtimeMs; + } + isJoinTimeInvalid |= isInvalidJoinTransition(currentPlaybackState, newPlaybackState); + hasBeenReady |= isReadyState(newPlaybackState); + hasEnded |= newPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED; + if (!isPausedState(currentPlaybackState) && isPausedState(newPlaybackState)) { + pauseCount++; + } + if (newPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING) { + seekCount++; + } + if (!isRebufferingState(currentPlaybackState) && isRebufferingState(newPlaybackState)) { + rebufferCount++; + lastRebufferStartTimeMs = eventTime.realtimeMs; + } + if (isRebufferingState(currentPlaybackState) + && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING + && newPlaybackState == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING) { + pauseBufferCount++; + } + + maybeUpdateMediaTimeHistory( + eventTime.realtimeMs, + /* mediaTimeMs= */ belongsToPlayback ? eventTime.eventPlaybackPositionMs : C.TIME_UNSET); + maybeUpdateMaxRebufferTimeMs(eventTime.realtimeMs); + maybeRecordVideoFormatTime(eventTime.realtimeMs); + maybeRecordAudioFormatTime(eventTime.realtimeMs); + + currentPlaybackState = newPlaybackState; + currentPlaybackStateStartTimeMs = eventTime.realtimeMs; + if (keepHistory) { + playbackStateHistory.add(Pair.create(eventTime, currentPlaybackState)); + } + } + + private @PlaybackState int resolveNewPlaybackState() { + if (isFinished) { + // Keep VIDEO_STATE_ENDED if playback naturally ended (or progressed to next item). + return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED + ? PlaybackStats.PLAYBACK_STATE_ENDED + : PlaybackStats.PLAYBACK_STATE_ABANDONED; + } else if (isSeeking) { + // Seeking takes precedence over errors such that we report a seek while in error state. + return PlaybackStats.PLAYBACK_STATE_SEEKING; + } else if (hasFatalError) { + return PlaybackStats.PLAYBACK_STATE_FAILED; + } else if (!isForeground) { + // Before the playback becomes foreground, only report background joining and not started. + return startedLoading + ? PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND + : PlaybackStats.PLAYBACK_STATE_NOT_STARTED; + } else if (isInterruptedByAd) { + return PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD; + } else if (playerPlaybackState == Player.STATE_ENDED) { + return PlaybackStats.PLAYBACK_STATE_ENDED; + } else if (playerPlaybackState == Player.STATE_BUFFERING) { + if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_NOT_STARTED + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) { + return PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND; + } + if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING) { + return PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING; + } + if (!playWhenReady) { + return PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; + } + return isSuppressed + ? PlaybackStats.PLAYBACK_STATE_SUPPRESSED_BUFFERING + : PlaybackStats.PLAYBACK_STATE_BUFFERING; + } else if (playerPlaybackState == Player.STATE_READY) { + if (!playWhenReady) { + return PlaybackStats.PLAYBACK_STATE_PAUSED; + } + return isSuppressed + ? PlaybackStats.PLAYBACK_STATE_SUPPRESSED + : PlaybackStats.PLAYBACK_STATE_PLAYING; + } else if (playerPlaybackState == Player.STATE_IDLE + && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_NOT_STARTED) { + // This case only applies for calls to player.stop(). All other IDLE cases are handled by + // !isForeground, hasFatalError or isSuspended. NOT_STARTED is deliberately ignored. + return PlaybackStats.PLAYBACK_STATE_STOPPED; + } + return currentPlaybackState; + } + + private void maybeUpdateMaxRebufferTimeMs(long nowMs) { + if (isRebufferingState(currentPlaybackState)) { + long rebufferDurationMs = nowMs - lastRebufferStartTimeMs; + if (maxRebufferTimeMs == C.TIME_UNSET || rebufferDurationMs > maxRebufferTimeMs) { + maxRebufferTimeMs = rebufferDurationMs; + } + } + } + + private void maybeUpdateMediaTimeHistory(long realtimeMs, long mediaTimeMs) { + if (!keepHistory) { + return; + } + if (currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PLAYING) { + if (mediaTimeMs == C.TIME_UNSET) { + return; + } + if (!mediaTimeHistory.isEmpty()) { + long previousMediaTimeMs = mediaTimeHistory.get(mediaTimeHistory.size() - 1)[1]; + if (previousMediaTimeMs != mediaTimeMs) { + mediaTimeHistory.add(new long[] {realtimeMs, previousMediaTimeMs}); + } + } + } + mediaTimeHistory.add( + mediaTimeMs == C.TIME_UNSET + ? guessMediaTimeBasedOnElapsedRealtime(realtimeMs) + : new long[] {realtimeMs, mediaTimeMs}); + } + + private long[] guessMediaTimeBasedOnElapsedRealtime(long realtimeMs) { + long[] previousKnownMediaTimeHistory = mediaTimeHistory.get(mediaTimeHistory.size() - 1); + long previousRealtimeMs = previousKnownMediaTimeHistory[0]; + long previousMediaTimeMs = previousKnownMediaTimeHistory[1]; + long elapsedMediaTimeEstimateMs = + (long) ((realtimeMs - previousRealtimeMs) * currentPlaybackSpeed); + long mediaTimeEstimateMs = previousMediaTimeMs + elapsedMediaTimeEstimateMs; + return new long[] {realtimeMs, mediaTimeEstimateMs}; + } + + private void maybeUpdateVideoFormat(EventTime eventTime, @Nullable Format newFormat) { + if (Util.areEqual(currentVideoFormat, newFormat)) { + return; + } + maybeRecordVideoFormatTime(eventTime.realtimeMs); + if (newFormat != null) { + if (initialVideoFormatHeight == C.LENGTH_UNSET && newFormat.height != Format.NO_VALUE) { + initialVideoFormatHeight = newFormat.height; + } + if (initialVideoFormatBitrate == C.LENGTH_UNSET && newFormat.bitrate != Format.NO_VALUE) { + initialVideoFormatBitrate = newFormat.bitrate; + } + } + currentVideoFormat = newFormat; + if (keepHistory) { + videoFormatHistory.add(Pair.create(eventTime, currentVideoFormat)); + } + } + + private void maybeUpdateAudioFormat(EventTime eventTime, @Nullable Format newFormat) { + if (Util.areEqual(currentAudioFormat, newFormat)) { + return; + } + maybeRecordAudioFormatTime(eventTime.realtimeMs); + if (newFormat != null + && initialAudioFormatBitrate == C.LENGTH_UNSET + && newFormat.bitrate != Format.NO_VALUE) { + initialAudioFormatBitrate = newFormat.bitrate; + } + currentAudioFormat = newFormat; + if (keepHistory) { + audioFormatHistory.add(Pair.create(eventTime, currentAudioFormat)); + } + } + + private void maybeRecordVideoFormatTime(long nowMs) { + if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING + && currentVideoFormat != null) { + long mediaDurationMs = (long) ((nowMs - lastVideoFormatStartTimeMs) * currentPlaybackSpeed); + if (currentVideoFormat.height != Format.NO_VALUE) { + videoFormatHeightTimeMs += mediaDurationMs; + videoFormatHeightTimeProduct += mediaDurationMs * currentVideoFormat.height; + } + if (currentVideoFormat.bitrate != Format.NO_VALUE) { + videoFormatBitrateTimeMs += mediaDurationMs; + videoFormatBitrateTimeProduct += mediaDurationMs * currentVideoFormat.bitrate; + } + } + lastVideoFormatStartTimeMs = nowMs; + } + + private void maybeRecordAudioFormatTime(long nowMs) { + if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING + && currentAudioFormat != null + && currentAudioFormat.bitrate != Format.NO_VALUE) { + long mediaDurationMs = (long) ((nowMs - lastAudioFormatStartTimeMs) * currentPlaybackSpeed); + audioFormatTimeMs += mediaDurationMs; + audioFormatBitrateTimeProduct += mediaDurationMs * currentAudioFormat.bitrate; + } + lastAudioFormatStartTimeMs = nowMs; + } + + private static boolean isReadyState(@PlaybackState int state) { + return state == PlaybackStats.PLAYBACK_STATE_PLAYING + || state == PlaybackStats.PLAYBACK_STATE_PAUSED + || state == PlaybackStats.PLAYBACK_STATE_SUPPRESSED; + } + + private static boolean isPausedState(@PlaybackState int state) { + return state == PlaybackStats.PLAYBACK_STATE_PAUSED + || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; + } + + private static boolean isRebufferingState(@PlaybackState int state) { + return state == PlaybackStats.PLAYBACK_STATE_BUFFERING + || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING + || state == PlaybackStats.PLAYBACK_STATE_SUPPRESSED_BUFFERING; + } + + private static boolean isInvalidJoinTransition( + @PlaybackState int oldState, @PlaybackState int newState) { + if (oldState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND + && oldState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND + && oldState != PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) { + return false; + } + return newState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND + && newState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND + && newState != PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD + && newState != PlaybackStats.PLAYBACK_STATE_PLAYING + && newState != PlaybackStats.PLAYBACK_STATE_PAUSED + && newState != PlaybackStats.PLAYBACK_STATE_SUPPRESSED + && newState != PlaybackStats.PLAYBACK_STATE_ENDED; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/package-info.java new file mode 100644 index 000000000000..08556b00b003 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.analytics; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Ac3Util.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Ac3Util.java index 7625a00ef521..c68e49dea150 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Ac3Util.java @@ -15,29 +15,56 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.audio; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util.SyncFrameInfo.StreamType; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; /** - * Utility methods for parsing (E-)AC-3 syncframes, which are access units in (E-)AC-3 bitstreams. + * Utility methods for parsing Dolby TrueHD and (E-)AC-3 syncframes. (E-)AC-3 parsing follows the + * definition in ETSI TS 102 366 V1.4.1. */ public final class Ac3Util { - /** - * Holds sample format information as presented by a syncframe header. - */ - public static final class Ac3SyncFrameInfo { + /** Holds sample format information as presented by a syncframe header. */ + public static final class SyncFrameInfo { /** - * The sample mime type of the bitstream. One of {@link MimeTypes#AUDIO_AC3} and - * {@link MimeTypes#AUDIO_E_AC3}. + * AC3 stream types. See also E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, {@link + * #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}. */ - public final String mimeType; + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({STREAM_TYPE_UNDEFINED, STREAM_TYPE_TYPE0, STREAM_TYPE_TYPE1, STREAM_TYPE_TYPE2}) + public @interface StreamType {} + /** Undefined AC3 stream type. */ + public static final int STREAM_TYPE_UNDEFINED = -1; + /** Type 0 AC3 stream type. */ + public static final int STREAM_TYPE_TYPE0 = 0; + /** Type 1 AC3 stream type. */ + public static final int STREAM_TYPE_TYPE1 = 1; + /** Type 2 AC3 stream type. */ + public static final int STREAM_TYPE_TYPE2 = 2; + + /** + * The sample mime type of the bitstream. One of {@link MimeTypes#AUDIO_AC3} and {@link + * MimeTypes#AUDIO_E_AC3}. + */ + @Nullable public final String mimeType; + /** + * The type of the stream if {@link #mimeType} is {@link MimeTypes#AUDIO_E_AC3}, or {@link + * #STREAM_TYPE_UNDEFINED} otherwise. + */ + public final @StreamType int streamType; /** * The audio sampling rate in Hz. */ @@ -55,9 +82,15 @@ public final class Ac3Util { */ public final int sampleCount; - private Ac3SyncFrameInfo(String mimeType, int channelCount, int sampleRate, int frameSize, + private SyncFrameInfo( + @Nullable String mimeType, + @StreamType int streamType, + int channelCount, + int sampleRate, + int frameSize, int sampleCount) { this.mimeType = mimeType; + this.streamType = streamType; this.channelCount = channelCount; this.sampleRate = sampleRate; this.frameSize = frameSize; @@ -66,13 +99,22 @@ public final class Ac3Util { } + /** + * The number of samples to store in each output chunk when rechunking TrueHD streams. The number + * of samples extracted from the container corresponding to one syncframe must be an integer + * multiple of this value. + */ + public static final int TRUEHD_RECHUNK_SAMPLE_COUNT = 16; + /** + * The number of bytes that must be parsed from a TrueHD syncframe to calculate the sample count. + */ + public static final int TRUEHD_SYNCFRAME_PREFIX_LENGTH = 10; + /** * The number of new samples per (E-)AC-3 audio block. */ private static final int AUDIO_SAMPLES_PER_AUDIO_BLOCK = 256; - /** - * Each syncframe has 6 blocks that provide 256 new audio samples. See ETSI TS 102 366 4.1. - */ + /** Each syncframe has 6 blocks that provide 256 new audio samples. See subsection 4.1. */ private static final int AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT = 6 * AUDIO_SAMPLES_PER_AUDIO_BLOCK; /** * Number of audio blocks per E-AC-3 syncframe, indexed by numblkscod. @@ -90,29 +132,30 @@ public final class Ac3Util { * Channel counts, indexed by acmod. */ private static final int[] CHANNEL_COUNT_BY_ACMOD = new int[] {2, 1, 2, 3, 3, 4, 4, 5}; - /** - * Nominal bitrates in kbps, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.) - */ - private static final int[] BITRATE_BY_HALF_FRMSIZECOD = new int[] {32, 40, 48, 56, 64, 80, 96, - 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640}; - /** - * 16-bit words per syncframe, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.) - */ - private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = new int[] {69, 87, 104, - 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393}; + /** Nominal bitrates in kbps, indexed by frmsizecod / 2. (See table 4.13.) */ + private static final int[] BITRATE_BY_HALF_FRMSIZECOD = + new int[] { + 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640 + }; + /** 16-bit words per syncframe, indexed by frmsizecod / 2. (See table 4.13.) */ + private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = + new int[] { + 69, 87, 104, 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, + 1393 + }; /** - * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to - * ETSI TS 102 366 Annex F. The reading position of {@code data} will be modified. + * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to Annex F. + * The reading position of {@code data} will be modified. * * @param data The AC3SpecificBox to parse. - * @param trackId The track identifier to set on the format, or null. + * @param trackId The track identifier to set on the format. * @param language The language to set on the format. * @param drmInitData {@link DrmInitData} to be included in the format. * @return The AC-3 format parsed from data in the header. */ - public static Format parseAc3AnnexFFormat(ParsableByteArray data, String trackId, - String language, DrmInitData drmInitData) { + public static Format parseAc3AnnexFFormat( + ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) { int fscod = (data.readUnsignedByte() & 0xC0) >> 6; int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; int nextByte = data.readUnsignedByte(); @@ -120,26 +163,35 @@ public final class Ac3Util { if ((nextByte & 0x04) != 0) { // lfeon channelCount++; } - return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_AC3, null, Format.NO_VALUE, - Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); + return Format.createAudioSampleFormat( + trackId, + MimeTypes.AUDIO_AC3, + /* codecs= */ null, + Format.NO_VALUE, + Format.NO_VALUE, + channelCount, + sampleRate, + /* initializationData= */ null, + drmInitData, + /* selectionFlags= */ 0, + language); } /** - * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to - * ETSI TS 102 366 Annex F. The reading position of {@code data} will be modified. + * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to Annex + * F. The reading position of {@code data} will be modified. * * @param data The EC3SpecificBox to parse. - * @param trackId The track identifier to set on the format, or null. + * @param trackId The track identifier to set on the format. * @param language The language to set on the format. * @param drmInitData {@link DrmInitData} to be included in the format. * @return The E-AC-3 format parsed from data in the header. */ - public static Format parseEAc3AnnexFFormat(ParsableByteArray data, String trackId, - String language, DrmInitData drmInitData) { + public static Format parseEAc3AnnexFFormat( + ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) { data.skipBytes(2); // data_rate, num_ind_sub - // Read only the first substream. - // TODO: Read later substreams? + // Read the first independent substream. int fscod = (data.readUnsignedByte() & 0xC0) >> 6; int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; int nextByte = data.readUnsignedByte(); @@ -147,8 +199,37 @@ public final class Ac3Util { if ((nextByte & 0x01) != 0) { // lfeon channelCount++; } - return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_E_AC3, null, Format.NO_VALUE, - Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); + + // Read the first dependent substream. + nextByte = data.readUnsignedByte(); + int numDepSub = ((nextByte & 0x1E) >> 1); + if (numDepSub > 0) { + int lowByteChanLoc = data.readUnsignedByte(); + // Read Lrs/Rrs pair + // TODO: Read other channel configuration + if ((lowByteChanLoc & 0x02) != 0) { + channelCount += 2; + } + } + String mimeType = MimeTypes.AUDIO_E_AC3; + if (data.bytesLeft() > 0) { + nextByte = data.readUnsignedByte(); + if ((nextByte & 0x01) != 0) { // flag_ec3_extension_type_a + mimeType = MimeTypes.AUDIO_E_AC3_JOC; + } + } + return Format.createAudioSampleFormat( + trackId, + mimeType, + /* codecs= */ null, + Format.NO_VALUE, + Format.NO_VALUE, + channelCount, + sampleRate, + /* initializationData= */ null, + drmInitData, + /* selectionFlags= */ 0, + language); } /** @@ -158,36 +239,206 @@ public final class Ac3Util { * @param data The data to parse, positioned at the start of the syncframe. * @return The (E-)AC-3 format data parsed from the header. */ - public static Ac3SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { + public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { int initialPosition = data.getPosition(); data.skipBits(40); - boolean isEac3 = data.readBits(5) == 16; + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = data.readBits(5) > 10; data.setPosition(initialPosition); - String mimeType; + @Nullable String mimeType; + @StreamType int streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED; int sampleRate; int acmod; int frameSize; int sampleCount; + boolean lfeon; + int channelCount; if (isEac3) { - mimeType = MimeTypes.AUDIO_E_AC3; - data.skipBits(16 + 2 + 3); // syncword, strmtype, substreamid - frameSize = (data.readBits(11) + 1) * 2; + // Subsection E.1.2. + data.skipBits(16); // syncword + switch (data.readBits(2)) { // strmtyp + case 0: + streamType = SyncFrameInfo.STREAM_TYPE_TYPE0; + break; + case 1: + streamType = SyncFrameInfo.STREAM_TYPE_TYPE1; + break; + case 2: + streamType = SyncFrameInfo.STREAM_TYPE_TYPE2; + break; + default: + streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED; + break; + } + data.skipBits(3); // substreamid + frameSize = (data.readBits(11) + 1) * 2; // See frmsiz in subsection E.1.3.1.3. int fscod = data.readBits(2); int audioBlocks; + int numblkscod; if (fscod == 3) { + numblkscod = 3; sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)]; audioBlocks = 6; } else { - int numblkscod = data.readBits(2); + numblkscod = data.readBits(2); audioBlocks = BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod]; sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; } sampleCount = AUDIO_SAMPLES_PER_AUDIO_BLOCK * audioBlocks; acmod = data.readBits(3); + lfeon = data.readBit(); + channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); + data.skipBits(5 + 5); // bsid, dialnorm + if (data.readBit()) { // compre + data.skipBits(8); // compr + } + if (acmod == 0) { + data.skipBits(5); // dialnorm2 + if (data.readBit()) { // compr2e + data.skipBits(8); // compr2 + } + } + if (streamType == SyncFrameInfo.STREAM_TYPE_TYPE1 && data.readBit()) { // chanmape + data.skipBits(16); // chanmap + } + if (data.readBit()) { // mixmdate + if (acmod > 2) { + data.skipBits(2); // dmixmod + } + if ((acmod & 0x01) != 0 && acmod > 2) { + data.skipBits(3 + 3); // ltrtcmixlev, lorocmixlev + } + if ((acmod & 0x04) != 0) { + data.skipBits(6); // ltrtsurmixlev, lorosurmixlev + } + if (lfeon && data.readBit()) { // lfemixlevcode + data.skipBits(5); // lfemixlevcod + } + if (streamType == SyncFrameInfo.STREAM_TYPE_TYPE0) { + if (data.readBit()) { // pgmscle + data.skipBits(6); //pgmscl + } + if (acmod == 0 && data.readBit()) { // pgmscl2e + data.skipBits(6); // pgmscl2 + } + if (data.readBit()) { // extpgmscle + data.skipBits(6); // extpgmscl + } + int mixdef = data.readBits(2); + if (mixdef == 1) { + data.skipBits(1 + 1 + 3); // premixcmpsel, drcsrc, premixcmpscl + } else if (mixdef == 2) { + data.skipBits(12); // mixdata + } else if (mixdef == 3) { + int mixdeflen = data.readBits(5); + if (data.readBit()) { // mixdata2e + data.skipBits(1 + 1 + 3); // premixcmpsel, drcsrc, premixcmpscl + if (data.readBit()) { // extpgmlscle + data.skipBits(4); // extpgmlscl + } + if (data.readBit()) { // extpgmcscle + data.skipBits(4); // extpgmcscl + } + if (data.readBit()) { // extpgmrscle + data.skipBits(4); // extpgmrscl + } + if (data.readBit()) { // extpgmlsscle + data.skipBits(4); // extpgmlsscl + } + if (data.readBit()) { // extpgmrsscle + data.skipBits(4); // extpgmrsscl + } + if (data.readBit()) { // extpgmlfescle + data.skipBits(4); // extpgmlfescl + } + if (data.readBit()) { // dmixscle + data.skipBits(4); // dmixscl + } + if (data.readBit()) { // addche + if (data.readBit()) { // extpgmaux1scle + data.skipBits(4); // extpgmaux1scl + } + if (data.readBit()) { // extpgmaux2scle + data.skipBits(4); // extpgmaux2scl + } + } + } + if (data.readBit()) { // mixdata3e + data.skipBits(5); // spchdat + if (data.readBit()) { // addspchdate + data.skipBits(5 + 2); // spchdat1, spchan1att + if (data.readBit()) { // addspdat1e + data.skipBits(5 + 3); // spchdat2, spchan2att + } + } + } + data.skipBits(8 * (mixdeflen + 2)); // mixdata + data.byteAlign(); // mixdatafill + } + if (acmod < 2) { + if (data.readBit()) { // paninfoe + data.skipBits(8 + 6); // panmean, paninfo + } + if (acmod == 0) { + if (data.readBit()) { // paninfo2e + data.skipBits(8 + 6); // panmean2, paninfo2 + } + } + } + if (data.readBit()) { // frmmixcfginfoe + if (numblkscod == 0) { + data.skipBits(5); // blkmixcfginfo[0] + } else { + for (int blk = 0; blk < audioBlocks; blk++) { + if (data.readBit()) { // blkmixcfginfoe + data.skipBits(5); // blkmixcfginfo[blk] + } + } + } + } + } + } + if (data.readBit()) { // infomdate + data.skipBits(3 + 1 + 1); // bsmod, copyrightb, origbs + if (acmod == 2) { + data.skipBits(2 + 2); // dsurmod, dheadphonmod + } + if (acmod >= 6) { + data.skipBits(2); // dsurexmod + } + if (data.readBit()) { // audioprodie + data.skipBits(5 + 2 + 1); // mixlevel, roomtyp, adconvtyp + } + if (acmod == 0 && data.readBit()) { // audioprodi2e + data.skipBits(5 + 2 + 1); // mixlevel2, roomtyp2, adconvtyp2 + } + if (fscod < 3) { + data.skipBit(); // sourcefscod + } + } + if (streamType == SyncFrameInfo.STREAM_TYPE_TYPE0 && numblkscod != 3) { + data.skipBit(); // convsync + } + if (streamType == SyncFrameInfo.STREAM_TYPE_TYPE2 + && (numblkscod == 3 || data.readBit())) { // blkid + data.skipBits(6); // frmsizecod + } + mimeType = MimeTypes.AUDIO_E_AC3; + if (data.readBit()) { // addbsie + int addbsil = data.readBits(6); + if (addbsil == 1 && data.readBits(8) == 1) { // addbsi + mimeType = MimeTypes.AUDIO_E_AC3_JOC; + } + } } else /* is AC-3 */ { mimeType = MimeTypes.AUDIO_AC3; data.skipBits(16 + 16); // syncword, crc1 int fscod = data.readBits(2); + if (fscod == 3) { + // fscod '11' indicates that the decoder should not attempt to decode audio. We invalidate + // the mime type to prevent association with a renderer. + mimeType = null; + } int frmsizecod = data.readBits(6); frameSize = getAc3SyncframeSize(fscod, frmsizecod); data.skipBits(5 + 3); // bsid, bsmod @@ -201,48 +452,112 @@ public final class Ac3Util { if (acmod == 2) { data.skipBits(2); // dsurmod } - sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; + sampleRate = + fscod < SAMPLE_RATE_BY_FSCOD.length ? SAMPLE_RATE_BY_FSCOD[fscod] : Format.NO_VALUE; sampleCount = AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; + lfeon = data.readBit(); + channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); } - boolean lfeon = data.readBit(); - int channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); - return new Ac3SyncFrameInfo(mimeType, channelCount, sampleRate, frameSize, sampleCount); + return new SyncFrameInfo( + mimeType, streamType, channelCount, sampleRate, frameSize, sampleCount); } /** - * Returns the size in bytes of the given AC-3 syncframe. + * Returns the size in bytes of the given (E-)AC-3 syncframe. * * @param data The syncframe to parse. * @return The syncframe size in bytes. {@link C#LENGTH_UNSET} if the input is invalid. */ public static int parseAc3SyncframeSize(byte[] data) { - if (data.length < 5) { + if (data.length < 6) { return C.LENGTH_UNSET; } - int fscod = (data[4] & 0xC0) >> 6; - int frmsizecod = data[4] & 0x3F; - return getAc3SyncframeSize(fscod, frmsizecod); + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = ((data[5] & 0xF8) >> 3) > 10; + if (isEac3) { + int frmsiz = (data[2] & 0x07) << 8; // Most significant 3 bits. + frmsiz |= data[3] & 0xFF; // Least significant 8 bits. + return (frmsiz + 1) * 2; // See frmsiz in subsection E.1.3.1.3. + } else { + int fscod = (data[4] & 0xC0) >> 6; + int frmsizecod = data[4] & 0x3F; + return getAc3SyncframeSize(fscod, frmsizecod); + } } /** - * Returns the number of audio samples in an AC-3 syncframe. - */ - public static int getAc3SyncframeAudioSampleCount() { - return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; - } - - /** - * Reads the number of audio samples represented by the given E-AC-3 syncframe. The buffer's + * Reads the number of audio samples represented by the given (E-)AC-3 syncframe. The buffer's * position is not modified. * * @param buffer The {@link ByteBuffer} from which to read the syncframe. * @return The number of audio samples represented by the syncframe. */ - public static int parseEAc3SyncframeAudioSampleCount(ByteBuffer buffer) { - // See ETSI TS 102 366 subsection E.1.2.2. - int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6; - return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (fscod == 0x03 ? 6 - : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]); + public static int parseAc3SyncframeAudioSampleCount(ByteBuffer buffer) { + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = ((buffer.get(buffer.position() + 5) & 0xF8) >> 3) > 10; + if (isEac3) { + int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6; + int numblkscod = fscod == 0x03 ? 3 : (buffer.get(buffer.position() + 4) & 0x30) >> 4; + return BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod] * AUDIO_SAMPLES_PER_AUDIO_BLOCK; + } else { + return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; + } + } + + /** + * Returns the offset relative to the buffer's position of the start of a TrueHD syncframe, or + * {@link C#INDEX_UNSET} if no syncframe was found. The buffer's position is not modified. + * + * @param buffer The {@link ByteBuffer} within which to find a syncframe. + * @return The offset relative to the buffer's position of the start of a TrueHD syncframe, or + * {@link C#INDEX_UNSET} if no syncframe was found. + */ + public static int findTrueHdSyncframeOffset(ByteBuffer buffer) { + int startIndex = buffer.position(); + int endIndex = buffer.limit() - TRUEHD_SYNCFRAME_PREFIX_LENGTH; + for (int i = startIndex; i <= endIndex; i++) { + // The syncword ends 0xBA for TrueHD or 0xBB for MLP. + if ((buffer.getInt(i + 4) & 0xFEFFFFFF) == 0xBA6F72F8) { + return i - startIndex; + } + } + return C.INDEX_UNSET; + } + + /** + * Returns the number of audio samples represented by the given TrueHD syncframe, or 0 if the + * buffer is not the start of a syncframe. + * + * @param syncframe The bytes from which to read the syncframe. Must be at least {@link + * #TRUEHD_SYNCFRAME_PREFIX_LENGTH} bytes long. + * @return The number of audio samples represented by the syncframe, or 0 if the buffer doesn't + * contain the start of a syncframe. + */ + public static int parseTrueHdSyncframeAudioSampleCount(byte[] syncframe) { + // See "Dolby TrueHD (MLP) high-level bitstream description" on the Dolby developer site, + // subsections 2.2 and 4.2.1. The syncword ends 0xBA for TrueHD or 0xBB for MLP. + if (syncframe[4] != (byte) 0xF8 + || syncframe[5] != (byte) 0x72 + || syncframe[6] != (byte) 0x6F + || (syncframe[7] & 0xFE) != 0xBA) { + return 0; + } + boolean isMlp = (syncframe[7] & 0xFF) == 0xBB; + return 40 << ((syncframe[isMlp ? 9 : 8] >> 4) & 0x07); + } + + /** + * Reads the number of audio samples represented by a TrueHD syncframe. The buffer's position is + * not modified. + * + * @param buffer The {@link ByteBuffer} from which to read the syncframe. + * @param offset The offset of the start of the syncframe relative to the buffer's position. + * @return The number of audio samples represented by the syncframe. + */ + public static int parseTrueHdSyncframeAudioSampleCount(ByteBuffer buffer, int offset) { + // TODO: Link to specification if available. + boolean isMlp = (buffer.get(buffer.position() + offset + 7) & 0xFF) == 0xBB; + return 40 << ((buffer.get(buffer.position() + offset + (isMlp ? 9 : 8)) >> 4) & 0x07); } private static int getAc3SyncframeSize(int fscod, int frmsizecod) { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Ac4Util.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Ac4Util.java new file mode 100644 index 000000000000..a921346e9085 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Ac4Util.java @@ -0,0 +1,250 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.nio.ByteBuffer; + +/** Utility methods for parsing AC-4 frames, which are access units in AC-4 bitstreams. */ +public final class Ac4Util { + + /** Holds sample format information as presented by a syncframe header. */ + public static final class SyncFrameInfo { + + /** The bitstream version. */ + public final int bitstreamVersion; + /** The audio sampling rate in Hz. */ + public final int sampleRate; + /** The number of audio channels */ + public final int channelCount; + /** The size of the frame. */ + public final int frameSize; + /** Number of audio samples in the frame. */ + public final int sampleCount; + + private SyncFrameInfo( + int bitstreamVersion, int channelCount, int sampleRate, int frameSize, int sampleCount) { + this.bitstreamVersion = bitstreamVersion; + this.channelCount = channelCount; + this.sampleRate = sampleRate; + this.frameSize = frameSize; + this.sampleCount = sampleCount; + } + } + + public static final int AC40_SYNCWORD = 0xAC40; + public static final int AC41_SYNCWORD = 0xAC41; + + /** The channel count of AC-4 stream. */ + // TODO: Parse AC-4 stream channel count. + private static final int CHANNEL_COUNT_2 = 2; + /** + * The AC-4 sync frame header size for extractor. The seven bytes are 0xAC, 0x40, 0xFF, 0xFF, + * sizeByte1, sizeByte2, sizeByte3. See ETSI TS 103 190-1 V1.3.1, Annex G + */ + public static final int SAMPLE_HEADER_SIZE = 7; + /** + * The header size for AC-4 parser. Only needs to be as big as we need to read, not the full + * header size. + */ + public static final int HEADER_SIZE_FOR_PARSER = 16; + /** + * Number of audio samples in the frame. Defined in IEC61937-14:2017 table 5 and 6. This table + * provides the number of samples per frame at the playback sampling frequency of 48 kHz. For 44.1 + * kHz, only frame_rate_index(13) is valid and corresponding sample count is 2048. + */ + private static final int[] SAMPLE_COUNT = + new int[] { + /* [ 0] 23.976 fps */ 2002, + /* [ 1] 24 fps */ 2000, + /* [ 2] 25 fps */ 1920, + /* [ 3] 29.97 fps */ 1601, // 1601 | 1602 | 1601 | 1602 | 1602 + /* [ 4] 30 fps */ 1600, + /* [ 5] 47.95 fps */ 1001, + /* [ 6] 48 fps */ 1000, + /* [ 7] 50 fps */ 960, + /* [ 8] 59.94 fps */ 800, // 800 | 801 | 801 | 801 | 801 + /* [ 9] 60 fps */ 800, + /* [10] 100 fps */ 480, + /* [11] 119.88 fps */ 400, // 400 | 400 | 401 | 400 | 401 + /* [12] 120 fps */ 400, + /* [13] 23.438 fps */ 2048 + }; + + /** + * Returns the AC-4 format given {@code data} containing the AC4SpecificBox according to ETSI TS + * 103 190-1 Annex E. The reading position of {@code data} will be modified. + * + * @param data The AC4SpecificBox to parse. + * @param trackId The track identifier to set on the format. + * @param language The language to set on the format. + * @param drmInitData {@link DrmInitData} to be included in the format. + * @return The AC-4 format parsed from data in the header. + */ + public static Format parseAc4AnnexEFormat( + ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) { + data.skipBytes(1); // ac4_dsi_version, bitstream_version[0:5] + int sampleRate = ((data.readUnsignedByte() & 0x20) >> 5 == 1) ? 48000 : 44100; + return Format.createAudioSampleFormat( + trackId, + MimeTypes.AUDIO_AC4, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + CHANNEL_COUNT_2, + sampleRate, + /* initializationData= */ null, + drmInitData, + /* selectionFlags= */ 0, + language); + } + + /** + * Returns AC-4 format information given {@code data} containing a syncframe. The reading position + * of {@code data} will be modified. + * + * @param data The data to parse, positioned at the start of the syncframe. + * @return The AC-4 format data parsed from the header. + */ + public static SyncFrameInfo parseAc4SyncframeInfo(ParsableBitArray data) { + int headerSize = 0; + int syncWord = data.readBits(16); + headerSize += 2; + int frameSize = data.readBits(16); + headerSize += 2; + if (frameSize == 0xFFFF) { + frameSize = data.readBits(24); + headerSize += 3; // Extended frame_size + } + frameSize += headerSize; + if (syncWord == AC41_SYNCWORD) { + frameSize += 2; // crc_word + } + int bitstreamVersion = data.readBits(2); + if (bitstreamVersion == 3) { + bitstreamVersion += readVariableBits(data, /* bitsPerRead= */ 2); + } + int sequenceCounter = data.readBits(10); + if (data.readBit()) { // b_wait_frames + if (data.readBits(3) > 0) { // wait_frames + data.skipBits(2); // reserved + } + } + int sampleRate = data.readBit() ? 48000 : 44100; + int frameRateIndex = data.readBits(4); + int sampleCount = 0; + if (sampleRate == 44100 && frameRateIndex == 13) { + sampleCount = SAMPLE_COUNT[frameRateIndex]; + } else if (sampleRate == 48000 && frameRateIndex < SAMPLE_COUNT.length) { + sampleCount = SAMPLE_COUNT[frameRateIndex]; + switch (sequenceCounter % 5) { + case 1: // fall through + case 3: + if (frameRateIndex == 3 || frameRateIndex == 8) { + sampleCount++; + } + break; + case 2: + if (frameRateIndex == 8 || frameRateIndex == 11) { + sampleCount++; + } + break; + case 4: + if (frameRateIndex == 3 || frameRateIndex == 8 || frameRateIndex == 11) { + sampleCount++; + } + break; + default: + break; + } + } + return new SyncFrameInfo(bitstreamVersion, CHANNEL_COUNT_2, sampleRate, frameSize, sampleCount); + } + + /** + * Returns the size in bytes of the given AC-4 syncframe. + * + * @param data The syncframe to parse. + * @param syncword The syncword value for the syncframe. + * @return The syncframe size in bytes, or {@link C#LENGTH_UNSET} if the input is invalid. + */ + public static int parseAc4SyncframeSize(byte[] data, int syncword) { + if (data.length < 7) { + return C.LENGTH_UNSET; + } + int headerSize = 2; // syncword + int frameSize = ((data[2] & 0xFF) << 8) | (data[3] & 0xFF); + headerSize += 2; + if (frameSize == 0xFFFF) { + frameSize = ((data[4] & 0xFF) << 16) | ((data[5] & 0xFF) << 8) | (data[6] & 0xFF); + headerSize += 3; + } + if (syncword == AC41_SYNCWORD) { + headerSize += 2; + } + frameSize += headerSize; + return frameSize; + } + + /** + * Reads the number of audio samples represented by the given AC-4 syncframe. The buffer's + * position is not modified. + * + * @param buffer The {@link ByteBuffer} from which to read the syncframe. + * @return The number of audio samples represented by the syncframe. + */ + public static int parseAc4SyncframeAudioSampleCount(ByteBuffer buffer) { + byte[] bufferBytes = new byte[HEADER_SIZE_FOR_PARSER]; + int position = buffer.position(); + buffer.get(bufferBytes); + buffer.position(position); + return parseAc4SyncframeInfo(new ParsableBitArray(bufferBytes)).sampleCount; + } + + /** Populates {@code buffer} with an AC-4 sample header for a sample of the specified size. */ + public static void getAc4SampleHeader(int size, ParsableByteArray buffer) { + // See ETSI TS 103 190-1 V1.3.1, Annex G. + buffer.reset(SAMPLE_HEADER_SIZE); + buffer.data[0] = (byte) 0xAC; + buffer.data[1] = 0x40; + buffer.data[2] = (byte) 0xFF; + buffer.data[3] = (byte) 0xFF; + buffer.data[4] = (byte) ((size >> 16) & 0xFF); + buffer.data[5] = (byte) ((size >> 8) & 0xFF); + buffer.data[6] = (byte) (size & 0xFF); + } + + private static int readVariableBits(ParsableBitArray data, int bitsPerRead) { + int value = 0; + while (true) { + value += data.readBits(bitsPerRead); + if (!data.readBit()) { + break; + } + value++; + value <<= bitsPerRead; + } + return value; + } + + private Ac4Util() {} +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioAttributes.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioAttributes.java new file mode 100644 index 000000000000..d0f3fcb4387c --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioAttributes.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +import android.annotation.TargetApi; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * Attributes for audio playback, which configure the underlying platform + * {@link android.media.AudioTrack}. + *

+ * To set the audio attributes, create an instance using the {@link Builder} and either pass it to + * {@link org.mozilla.thirdparty.com.google.android.exoplayer2SimpleExoPlayer#setAudioAttributes(AudioAttributes)} or + * send a message of type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to the audio renderers. + *

+ * This class is based on {@link android.media.AudioAttributes}, but can be used on all supported + * API versions. + */ +public final class AudioAttributes { + + public static final AudioAttributes DEFAULT = new Builder().build(); + + /** + * Builder for {@link AudioAttributes}. + */ + public static final class Builder { + + private @C.AudioContentType int contentType; + private @C.AudioFlags int flags; + private @C.AudioUsage int usage; + private @C.AudioAllowedCapturePolicy int allowedCapturePolicy; + + /** + * Creates a new builder for {@link AudioAttributes}. + * + *

By default the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage is {@link + * C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags are set. + */ + public Builder() { + contentType = C.CONTENT_TYPE_UNKNOWN; + flags = 0; + usage = C.USAGE_MEDIA; + allowedCapturePolicy = C.ALLOW_CAPTURE_BY_ALL; + } + + /** + * @see android.media.AudioAttributes.Builder#setContentType(int) + */ + public Builder setContentType(@C.AudioContentType int contentType) { + this.contentType = contentType; + return this; + } + + /** + * @see android.media.AudioAttributes.Builder#setFlags(int) + */ + public Builder setFlags(@C.AudioFlags int flags) { + this.flags = flags; + return this; + } + + /** + * @see android.media.AudioAttributes.Builder#setUsage(int) + */ + public Builder setUsage(@C.AudioUsage int usage) { + this.usage = usage; + return this; + } + + /** See {@link android.media.AudioAttributes.Builder#setAllowedCapturePolicy(int)}. */ + public Builder setAllowedCapturePolicy(@C.AudioAllowedCapturePolicy int allowedCapturePolicy) { + this.allowedCapturePolicy = allowedCapturePolicy; + return this; + } + + /** Creates an {@link AudioAttributes} instance from this builder. */ + public AudioAttributes build() { + return new AudioAttributes(contentType, flags, usage, allowedCapturePolicy); + } + + } + + public final @C.AudioContentType int contentType; + public final @C.AudioFlags int flags; + public final @C.AudioUsage int usage; + public final @C.AudioAllowedCapturePolicy int allowedCapturePolicy; + + @Nullable private android.media.AudioAttributes audioAttributesV21; + + private AudioAttributes( + @C.AudioContentType int contentType, + @C.AudioFlags int flags, + @C.AudioUsage int usage, + @C.AudioAllowedCapturePolicy int allowedCapturePolicy) { + this.contentType = contentType; + this.flags = flags; + this.usage = usage; + this.allowedCapturePolicy = allowedCapturePolicy; + } + + /** + * Returns a {@link android.media.AudioAttributes} from this instance. + * + *

Field {@link AudioAttributes#allowedCapturePolicy} is ignored for API levels prior to 29. + */ + @TargetApi(21) + public android.media.AudioAttributes getAudioAttributesV21() { + if (audioAttributesV21 == null) { + android.media.AudioAttributes.Builder builder = + new android.media.AudioAttributes.Builder() + .setContentType(contentType) + .setFlags(flags) + .setUsage(usage); + if (Util.SDK_INT >= 29) { + builder.setAllowedCapturePolicy(allowedCapturePolicy); + } + audioAttributesV21 = builder.build(); + } + return audioAttributesV21; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + AudioAttributes other = (AudioAttributes) obj; + return this.contentType == other.contentType + && this.flags == other.flags + && this.usage == other.usage + && this.allowedCapturePolicy == other.allowedCapturePolicy; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + contentType; + result = 31 * result + flags; + result = 31 * result + usage; + result = 31 * result + allowedCapturePolicy; + return result; + } + +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilities.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilities.java index 2e494347f93b..f985891465c2 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilities.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilities.java @@ -22,19 +22,32 @@ import android.content.Intent; import android.content.IntentFilter; import android.media.AudioFormat; import android.media.AudioManager; +import android.net.Uri; +import android.provider.Settings.Global; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.util.Arrays; -/** - * Represents the set of audio formats that a device is capable of playing. - */ +/** Represents the set of audio formats that a device is capable of playing. */ @TargetApi(21) public final class AudioCapabilities { - /** - * The minimum audio capabilities supported by all devices. - */ + private static final int DEFAULT_MAX_CHANNEL_COUNT = 8; + + /** The minimum audio capabilities supported by all devices. */ public static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES = - new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, 2); + new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, DEFAULT_MAX_CHANNEL_COUNT); + + /** Audio capabilities when the device specifies external surround sound. */ + private static final AudioCapabilities EXTERNAL_SURROUND_SOUND_CAPABILITIES = + new AudioCapabilities( + new int[] { + AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_AC3, AudioFormat.ENCODING_E_AC3 + }, + DEFAULT_MAX_CHANNEL_COUNT); + + /** Global settings key for devices that can specify external surround sound. */ + private static final String EXTERNAL_SURROUND_SOUND_KEY = "external_surround_sound_enabled"; /** * Returns the current audio capabilities for the device. @@ -44,17 +57,36 @@ public final class AudioCapabilities { */ @SuppressWarnings("InlinedApi") public static AudioCapabilities getCapabilities(Context context) { - return getCapabilities( - context.registerReceiver(null, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG))); + Intent intent = + context.registerReceiver( + /* receiver= */ null, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG)); + return getCapabilities(context, intent); } @SuppressLint("InlinedApi") - /* package */ static AudioCapabilities getCapabilities(Intent intent) { + /* package */ static AudioCapabilities getCapabilities(Context context, @Nullable Intent intent) { + if (deviceMaySetExternalSurroundSoundGlobalSetting() + && Global.getInt(context.getContentResolver(), EXTERNAL_SURROUND_SOUND_KEY, 0) == 1) { + return EXTERNAL_SURROUND_SOUND_CAPABILITIES; + } if (intent == null || intent.getIntExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 0) == 0) { return DEFAULT_AUDIO_CAPABILITIES; } - return new AudioCapabilities(intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS), - intent.getIntExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, 0)); + return new AudioCapabilities( + intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS), + intent.getIntExtra( + AudioManager.EXTRA_MAX_CHANNEL_COUNT, /* defaultValue= */ DEFAULT_MAX_CHANNEL_COUNT)); + } + + /** + * Returns the global settings {@link Uri} used by the device to specify external surround sound, + * or null if the device does not support this functionality. + */ + @Nullable + /* package */ static Uri getExternalSurroundSoundGlobalSettingUri() { + return deviceMaySetExternalSurroundSoundGlobalSetting() + ? Global.getUriFor(EXTERNAL_SURROUND_SOUND_KEY) + : null; } private final int[] supportedEncodings; @@ -64,11 +96,15 @@ public final class AudioCapabilities { * Constructs new audio capabilities based on a set of supported encodings and a maximum channel * count. * + *

Applications should generally call {@link #getCapabilities(Context)} to obtain an instance + * based on the capabilities advertised by the platform, rather than calling this constructor. + * * @param supportedEncodings Supported audio encodings from {@link android.media.AudioFormat}'s - * {@code ENCODING_*} constants. + * {@code ENCODING_*} constants. Passing {@code null} indicates that no encodings are + * supported. * @param maxChannelCount The maximum number of audio channels that can be played simultaneously. */ - /* package */ AudioCapabilities(int[] supportedEncodings, int maxChannelCount) { + public AudioCapabilities(@Nullable int[] supportedEncodings, int maxChannelCount) { if (supportedEncodings != null) { this.supportedEncodings = Arrays.copyOf(supportedEncodings, supportedEncodings.length); Arrays.sort(this.supportedEncodings); @@ -96,7 +132,7 @@ public final class AudioCapabilities { } @Override - public boolean equals(Object other) { + public boolean equals(@Nullable Object other) { if (this == other) { return true; } @@ -119,4 +155,7 @@ public final class AudioCapabilities { + ", supportedEncodings=" + Arrays.toString(supportedEncodings) + "]"; } + private static boolean deviceMaySetExternalSurroundSoundGlobalSetting() { + return Util.SDK_INT >= 17 && "Amazon".equals(Util.MANUFACTURER); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java index f71026510bdb..d96fd32f53a5 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java @@ -16,10 +16,15 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.audio; import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.database.ContentObserver; import android.media.AudioManager; +import android.net.Uri; +import android.os.Handler; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; @@ -45,18 +50,29 @@ public final class AudioCapabilitiesReceiver { private final Context context; private final Listener listener; - private final BroadcastReceiver receiver; + private final Handler handler; + @Nullable private final BroadcastReceiver receiver; + @Nullable private final ExternalSurroundSoundSettingObserver externalSurroundSoundSettingObserver; - /* package */ AudioCapabilities audioCapabilities; + /* package */ @Nullable AudioCapabilities audioCapabilities; + private boolean registered; /** * @param context A context for registering the receiver. * @param listener The listener to notify when audio capabilities change. */ public AudioCapabilitiesReceiver(Context context, Listener listener) { - this.context = Assertions.checkNotNull(context); + context = context.getApplicationContext(); + this.context = context; this.listener = Assertions.checkNotNull(listener); - this.receiver = Util.SDK_INT >= 21 ? new HdmiAudioPlugBroadcastReceiver() : null; + handler = new Handler(Util.getLooper()); + receiver = Util.SDK_INT >= 21 ? new HdmiAudioPlugBroadcastReceiver() : null; + Uri externalSurroundSoundUri = AudioCapabilities.getExternalSurroundSoundGlobalSettingUri(); + externalSurroundSoundSettingObserver = + externalSurroundSoundUri != null + ? new ExternalSurroundSoundSettingObserver( + handler, context.getContentResolver(), externalSurroundSoundUri) + : null; } /** @@ -68,9 +84,21 @@ public final class AudioCapabilitiesReceiver { */ @SuppressWarnings("InlinedApi") public AudioCapabilities register() { - Intent stickyIntent = receiver == null ? null - : context.registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG)); - audioCapabilities = AudioCapabilities.getCapabilities(stickyIntent); + if (registered) { + return Assertions.checkNotNull(audioCapabilities); + } + registered = true; + if (externalSurroundSoundSettingObserver != null) { + externalSurroundSoundSettingObserver.register(); + } + Intent stickyIntent = null; + if (receiver != null) { + IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG); + stickyIntent = + context.registerReceiver( + receiver, intentFilter, /* broadcastPermission= */ null, handler); + } + audioCapabilities = AudioCapabilities.getCapabilities(context, stickyIntent); return audioCapabilities; } @@ -79,9 +107,24 @@ public final class AudioCapabilitiesReceiver { * changes occur. */ public void unregister() { + if (!registered) { + return; + } + audioCapabilities = null; if (receiver != null) { context.unregisterReceiver(receiver); } + if (externalSurroundSoundSettingObserver != null) { + externalSurroundSoundSettingObserver.unregister(); + } + registered = false; + } + + private void onNewAudioCapabilities(AudioCapabilities newAudioCapabilities) { + if (registered && !newAudioCapabilities.equals(audioCapabilities)) { + audioCapabilities = newAudioCapabilities; + listener.onAudioCapabilitiesChanged(newAudioCapabilities); + } } private final class HdmiAudioPlugBroadcastReceiver extends BroadcastReceiver { @@ -89,14 +132,35 @@ public final class AudioCapabilitiesReceiver { @Override public void onReceive(Context context, Intent intent) { if (!isInitialStickyBroadcast()) { - AudioCapabilities newAudioCapabilities = AudioCapabilities.getCapabilities(intent); - if (!newAudioCapabilities.equals(audioCapabilities)) { - audioCapabilities = newAudioCapabilities; - listener.onAudioCapabilitiesChanged(newAudioCapabilities); - } + onNewAudioCapabilities(AudioCapabilities.getCapabilities(context, intent)); } } + } + private final class ExternalSurroundSoundSettingObserver extends ContentObserver { + + private final ContentResolver resolver; + private final Uri settingUri; + + public ExternalSurroundSoundSettingObserver( + Handler handler, ContentResolver resolver, Uri settingUri) { + super(handler); + this.resolver = resolver; + this.settingUri = settingUri; + } + + public void register() { + resolver.registerContentObserver(settingUri, /* notifyForDescendants= */ false, this); + } + + public void unregister() { + resolver.unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange) { + onNewAudioCapabilities(AudioCapabilities.getCapabilities(context)); + } } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioDecoderException.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioDecoderException.java index 5e49d288974d..0f4ac159b99c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioDecoderException.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioDecoderException.java @@ -15,27 +15,21 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.audio; -/** - * Thrown when an audio decoder error occurs. - */ -public abstract class AudioDecoderException extends Exception { +/** Thrown when an audio decoder error occurs. */ +public class AudioDecoderException extends Exception { - /** - * @param detailMessage The detail message for this exception. - */ - public AudioDecoderException(String detailMessage) { - super(detailMessage); + /** @param message The detail message for this exception. */ + public AudioDecoderException(String message) { + super(message); } /** - * @param detailMessage The detail message for this exception. - * @param cause the cause (which is saved for later retrieval by the - * {@link #getCause()} method). (A null value is - * permitted, and indicates that the cause is nonexistent or - * unknown.) + * @param message The detail message for this exception. + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). + * A null value is permitted, and indicates that the cause is nonexistent or unknown. */ - public AudioDecoderException(String detailMessage, Throwable cause) { - super(detailMessage, cause); + public AudioDecoderException(String message, Throwable cause) { + super(message, cause); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioListener.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioListener.java new file mode 100644 index 000000000000..457f52b88760 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioListener.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +/** A listener for changes in audio configuration. */ +public interface AudioListener { + + /** + * Called when the audio session is set. + * + * @param audioSessionId The audio session id. + */ + default void onAudioSessionId(int audioSessionId) {} + + /** + * Called when the audio attributes change. + * + * @param audioAttributes The audio attributes. + */ + default void onAudioAttributesChanged(AudioAttributes audioAttributes) {} + + /** + * Called when the volume changes. + * + * @param volume The new volume, with 0 being silence and 1 being unity gain. + */ + default void onVolumeChanged(float volume) {} +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioProcessor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioProcessor.java index 1649f2d23fce..e0814314ca69 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioProcessor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioProcessor.java @@ -16,65 +16,92 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.audio; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.nio.ByteOrder; /** - * Interface for audio processors. + * Interface for audio processors, which take audio data as input and transform it, potentially + * modifying its channel count, encoding and/or sample rate. + * + *

In addition to being able to modify the format of audio, implementations may allow parameters + * to be set that affect the output audio and whether the processor is active/inactive. */ public interface AudioProcessor { - /** - * Exception thrown when a processor can't be configured for a given input audio format. - */ - final class UnhandledFormatException extends Exception { + /** PCM audio format that may be handled by an audio processor. */ + final class AudioFormat { + public static final AudioFormat NOT_SET = + new AudioFormat( + /* sampleRate= */ Format.NO_VALUE, + /* channelCount= */ Format.NO_VALUE, + /* encoding= */ Format.NO_VALUE); - public UnhandledFormatException(int sampleRateHz, int channelCount, @C.Encoding int encoding) { - super("Unhandled format: " + sampleRateHz + " Hz, " + channelCount + " channels in encoding " - + encoding); + /** The sample rate in Hertz. */ + public final int sampleRate; + /** The number of interleaved channels. */ + public final int channelCount; + /** The type of linear PCM encoding. */ + @C.PcmEncoding public final int encoding; + /** The number of bytes used to represent one audio frame. */ + public final int bytesPerFrame; + + public AudioFormat(int sampleRate, int channelCount, @C.PcmEncoding int encoding) { + this.sampleRate = sampleRate; + this.channelCount = channelCount; + this.encoding = encoding; + bytesPerFrame = + Util.isEncodingLinearPcm(encoding) + ? Util.getPcmFrameSize(encoding, channelCount) + : Format.NO_VALUE; + } + + @Override + public String toString() { + return "AudioFormat[" + + "sampleRate=" + + sampleRate + + ", channelCount=" + + channelCount + + ", encoding=" + + encoding + + ']'; + } + } + + /** Exception thrown when a processor can't be configured for a given input audio format. */ + final class UnhandledAudioFormatException extends Exception { + + public UnhandledAudioFormatException(AudioFormat inputAudioFormat) { + super("Unhandled format: " + inputAudioFormat); } } - /** - * An empty, direct {@link ByteBuffer}. - */ + /** An empty, direct {@link ByteBuffer}. */ ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder()); /** * Configures the processor to process input audio with the specified format. After calling this - * method, {@link #isActive()} returns whether the processor needs to handle buffers; if not, the - * processor will not accept any buffers until it is reconfigured. Returns {@code true} if the - * processor must be flushed, or if the value returned by {@link #isActive()} has changed as a - * result of the call. If it's active, {@link #getOutputChannelCount()} and - * {@link #getOutputEncoding()} return the processor's output format. + * method, call {@link #isActive()} to determine whether the audio processor is active. Returns + * the configured output audio format if this instance is active. * - * @param sampleRateHz The sample rate of input audio in Hz. - * @param channelCount The number of interleaved channels in input audio. - * @param encoding The encoding of input audio. - * @return {@code true} if the processor must be flushed or the value returned by - * {@link #isActive()} has changed as a result of the call. - * @throws UnhandledFormatException Thrown if the specified format can't be handled as input. + *

After calling this method, it is necessary to {@link #flush()} the processor to apply the + * new configuration. Before applying the new configuration, it is safe to queue input and get + * output in the old input/output formats. Call {@link #queueEndOfStream()} when no more input + * will be supplied in the old input format. + * + * @param inputAudioFormat The format of audio that will be queued after the next call to {@link + * #flush()}. + * @return The configured output audio format if this instance is {@link #isActive() active}. + * @throws UnhandledAudioFormatException Thrown if the specified format can't be handled as input. */ - boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) - throws UnhandledFormatException; + AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException; - /** - * Returns whether the processor is configured and active. - */ + /** Returns whether the processor is configured and will process input buffers. */ boolean isActive(); - /** - * Returns the number of audio channels in the data output by the processor. - */ - int getOutputChannelCount(); - - /** - * Returns the audio encoding used in the data output by the processor. - */ - @C.Encoding - int getOutputEncoding(); - /** * Queues audio data between the position and limit of the input {@code buffer} for processing. * {@code buffer} must be a direct byte buffer with native byte order. Its contents are treated as @@ -111,13 +138,11 @@ public interface AudioProcessor { boolean isEnded(); /** - * Clears any state in preparation for receiving a new stream of input buffers. + * Clears any buffered data and pending output. If the audio processor is active, also prepares + * the audio processor to receive a new stream of input in the last configured (pending) format. */ void flush(); - /** - * Resets the processor to its initial state. - */ + /** Resets the processor to its unconfigured state, releasing any resources. */ void reset(); - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioRendererEventListener.java index 5dfdae0b18c7..bb1ae7285576 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioRendererEventListener.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioRendererEventListener.java @@ -15,8 +15,11 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.audio; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Handler; import android.os.SystemClock; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.Renderer; @@ -24,7 +27,8 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderCount import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; /** - * Listener of audio {@link Renderer} events. + * Listener of audio {@link Renderer} events. All methods have no-op default implementations to + * allow selective overrides. */ public interface AudioRendererEventListener { @@ -34,14 +38,14 @@ public interface AudioRendererEventListener { * @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it * remains enabled. */ - void onAudioEnabled(DecoderCounters counters); + default void onAudioEnabled(DecoderCounters counters) {} /** * Called when the audio session is set. * * @param audioSessionId The audio session id. */ - void onAudioSessionId(int audioSessionId); + default void onAudioSessionId(int audioSessionId) {} /** * Called when a decoder is created. @@ -51,48 +55,50 @@ public interface AudioRendererEventListener { * finished. * @param initializationDurationMs The time taken to initialize the decoder in milliseconds. */ - void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs, - long initializationDurationMs); + default void onAudioDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) {} /** * Called when the format of the media being consumed by the renderer changes. * * @param format The new format. */ - void onAudioInputFormatChanged(Format format); + default void onAudioInputFormatChanged(Format format) {} /** - * Called when an {@link AudioTrack} underrun occurs. + * Called when an {@link AudioSink} underrun occurs. * - * @param bufferSize The size of the {@link AudioTrack}'s buffer, in bytes. - * @param bufferSizeMs The size of the {@link AudioTrack}'s buffer, in milliseconds, if it is + * @param bufferSize The size of the {@link AudioSink}'s buffer, in bytes. + * @param bufferSizeMs The size of the {@link AudioSink}'s buffer, in milliseconds, if it is * configured for PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output, * as the buffered media can have a variable bitrate so the duration may be unknown. - * @param elapsedSinceLastFeedMs The time since the {@link AudioTrack} was last fed data. + * @param elapsedSinceLastFeedMs The time since the {@link AudioSink} was last fed data. */ - void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); + default void onAudioSinkUnderrun( + int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {} /** * Called when the renderer is disabled. * * @param counters {@link DecoderCounters} that were updated by the renderer. */ - void onAudioDisabled(DecoderCounters counters); + default void onAudioDisabled(DecoderCounters counters) {} /** * Dispatches events to a {@link AudioRendererEventListener}. */ final class EventDispatcher { - private final Handler handler; - private final AudioRendererEventListener listener; + @Nullable private final Handler handler; + @Nullable private final AudioRendererEventListener listener; /** * @param handler A handler for dispatching events, or null if creating a dummy instance. * @param listener The listener to which events should be dispatched, or null if creating a * dummy instance. */ - public EventDispatcher(Handler handler, AudioRendererEventListener listener) { + public EventDispatcher(@Nullable Handler handler, + @Nullable AudioRendererEventListener listener) { this.handler = listener != null ? Assertions.checkNotNull(handler) : null; this.listener = listener; } @@ -101,13 +107,8 @@ public interface AudioRendererEventListener { * Invokes {@link AudioRendererEventListener#onAudioEnabled(DecoderCounters)}. */ public void enabled(final DecoderCounters decoderCounters) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onAudioEnabled(decoderCounters); - } - }); + if (handler != null) { + handler.post(() -> castNonNull(listener).onAudioEnabled(decoderCounters)); } } @@ -116,14 +117,12 @@ public interface AudioRendererEventListener { */ public void decoderInitialized(final String decoderName, final long initializedTimestampMs, final long initializationDurationMs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onAudioDecoderInitialized(decoderName, initializedTimestampMs, - initializationDurationMs); - } - }); + if (handler != null) { + handler.post( + () -> + castNonNull(listener) + .onAudioDecoderInitialized( + decoderName, initializedTimestampMs, initializationDurationMs)); } } @@ -131,28 +130,21 @@ public interface AudioRendererEventListener { * Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}. */ public void inputFormatChanged(final Format format) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onAudioInputFormatChanged(format); - } - }); + if (handler != null) { + handler.post(() -> castNonNull(listener).onAudioInputFormatChanged(format)); } } /** - * Invokes {@link AudioRendererEventListener#onAudioTrackUnderrun(int, long, long)}. + * Invokes {@link AudioRendererEventListener#onAudioSinkUnderrun(int, long, long)}. */ public void audioTrackUnderrun(final int bufferSize, final long bufferSizeMs, final long elapsedSinceLastFeedMs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); - } - }); + if (handler != null) { + handler.post( + () -> + castNonNull(listener) + .onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs)); } } @@ -160,14 +152,13 @@ public interface AudioRendererEventListener { * Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}. */ public void disabled(final DecoderCounters counters) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - counters.ensureUpdated(); - listener.onAudioDisabled(counters); - } - }); + counters.ensureUpdated(); + if (handler != null) { + handler.post( + () -> { + counters.ensureUpdated(); + castNonNull(listener).onAudioDisabled(counters); + }); } } @@ -175,16 +166,9 @@ public interface AudioRendererEventListener { * Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}. */ public void audioSessionId(final int audioSessionId) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onAudioSessionId(audioSessionId); - } - }); + if (handler != null) { + handler.post(() -> castNonNull(listener).onAudioSessionId(audioSessionId)); } } - } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioSink.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioSink.java new file mode 100644 index 000000000000..db87e28e7f3f --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioSink.java @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +import android.media.AudioTrack; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters; +import java.nio.ByteBuffer; + +/** + * A sink that consumes audio data. + * + *

Before starting playback, specify the input audio format by calling {@link #configure(int, + * int, int, int, int[], int, int)}. + * + *

Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} + * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data. + * + *

Call {@link #configure(int, int, int, int, int[], int, int)} whenever the input format + * changes. The sink will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, + * long)}. + * + *

Call {@link #flush()} to prepare the sink to receive audio data from a new playback position. + * + *

Call {@link #playToEndOfStream()} repeatedly to play out all data when no more input buffers + * will be provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #flush()}. + * Call {@link #reset()} when the instance is no longer required. + * + *

The implementation may be backed by a platform {@link AudioTrack}. In this case, {@link + * #setAudioSessionId(int)}, {@link #setAudioAttributes(AudioAttributes)}, {@link + * #enableTunnelingV21(int)} and/or {@link #disableTunneling()} may be called before writing data to + * the sink. These methods may also be called after writing data to the sink, in which case it will + * be reinitialized as required. For implementations that are not based on platform {@link + * AudioTrack}s, calling methods relating to audio sessions, audio attributes, and tunneling may + * have no effect. + */ +public interface AudioSink { + + /** + * Listener for audio sink events. + */ + interface Listener { + + /** + * Called if the audio sink has started rendering audio to a new platform audio session. + * + * @param audioSessionId The newly generated audio session's identifier. + */ + void onAudioSessionId(int audioSessionId); + + /** + * Called when the audio sink handles a buffer whose timestamp is discontinuous with the last + * buffer handled since it was reset. + */ + void onPositionDiscontinuity(); + + /** + * Called when the audio sink runs out of data. + *

+ * An audio sink implementation may never call this method (for example, if audio data is + * consumed in batches rather than based on the sink's own clock). + * + * @param bufferSize The size of the sink's buffer, in bytes. + * @param bufferSizeMs The size of the sink's buffer, in milliseconds, if it is configured for + * PCM output. {@link C#TIME_UNSET} if it is configured for encoded audio output, as the + * buffered media can have a variable bitrate so the duration may be unknown. + * @param elapsedSinceLastFeedMs The time since the sink was last fed data, in milliseconds. + */ + void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); + + } + + /** + * Thrown when a failure occurs configuring the sink. + */ + final class ConfigurationException extends Exception { + + /** + * Creates a new configuration exception with the specified {@code cause} and no message. + */ + public ConfigurationException(Throwable cause) { + super(cause); + } + + /** + * Creates a new configuration exception with the specified {@code message} and no cause. + */ + public ConfigurationException(String message) { + super(message); + } + + } + + /** + * Thrown when a failure occurs initializing the sink. + */ + final class InitializationException extends Exception { + + /** + * The underlying {@link AudioTrack}'s state, if applicable. + */ + public final int audioTrackState; + + /** + * @param audioTrackState The underlying {@link AudioTrack}'s state, if applicable. + * @param sampleRate The requested sample rate in Hz. + * @param channelConfig The requested channel configuration. + * @param bufferSize The requested buffer size in bytes. + */ + public InitializationException(int audioTrackState, int sampleRate, int channelConfig, + int bufferSize) { + super("AudioTrack init failed: " + audioTrackState + ", Config(" + sampleRate + ", " + + channelConfig + ", " + bufferSize + ")"); + this.audioTrackState = audioTrackState; + } + + } + + /** + * Thrown when a failure occurs writing to the sink. + */ + final class WriteException extends Exception { + + /** + * The error value returned from the sink implementation. If the sink writes to a platform + * {@link AudioTrack}, this will be the error value returned from + * {@link AudioTrack#write(byte[], int, int)} or {@link AudioTrack#write(ByteBuffer, int, int)}. + * Otherwise, the meaning of the error code depends on the sink implementation. + */ + public final int errorCode; + + /** + * @param errorCode The error value returned from the sink implementation. + */ + public WriteException(int errorCode) { + super("AudioTrack write failed: " + errorCode); + this.errorCode = errorCode; + } + + } + + /** + * Returned by {@link #getCurrentPositionUs(boolean)} when the position is not set. + */ + long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE; + + /** + * Sets the listener for sink events, which should be the audio renderer. + * + * @param listener The listener for sink events, which should be the audio renderer. + */ + void setListener(Listener listener); + + /** + * Returns whether the sink supports the audio format. + * + * @param channelCount The number of channels, or {@link Format#NO_VALUE} if not known. + * @param encoding The audio encoding, or {@link Format#NO_VALUE} if not known. + * @return Whether the sink supports the audio format. + */ + boolean supportsOutput(int channelCount, @C.Encoding int encoding); + + /** + * Returns the playback position in the stream starting at zero, in microseconds, or + * {@link #CURRENT_POSITION_NOT_SET} if it is not yet available. + * + * @param sourceEnded Specify {@code true} if no more input buffers will be provided. + * @return The playback position relative to the start of playback, in microseconds. + */ + long getCurrentPositionUs(boolean sourceEnded); + + /** + * Configures (or reconfigures) the sink. + * + * @param inputEncoding The encoding of audio data provided in the input buffers. + * @param inputChannelCount The number of channels. + * @param inputSampleRate The sample rate in Hz. + * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a + * suitable buffer size. + * @param outputChannels A mapping from input to output channels that is applied to this sink's + * input as a preprocessing step, if handling PCM input. Specify {@code null} to leave the + * input unchanged. Otherwise, the element at index {@code i} specifies index of the input + * channel to map to output channel {@code i} when preprocessing input buffers. After the map + * is applied the audio data will have {@code outputChannels.length} channels. + * @param trimStartFrames The number of audio frames to trim from the start of data written to the + * sink after this call. + * @param trimEndFrames The number of audio frames to trim from data written to the sink + * immediately preceding the next call to {@link #flush()} or this method. + * @throws ConfigurationException If an error occurs configuring the sink. + */ + void configure( + @C.Encoding int inputEncoding, + int inputChannelCount, + int inputSampleRate, + int specifiedBufferSize, + @Nullable int[] outputChannels, + int trimStartFrames, + int trimEndFrames) + throws ConfigurationException; + + /** + * Starts or resumes consuming audio if initialized. + */ + void play(); + + /** Signals to the sink that the next buffer may be discontinuous with the previous buffer. */ + void handleDiscontinuity(); + + /** + * Attempts to process data from a {@link ByteBuffer}, starting from its current position and + * ending at its limit (exclusive). The position of the {@link ByteBuffer} is advanced by the + * number of bytes that were handled. {@link Listener#onPositionDiscontinuity()} will be called if + * {@code presentationTimeUs} is discontinuous with the last buffer handled since the last reset. + * + *

Returns whether the data was handled in full. If the data was not handled in full then the + * same {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed, + * except in the case of an intervening call to {@link #flush()} (or to {@link #configure(int, + * int, int, int, int[], int, int)} that causes the sink to be flushed). + * + * @param buffer The buffer containing audio data. + * @param presentationTimeUs The presentation timestamp of the buffer in microseconds. + * @return Whether the buffer was handled fully. + * @throws InitializationException If an error occurs initializing the sink. + * @throws WriteException If an error occurs writing the audio data. + */ + boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) + throws InitializationException, WriteException; + + /** + * Processes any remaining data. {@link #isEnded()} will return {@code true} when no data remains. + * + * @throws WriteException If an error occurs draining data to the sink. + */ + void playToEndOfStream() throws WriteException; + + /** + * Returns whether {@link #playToEndOfStream} has been called and all buffers have been processed. + */ + boolean isEnded(); + + /** + * Returns whether the sink has data pending that has not been consumed yet. + */ + boolean hasPendingData(); + + /** + * Attempts to set the playback parameters. The audio sink may override these parameters if they + * are not supported. + * + * @param playbackParameters The new playback parameters to attempt to set. + */ + void setPlaybackParameters(PlaybackParameters playbackParameters); + + /** + * Gets the active {@link PlaybackParameters}. + */ + PlaybackParameters getPlaybackParameters(); + + /** + * Sets attributes for audio playback. If the attributes have changed and if the sink is not + * configured for use with tunneling, then it is reset and the audio session id is cleared. + *

+ * If the sink is configured for use with tunneling then the audio attributes are ignored. The + * sink is not reset and the audio session id is not cleared. The passed attributes will be used + * if the sink is later re-configured into non-tunneled mode. + * + * @param audioAttributes The attributes for audio playback. + */ + void setAudioAttributes(AudioAttributes audioAttributes); + + /** Sets the audio session id. */ + void setAudioSessionId(int audioSessionId); + + /** Sets the auxiliary effect. */ + void setAuxEffectInfo(AuxEffectInfo auxEffectInfo); + + /** + * Enables tunneling, if possible. The sink is reset if tunneling was previously disabled or if + * the audio session id has changed. Enabling tunneling is only possible if the sink is based on a + * platform {@link AudioTrack}, and requires platform API version 21 onwards. + * + * @param tunnelingAudioSessionId The audio session id to use. + * @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21. + */ + void enableTunnelingV21(int tunnelingAudioSessionId); + + /** + * Disables tunneling. If tunneling was previously enabled then the sink is reset and any audio + * session id is cleared. + */ + void disableTunneling(); + + /** + * Sets the playback volume. + * + * @param volume A volume in the range [0.0, 1.0]. + */ + void setVolume(float volume); + + /** + * Pauses playback. + */ + void pause(); + + /** + * Flushes the sink, after which it is ready to receive buffers from a new playback position. + * + *

The audio session may remain active until {@link #reset()} is called. + */ + void flush(); + + /** Resets the renderer, releasing any resources that it currently holds. */ + void reset(); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTimestampPoller.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTimestampPoller.java new file mode 100644 index 000000000000..153947fec07a --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTimestampPoller.java @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +import android.annotation.TargetApi; +import android.media.AudioTimestamp; +import android.media.AudioTrack; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Polls the {@link AudioTrack} timestamp, if the platform supports it, taking care of polling at + * the appropriate rate to detect when the timestamp starts to advance. + * + *

When the audio track isn't paused, call {@link #maybePollTimestamp(long)} regularly to check + * for timestamp updates. If it returns {@code true}, call {@link #getTimestampPositionFrames()} and + * {@link #getTimestampSystemTimeUs()} to access the updated timestamp, then call {@link + * #acceptTimestamp()} or {@link #rejectTimestamp()} to accept or reject it. + * + *

If {@link #hasTimestamp()} returns {@code true}, call {@link #getTimestampSystemTimeUs()} to + * get the system time at which the latest timestamp was sampled and {@link + * #getTimestampPositionFrames()} to get its position in frames. If {@link #isTimestampAdvancing()} + * returns {@code true}, the caller should assume that the timestamp has been increasing in real + * time since it was sampled. Otherwise, it may be stationary. + * + *

Call {@link #reset()} when pausing or resuming the track. + */ +/* package */ final class AudioTimestampPoller { + + /** Timestamp polling states. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + STATE_INITIALIZING, + STATE_TIMESTAMP, + STATE_TIMESTAMP_ADVANCING, + STATE_NO_TIMESTAMP, + STATE_ERROR + }) + private @interface State {} + /** State when first initializing. */ + private static final int STATE_INITIALIZING = 0; + /** State when we have a timestamp and we don't know if it's advancing. */ + private static final int STATE_TIMESTAMP = 1; + /** State when we have a timestamp and we know it is advancing. */ + private static final int STATE_TIMESTAMP_ADVANCING = 2; + /** State when the no timestamp is available. */ + private static final int STATE_NO_TIMESTAMP = 3; + /** State when the last timestamp was rejected as invalid. */ + private static final int STATE_ERROR = 4; + + /** The polling interval for {@link #STATE_INITIALIZING} and {@link #STATE_TIMESTAMP}. */ + private static final int FAST_POLL_INTERVAL_US = 5_000; + /** + * The polling interval for {@link #STATE_TIMESTAMP_ADVANCING} and {@link #STATE_NO_TIMESTAMP}. + */ + private static final int SLOW_POLL_INTERVAL_US = 10_000_000; + /** The polling interval for {@link #STATE_ERROR}. */ + private static final int ERROR_POLL_INTERVAL_US = 500_000; + + /** + * The minimum duration to remain in {@link #STATE_INITIALIZING} if no timestamps are being + * returned before transitioning to {@link #STATE_NO_TIMESTAMP}. + */ + private static final int INITIALIZING_DURATION_US = 500_000; + + @Nullable private final AudioTimestampV19 audioTimestamp; + + private @State int state; + private long initializeSystemTimeUs; + private long sampleIntervalUs; + private long lastTimestampSampleTimeUs; + private long initialTimestampPositionFrames; + + /** + * Creates a new audio timestamp poller. + * + * @param audioTrack The audio track that will provide timestamps, if the platform supports it. + */ + public AudioTimestampPoller(AudioTrack audioTrack) { + if (Util.SDK_INT >= 19) { + audioTimestamp = new AudioTimestampV19(audioTrack); + reset(); + } else { + audioTimestamp = null; + updateState(STATE_NO_TIMESTAMP); + } + } + + /** + * Polls the timestamp if required and returns whether it was updated. If {@code true}, the latest + * timestamp is available via {@link #getTimestampSystemTimeUs()} and {@link + * #getTimestampPositionFrames()}, and the caller should call {@link #acceptTimestamp()} if the + * timestamp was valid, or {@link #rejectTimestamp()} otherwise. The values returned by {@link + * #hasTimestamp()} and {@link #isTimestampAdvancing()} may be updated. + * + * @param systemTimeUs The current system time, in microseconds. + * @return Whether the timestamp was updated. + */ + public boolean maybePollTimestamp(long systemTimeUs) { + if (audioTimestamp == null || (systemTimeUs - lastTimestampSampleTimeUs) < sampleIntervalUs) { + return false; + } + lastTimestampSampleTimeUs = systemTimeUs; + boolean updatedTimestamp = audioTimestamp.maybeUpdateTimestamp(); + switch (state) { + case STATE_INITIALIZING: + if (updatedTimestamp) { + if (audioTimestamp.getTimestampSystemTimeUs() >= initializeSystemTimeUs) { + // We have an initial timestamp, but don't know if it's advancing yet. + initialTimestampPositionFrames = audioTimestamp.getTimestampPositionFrames(); + updateState(STATE_TIMESTAMP); + } else { + // Drop the timestamp, as it was sampled before the last reset. + updatedTimestamp = false; + } + } else if (systemTimeUs - initializeSystemTimeUs > INITIALIZING_DURATION_US) { + // We haven't received a timestamp for a while, so they probably aren't available for the + // current audio route. Poll infrequently in case the route changes later. + // TODO: Ideally we should listen for audio route changes in order to detect when a + // timestamp becomes available again. + updateState(STATE_NO_TIMESTAMP); + } + break; + case STATE_TIMESTAMP: + if (updatedTimestamp) { + long timestampPositionFrames = audioTimestamp.getTimestampPositionFrames(); + if (timestampPositionFrames > initialTimestampPositionFrames) { + updateState(STATE_TIMESTAMP_ADVANCING); + } + } else { + reset(); + } + break; + case STATE_TIMESTAMP_ADVANCING: + if (!updatedTimestamp) { + // The audio route may have changed, so reset polling. + reset(); + } + break; + case STATE_NO_TIMESTAMP: + if (updatedTimestamp) { + // The audio route may have changed, so reset polling. + reset(); + } + break; + case STATE_ERROR: + // Do nothing. If the caller accepts any new timestamp we'll reset polling. + break; + default: + throw new IllegalStateException(); + } + return updatedTimestamp; + } + + /** + * Rejects the timestamp last polled in {@link #maybePollTimestamp(long)}. The instance will enter + * the error state and poll timestamps infrequently until the next call to {@link + * #acceptTimestamp()}. + */ + public void rejectTimestamp() { + updateState(STATE_ERROR); + } + + /** + * Accepts the timestamp last polled in {@link #maybePollTimestamp(long)}. If the instance is in + * the error state, it will begin to poll timestamps frequently again. + */ + public void acceptTimestamp() { + if (state == STATE_ERROR) { + reset(); + } + } + + /** + * Returns whether this instance has a timestamp that can be used to calculate the audio track + * position. If {@code true}, call {@link #getTimestampSystemTimeUs()} and {@link + * #getTimestampSystemTimeUs()} to access the timestamp. + */ + public boolean hasTimestamp() { + return state == STATE_TIMESTAMP || state == STATE_TIMESTAMP_ADVANCING; + } + + /** + * Returns whether the timestamp appears to be advancing. If {@code true}, call {@link + * #getTimestampSystemTimeUs()} and {@link #getTimestampSystemTimeUs()} to access the timestamp. A + * current position for the track can be extrapolated based on elapsed real time since the system + * time at which the timestamp was sampled. + */ + public boolean isTimestampAdvancing() { + return state == STATE_TIMESTAMP_ADVANCING; + } + + /** Resets polling. Should be called whenever the audio track is paused or resumed. */ + public void reset() { + if (audioTimestamp != null) { + updateState(STATE_INITIALIZING); + } + } + + /** + * If {@link #maybePollTimestamp(long)} or {@link #hasTimestamp()} returned {@code true}, returns + * the system time at which the latest timestamp was sampled, in microseconds. + */ + public long getTimestampSystemTimeUs() { + return audioTimestamp != null ? audioTimestamp.getTimestampSystemTimeUs() : C.TIME_UNSET; + } + + /** + * If {@link #maybePollTimestamp(long)} or {@link #hasTimestamp()} returned {@code true}, returns + * the latest timestamp's position in frames. + */ + public long getTimestampPositionFrames() { + return audioTimestamp != null ? audioTimestamp.getTimestampPositionFrames() : C.POSITION_UNSET; + } + + private void updateState(@State int state) { + this.state = state; + switch (state) { + case STATE_INITIALIZING: + // Force polling a timestamp immediately, and poll quickly. + lastTimestampSampleTimeUs = 0; + initialTimestampPositionFrames = C.POSITION_UNSET; + initializeSystemTimeUs = System.nanoTime() / 1000; + sampleIntervalUs = FAST_POLL_INTERVAL_US; + break; + case STATE_TIMESTAMP: + sampleIntervalUs = FAST_POLL_INTERVAL_US; + break; + case STATE_TIMESTAMP_ADVANCING: + case STATE_NO_TIMESTAMP: + sampleIntervalUs = SLOW_POLL_INTERVAL_US; + break; + case STATE_ERROR: + sampleIntervalUs = ERROR_POLL_INTERVAL_US; + break; + default: + throw new IllegalStateException(); + } + } + + @TargetApi(19) + private static final class AudioTimestampV19 { + + private final AudioTrack audioTrack; + private final AudioTimestamp audioTimestamp; + + private long rawTimestampFramePositionWrapCount; + private long lastTimestampRawPositionFrames; + private long lastTimestampPositionFrames; + + /** + * Creates a new {@link AudioTimestamp} wrapper. + * + * @param audioTrack The audio track that will provide timestamps. + */ + public AudioTimestampV19(AudioTrack audioTrack) { + this.audioTrack = audioTrack; + audioTimestamp = new AudioTimestamp(); + } + + /** + * Attempts to update the audio track timestamp. Returns {@code true} if the timestamp was + * updated, in which case the updated timestamp system time and position can be accessed with + * {@link #getTimestampSystemTimeUs()} and {@link #getTimestampPositionFrames()}. Returns {@code + * false} if no timestamp is available, in which case those methods should not be called. + */ + public boolean maybeUpdateTimestamp() { + boolean updated = audioTrack.getTimestamp(audioTimestamp); + if (updated) { + long rawPositionFrames = audioTimestamp.framePosition; + if (lastTimestampRawPositionFrames > rawPositionFrames) { + // The value must have wrapped around. + rawTimestampFramePositionWrapCount++; + } + lastTimestampRawPositionFrames = rawPositionFrames; + lastTimestampPositionFrames = + rawPositionFrames + (rawTimestampFramePositionWrapCount << 32); + } + return updated; + } + + public long getTimestampSystemTimeUs() { + return audioTimestamp.nanoTime / 1000; + } + + public long getTimestampPositionFrames() { + return lastTimestampPositionFrames; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTrack.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTrack.java deleted file mode 100644 index b1a5b8e17602..000000000000 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTrack.java +++ /dev/null @@ -1,1732 +0,0 @@ -/* - * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.media.AudioAttributes; -import android.media.AudioFormat; -import android.media.AudioTimestamp; -import android.os.ConditionVariable; -import android.os.SystemClock; -import android.util.Log; -import org.mozilla.thirdparty.com.google.android.exoplayer2.C; -import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; -import java.lang.reflect.Method; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.ArrayList; -import java.util.LinkedList; - -/** - * Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles - * playback position smoothing, non-blocking writes and reconfiguration. - *

- * Before starting playback, specify the input format by calling - * {@link #configure(String, int, int, int, int)}. Optionally call {@link #setAudioSessionId(int)}, - * {@link #setStreamType(int)}, {@link #enableTunnelingV21(int)} and {@link #disableTunneling()} - * to configure audio playback. These methods may be called after writing data to the track, in - * which case it will be reinitialized as required. - *

- * Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} - * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data. - *

- * Call {@link #configure(String, int, int, int, int)} whenever the input format changes. The track - * will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, long)}. - *

- * Calling {@link #reset()} releases the underlying {@link android.media.AudioTrack} (and so does - * calling {@link #configure(String, int, int, int, int)} unless the format is unchanged). It is - * safe to call {@link #handleBuffer(ByteBuffer, long)} after {@link #reset()} without calling - * {@link #configure(String, int, int, int, int)}. - *

- * Call {@link #playToEndOfStream()} repeatedly to play out all data when no more input buffers will - * be provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #reset}. Call - * {@link #release()} when the instance is no longer required. - */ -public final class AudioTrack { - - /** - * Listener for audio track events. - */ - public interface Listener { - - /** - * Called when the audio track has been initialized with a newly generated audio session id. - * - * @param audioSessionId The newly generated audio session id. - */ - void onAudioSessionId(int audioSessionId); - - /** - * Called when the audio track handles a buffer whose timestamp is discontinuous with the last - * buffer handled since it was reset. - */ - void onPositionDiscontinuity(); - - /** - * Called when the audio track underruns. - * - * @param bufferSize The size of the track's buffer, in bytes. - * @param bufferSizeMs The size of the track's buffer, in milliseconds, if it is configured for - * PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output, as the - * buffered media can have a variable bitrate so the duration may be unknown. - * @param elapsedSinceLastFeedMs The time since the track was last fed data, in milliseconds. - */ - void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); - - } - - /** - * Thrown when a failure occurs configuring the track. - */ - public static final class ConfigurationException extends Exception { - - public ConfigurationException(Throwable cause) { - super(cause); - } - - public ConfigurationException(String message) { - super(message); - } - - } - - /** - * Thrown when a failure occurs initializing an {@link android.media.AudioTrack}. - */ - public static final class InitializationException extends Exception { - - /** - * The state as reported by {@link android.media.AudioTrack#getState()}. - */ - public final int audioTrackState; - - /** - * @param audioTrackState The state as reported by {@link android.media.AudioTrack#getState()}. - * @param sampleRate The requested sample rate in Hz. - * @param channelConfig The requested channel configuration. - * @param bufferSize The requested buffer size in bytes. - */ - public InitializationException(int audioTrackState, int sampleRate, int channelConfig, - int bufferSize) { - super("AudioTrack init failed: " + audioTrackState + ", Config(" + sampleRate + ", " - + channelConfig + ", " + bufferSize + ")"); - this.audioTrackState = audioTrackState; - } - - } - - /** - * Thrown when a failure occurs writing to an {@link android.media.AudioTrack}. - */ - public static final class WriteException extends Exception { - - /** - * The error value returned from {@link android.media.AudioTrack#write(byte[], int, int)} or - * {@link android.media.AudioTrack#write(ByteBuffer, int, int)}. - */ - public final int errorCode; - - /** - * @param errorCode The error value returned from - * {@link android.media.AudioTrack#write(byte[], int, int)} or - * {@link android.media.AudioTrack#write(ByteBuffer, int, int)}. - */ - public WriteException(int errorCode) { - super("AudioTrack write failed: " + errorCode); - this.errorCode = errorCode; - } - - } - - /** - * Thrown when {@link android.media.AudioTrack#getTimestamp} returns a spurious timestamp, if - * {@code AudioTrack#failOnSpuriousAudioTimestamp} is set. - */ - public static final class InvalidAudioTrackTimestampException extends RuntimeException { - - /** - * @param detailMessage The detail message for this exception. - */ - public InvalidAudioTrackTimestampException(String detailMessage) { - super(detailMessage); - } - - } - - /** - * Returned by {@link #getCurrentPositionUs(boolean)} when the position is not set. - */ - public static final long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE; - - /** - * A minimum length for the {@link android.media.AudioTrack} buffer, in microseconds. - */ - private static final long MIN_BUFFER_DURATION_US = 250000; - /** - * A maximum length for the {@link android.media.AudioTrack} buffer, in microseconds. - */ - private static final long MAX_BUFFER_DURATION_US = 750000; - /** - * The length for passthrough {@link android.media.AudioTrack} buffers, in microseconds. - */ - private static final long PASSTHROUGH_BUFFER_DURATION_US = 250000; - /** - * A multiplication factor to apply to the minimum buffer size requested by the underlying - * {@link android.media.AudioTrack}. - */ - private static final int BUFFER_MULTIPLICATION_FACTOR = 4; - - /** - * @see android.media.AudioTrack#PLAYSTATE_STOPPED - */ - private static final int PLAYSTATE_STOPPED = android.media.AudioTrack.PLAYSTATE_STOPPED; - /** - * @see android.media.AudioTrack#PLAYSTATE_PAUSED - */ - private static final int PLAYSTATE_PAUSED = android.media.AudioTrack.PLAYSTATE_PAUSED; - /** - * @see android.media.AudioTrack#PLAYSTATE_PLAYING - */ - private static final int PLAYSTATE_PLAYING = android.media.AudioTrack.PLAYSTATE_PLAYING; - /** - * @see android.media.AudioTrack#ERROR_BAD_VALUE - */ - private static final int ERROR_BAD_VALUE = android.media.AudioTrack.ERROR_BAD_VALUE; - /** - * @see android.media.AudioTrack#MODE_STATIC - */ - private static final int MODE_STATIC = android.media.AudioTrack.MODE_STATIC; - /** - * @see android.media.AudioTrack#MODE_STREAM - */ - private static final int MODE_STREAM = android.media.AudioTrack.MODE_STREAM; - /** - * @see android.media.AudioTrack#STATE_INITIALIZED - */ - private static final int STATE_INITIALIZED = android.media.AudioTrack.STATE_INITIALIZED; - /** - * @see android.media.AudioTrack#WRITE_NON_BLOCKING - */ - @SuppressLint("InlinedApi") - private static final int WRITE_NON_BLOCKING = android.media.AudioTrack.WRITE_NON_BLOCKING; - - private static final String TAG = "AudioTrack"; - - /** - * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more - * than this amount. - *

- * This is a fail safe that should not be required on correctly functioning devices. - */ - private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND; - - /** - * AudioTrack latencies are deemed impossibly large if they are greater than this amount. - *

- * This is a fail safe that should not be required on correctly functioning devices. - */ - private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND; - - private static final int START_NOT_SET = 0; - private static final int START_IN_SYNC = 1; - private static final int START_NEED_SYNC = 2; - - private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10; - private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; - private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000; - - /** - * The minimum number of output bytes from {@link #sonicAudioProcessor} at which the speedup is - * calculated using the input/output byte counts from the processor, rather than using the - * current playback parameters speed. - */ - private static final int SONIC_MIN_BYTES_FOR_SPEEDUP = 1024; - - /** - * Whether to enable a workaround for an issue where an audio effect does not keep its session - * active across releasing/initializing a new audio track, on platform builds where - * {@link Util#SDK_INT} < 21. - *

- * The flag must be set before creating a player. - */ - public static boolean enablePreV21AudioSessionWorkaround = false; - - /** - * Whether to throw an {@link InvalidAudioTrackTimestampException} when a spurious timestamp is - * reported from {@link android.media.AudioTrack#getTimestamp}. - *

- * The flag must be set before creating a player. Should be set to {@code true} for testing and - * debugging purposes only. - */ - public static boolean failOnSpuriousAudioTimestamp = false; - - private final AudioCapabilities audioCapabilities; - private final ChannelMappingAudioProcessor channelMappingAudioProcessor; - private final SonicAudioProcessor sonicAudioProcessor; - private final AudioProcessor[] availableAudioProcessors; - private final Listener listener; - private final ConditionVariable releasingConditionVariable; - private final long[] playheadOffsets; - private final AudioTrackUtil audioTrackUtil; - private final LinkedList playbackParametersCheckpoints; - - /** - * Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}). - */ - private android.media.AudioTrack keepSessionIdAudioTrack; - - private android.media.AudioTrack audioTrack; - private int sampleRate; - private int channelConfig; - @C.Encoding - private int encoding; - @C.Encoding - private int outputEncoding; - @C.StreamType - private int streamType; - private boolean passthrough; - private int bufferSize; - private long bufferSizeUs; - - private PlaybackParameters drainingPlaybackParameters; - private PlaybackParameters playbackParameters; - private long playbackParametersOffsetUs; - private long playbackParametersPositionUs; - - private ByteBuffer avSyncHeader; - private int bytesUntilNextAvSync; - - private int nextPlayheadOffsetIndex; - private int playheadOffsetCount; - private long smoothedPlayheadOffsetUs; - private long lastPlayheadSampleTimeUs; - private boolean audioTimestampSet; - private long lastTimestampSampleTimeUs; - - private Method getLatencyMethod; - private int pcmFrameSize; - private long submittedPcmBytes; - private long submittedEncodedFrames; - private int outputPcmFrameSize; - private long writtenPcmBytes; - private long writtenEncodedFrames; - private int framesPerEncodedSample; - private int startMediaTimeState; - private long startMediaTimeUs; - private long resumeSystemTimeUs; - private long latencyUs; - private float volume; - - private AudioProcessor[] audioProcessors; - private ByteBuffer[] outputBuffers; - private ByteBuffer inputBuffer; - private ByteBuffer outputBuffer; - private byte[] preV21OutputBuffer; - private int preV21OutputBufferOffset; - private int drainingAudioProcessorIndex; - private boolean handledEndOfStream; - - private boolean playing; - private int audioSessionId; - private boolean tunneling; - private boolean hasData; - private long lastFeedElapsedRealtimeMs; - - /** - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. - * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before - * output. May be empty. - * @param listener Listener for audio track events. - */ - public AudioTrack(AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors, - Listener listener) { - this.audioCapabilities = audioCapabilities; - this.listener = listener; - releasingConditionVariable = new ConditionVariable(true); - if (Util.SDK_INT >= 18) { - try { - getLatencyMethod = - android.media.AudioTrack.class.getMethod("getLatency", (Class[]) null); - } catch (NoSuchMethodException e) { - // There's no guarantee this method exists. Do nothing. - } - } - if (Util.SDK_INT >= 19) { - audioTrackUtil = new AudioTrackUtilV19(); - } else { - audioTrackUtil = new AudioTrackUtil(); - } - channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); - sonicAudioProcessor = new SonicAudioProcessor(); - availableAudioProcessors = new AudioProcessor[3 + audioProcessors.length]; - availableAudioProcessors[0] = new ResamplingAudioProcessor(); - availableAudioProcessors[1] = channelMappingAudioProcessor; - System.arraycopy(audioProcessors, 0, availableAudioProcessors, 2, audioProcessors.length); - availableAudioProcessors[2 + audioProcessors.length] = sonicAudioProcessor; - playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; - volume = 1.0f; - startMediaTimeState = START_NOT_SET; - streamType = C.STREAM_TYPE_DEFAULT; - audioSessionId = C.AUDIO_SESSION_ID_UNSET; - playbackParameters = PlaybackParameters.DEFAULT; - drainingAudioProcessorIndex = C.INDEX_UNSET; - this.audioProcessors = new AudioProcessor[0]; - outputBuffers = new ByteBuffer[0]; - playbackParametersCheckpoints = new LinkedList<>(); - } - - /** - * Returns whether it's possible to play audio in the specified format using encoded passthrough. - * - * @param mimeType The format mime type. - * @return Whether it's possible to play audio in the format using encoded passthrough. - */ - public boolean isPassthroughSupported(String mimeType) { - return audioCapabilities != null - && audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType)); - } - - /** - * Returns the playback position in the stream starting at zero, in microseconds, or - * {@link #CURRENT_POSITION_NOT_SET} if it is not yet available. - * - *

If the device supports it, the method uses the playback timestamp from - * {@link android.media.AudioTrack#getTimestamp}. Otherwise, it derives a smoothed position by - * sampling the {@link android.media.AudioTrack}'s frame position. - * - * @param sourceEnded Specify {@code true} if no more input buffers will be provided. - * @return The playback position relative to the start of playback, in microseconds. - */ - public long getCurrentPositionUs(boolean sourceEnded) { - if (!hasCurrentPositionUs()) { - return CURRENT_POSITION_NOT_SET; - } - - if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) { - maybeSampleSyncParams(); - } - - long systemClockUs = System.nanoTime() / 1000; - long positionUs; - if (audioTimestampSet) { - // Calculate the speed-adjusted position using the timestamp (which may be in the future). - long elapsedSinceTimestampUs = systemClockUs - (audioTrackUtil.getTimestampNanoTime() / 1000); - long elapsedSinceTimestampFrames = durationUsToFrames(elapsedSinceTimestampUs); - long elapsedFrames = audioTrackUtil.getTimestampFramePosition() + elapsedSinceTimestampFrames; - positionUs = framesToDurationUs(elapsedFrames); - } else { - if (playheadOffsetCount == 0) { - // The AudioTrack has started, but we don't have any samples to compute a smoothed position. - positionUs = audioTrackUtil.getPositionUs(); - } else { - // getPlayheadPositionUs() only has a granularity of ~20 ms, so we base the position off the - // system clock (and a smoothed offset between it and the playhead position) so as to - // prevent jitter in the reported positions. - positionUs = systemClockUs + smoothedPlayheadOffsetUs; - } - if (!sourceEnded) { - positionUs -= latencyUs; - } - } - - return startMediaTimeUs + applySpeedup(positionUs); - } - - /** - * Configures (or reconfigures) the audio track. - * - * @param mimeType The mime type. - * @param channelCount The number of channels. - * @param sampleRate The sample rate in Hz. - * @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT}, - * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and - * {@link C#ENCODING_PCM_32BIT}. - * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a - * suitable buffer size automatically. - * @throws ConfigurationException If an error occurs configuring the track. - */ - public void configure(String mimeType, int channelCount, int sampleRate, - @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) throws ConfigurationException { - configure(mimeType, channelCount, sampleRate, pcmEncoding, specifiedBufferSize, null); - } - - /** - * Configures (or reconfigures) the audio track. - * - * @param mimeType The mime type. - * @param channelCount The number of channels. - * @param sampleRate The sample rate in Hz. - * @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT}, - * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and - * {@link C#ENCODING_PCM_32BIT}. - * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a - * suitable buffer size automatically. - * @param outputChannels A mapping from input to output channels that is applied to this track's - * input as a preprocessing step, if handling PCM input. Specify {@code null} to leave the - * input unchanged. Otherwise, the element at index {@code i} specifies index of the input - * channel to map to output channel {@code i} when preprocessing input buffers. After the - * map is applied the audio data will have {@code outputChannels.length} channels. - * @throws ConfigurationException If an error occurs configuring the track. - */ - public void configure(String mimeType, int channelCount, int sampleRate, - @C.PcmEncoding int pcmEncoding, int specifiedBufferSize, int[] outputChannels) - throws ConfigurationException { - boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); - @C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding; - boolean flush = false; - if (!passthrough) { - pcmFrameSize = Util.getPcmFrameSize(pcmEncoding, channelCount); - channelMappingAudioProcessor.setChannelMap(outputChannels); - for (AudioProcessor audioProcessor : availableAudioProcessors) { - try { - flush |= audioProcessor.configure(sampleRate, channelCount, encoding); - } catch (AudioProcessor.UnhandledFormatException e) { - throw new ConfigurationException(e); - } - if (audioProcessor.isActive()) { - channelCount = audioProcessor.getOutputChannelCount(); - encoding = audioProcessor.getOutputEncoding(); - } - } - if (flush) { - resetAudioProcessors(); - } - } - - int channelConfig; - switch (channelCount) { - case 1: - channelConfig = AudioFormat.CHANNEL_OUT_MONO; - break; - case 2: - channelConfig = AudioFormat.CHANNEL_OUT_STEREO; - break; - case 3: - channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER; - break; - case 4: - channelConfig = AudioFormat.CHANNEL_OUT_QUAD; - break; - case 5: - channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER; - break; - case 6: - channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; - break; - case 7: - channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER; - break; - case 8: - channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; - break; - default: - throw new ConfigurationException("Unsupported channel count: " + channelCount); - } - - // Workaround for overly strict channel configuration checks on nVidia Shield. - if (Util.SDK_INT <= 23 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER)) { - switch (channelCount) { - case 7: - channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; - break; - case 3: - case 5: - channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; - break; - default: - break; - } - } - - // Workaround for Nexus Player not reporting support for mono passthrough. - // (See [Internal: b/34268671].) - if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) { - channelConfig = AudioFormat.CHANNEL_OUT_STEREO; - } - - if (!flush && isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate - && this.channelConfig == channelConfig) { - // We already have an audio track with the correct sample rate, channel config and encoding. - return; - } - - reset(); - - this.encoding = encoding; - this.passthrough = passthrough; - this.sampleRate = sampleRate; - this.channelConfig = channelConfig; - outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT; - outputPcmFrameSize = Util.getPcmFrameSize(C.ENCODING_PCM_16BIT, channelCount); - - if (specifiedBufferSize != 0) { - bufferSize = specifiedBufferSize; - } else if (passthrough) { - // TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into - // account. [Internal: b/25181305] - if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) { - // AC-3 allows bitrates up to 640 kbit/s. - bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND); - } else /* (outputEncoding == C.ENCODING_DTS || outputEncoding == C.ENCODING_DTS_HD */ { - // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. - bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND); - } - } else { - int minBufferSize = - android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding); - Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); - int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; - int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize; - int maxAppBufferSize = (int) Math.max(minBufferSize, - durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize); - bufferSize = multipliedBufferSize < minAppBufferSize ? minAppBufferSize - : multipliedBufferSize > maxAppBufferSize ? maxAppBufferSize - : multipliedBufferSize; - } - bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(bufferSize / outputPcmFrameSize); - - // The old playback parameters may no longer be applicable so try to reset them now. - setPlaybackParameters(playbackParameters); - } - - private void resetAudioProcessors() { - ArrayList newAudioProcessors = new ArrayList<>(); - for (AudioProcessor audioProcessor : availableAudioProcessors) { - if (audioProcessor.isActive()) { - newAudioProcessors.add(audioProcessor); - } else { - audioProcessor.flush(); - } - } - int count = newAudioProcessors.size(); - audioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]); - outputBuffers = new ByteBuffer[count]; - for (int i = 0; i < count; i++) { - AudioProcessor audioProcessor = audioProcessors[i]; - audioProcessor.flush(); - outputBuffers[i] = audioProcessor.getOutput(); - } - } - - private void initialize() throws InitializationException { - // If we're asynchronously releasing a previous audio track then we block 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 (tunneling) { - audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, outputEncoding, - bufferSize, audioSessionId); - } else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { - audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - outputEncoding, bufferSize, MODE_STREAM); - } else { - // Re-attach to the same audio session. - audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - outputEncoding, bufferSize, MODE_STREAM, audioSessionId); - } - checkAudioTrackInitialized(); - - int audioSessionId = audioTrack.getAudioSessionId(); - if (enablePreV21AudioSessionWorkaround) { - if (Util.SDK_INT < 21) { - // The workaround creates an audio track with a two byte buffer on the same session, and - // does not release it until this object is released, which keeps the session active. - if (keepSessionIdAudioTrack != null - && audioSessionId != keepSessionIdAudioTrack.getAudioSessionId()) { - releaseKeepSessionIdAudioTrack(); - } - if (keepSessionIdAudioTrack == null) { - int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE. - int channelConfig = AudioFormat.CHANNEL_OUT_MONO; - @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT; - int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback. - keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate, - channelConfig, encoding, bufferSize, MODE_STATIC, audioSessionId); - } - } - } - if (this.audioSessionId != audioSessionId) { - this.audioSessionId = audioSessionId; - listener.onAudioSessionId(audioSessionId); - } - - audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds()); - setVolumeInternal(); - hasData = false; - } - - /** - * Starts or resumes playing audio if the audio track has been initialized. - */ - public void play() { - playing = true; - if (isInitialized()) { - resumeSystemTimeUs = System.nanoTime() / 1000; - audioTrack.play(); - } - } - - /** - * Signals to the audio track that the next buffer is discontinuous with the previous buffer. - */ - public void handleDiscontinuity() { - // Force resynchronization after a skipped buffer. - if (startMediaTimeState == START_IN_SYNC) { - startMediaTimeState = START_NEED_SYNC; - } - } - - /** - * Attempts to process data from a {@link ByteBuffer}, starting from its current position and - * ending at its limit (exclusive). The position of the {@link ByteBuffer} is advanced by the - * number of bytes that were handled. {@link Listener#onPositionDiscontinuity()} will be called if - * {@code presentationTimeUs} is discontinuous with the last buffer handled since the last reset. - *

- * Returns whether the data was handled in full. If the data was not handled in full then the same - * {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed, - * except in the case of an interleaving call to {@link #reset()} (or an interleaving call to - * {@link #configure(String, int, int, int, int)} that caused the track to be reset). - * - * @param buffer The buffer containing audio data. - * @param presentationTimeUs The presentation timestamp of the buffer in microseconds. - * @return Whether the buffer was handled fully. - * @throws InitializationException If an error occurs initializing the track. - * @throws WriteException If an error occurs writing the audio data. - */ - @SuppressWarnings("ReferenceEquality") - public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) - throws InitializationException, WriteException { - Assertions.checkArgument(inputBuffer == null || buffer == inputBuffer); - if (!isInitialized()) { - initialize(); - if (playing) { - play(); - } - } - - if (needsPassthroughWorkarounds()) { - // An AC-3 audio track continues to play data written while it is paused. Stop writing so its - // buffer empties. See [Internal: b/18899620]. - if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) { - // We force an underrun to pause the track, so don't notify the listener in this case. - hasData = false; - return false; - } - - // A new AC-3 audio track's playback position continues to increase from the old track's - // position for a short time after is has been released. Avoid writing data until the playback - // head position actually returns to zero. - if (audioTrack.getPlayState() == PLAYSTATE_STOPPED - && audioTrackUtil.getPlaybackHeadPosition() != 0) { - return false; - } - } - - boolean hadData = hasData; - hasData = hasPendingData(); - if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) { - long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; - listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs); - } - - if (inputBuffer == null) { - // We are seeing this buffer for the first time. - if (!buffer.hasRemaining()) { - // The buffer is empty. - return true; - } - - if (passthrough && framesPerEncodedSample == 0) { - // If this is the first encoded sample, calculate the sample size in frames. - framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer); - } - - if (drainingPlaybackParameters != null) { - if (!drainAudioProcessorsToEndOfStream()) { - // Don't process any more input until draining completes. - return false; - } - // Store the position and corresponding media time from which the parameters will apply. - playbackParametersCheckpoints.add(new PlaybackParametersCheckpoint( - drainingPlaybackParameters, Math.max(0, presentationTimeUs), - framesToDurationUs(getWrittenFrames()))); - drainingPlaybackParameters = null; - // The audio processors have drained, so flush them. This will cause any active speed - // adjustment audio processor to start producing audio with the new parameters. - resetAudioProcessors(); - } - - if (startMediaTimeState == START_NOT_SET) { - startMediaTimeUs = Math.max(0, presentationTimeUs); - startMediaTimeState = START_IN_SYNC; - } else { - // Sanity check that presentationTimeUs is consistent with the expected value. - long expectedPresentationTimeUs = startMediaTimeUs - + framesToDurationUs(getSubmittedFrames()); - if (startMediaTimeState == START_IN_SYNC - && Math.abs(expectedPresentationTimeUs - presentationTimeUs) > 200000) { - Log.e(TAG, "Discontinuity detected [expected " + expectedPresentationTimeUs + ", got " - + presentationTimeUs + "]"); - startMediaTimeState = START_NEED_SYNC; - } - if (startMediaTimeState == START_NEED_SYNC) { - // Adjust startMediaTimeUs to be consistent with the current buffer's start time and the - // number of bytes submitted. - startMediaTimeUs += (presentationTimeUs - expectedPresentationTimeUs); - startMediaTimeState = START_IN_SYNC; - listener.onPositionDiscontinuity(); - } - } - - if (passthrough) { - submittedEncodedFrames += framesPerEncodedSample; - } else { - submittedPcmBytes += buffer.remaining(); - } - - inputBuffer = buffer; - } - - if (passthrough) { - // Passthrough buffers are not processed. - writeBuffer(inputBuffer, presentationTimeUs); - } else { - processBuffers(presentationTimeUs); - } - - if (!inputBuffer.hasRemaining()) { - inputBuffer = null; - return true; - } - return false; - } - - private void processBuffers(long avSyncPresentationTimeUs) throws WriteException { - int count = audioProcessors.length; - int index = count; - while (index >= 0) { - ByteBuffer input = index > 0 ? outputBuffers[index - 1] - : (inputBuffer != null ? inputBuffer : AudioProcessor.EMPTY_BUFFER); - if (index == count) { - writeBuffer(input, avSyncPresentationTimeUs); - } else { - AudioProcessor audioProcessor = audioProcessors[index]; - audioProcessor.queueInput(input); - ByteBuffer output = audioProcessor.getOutput(); - outputBuffers[index] = output; - if (output.hasRemaining()) { - // Handle the output as input to the next audio processor or the AudioTrack. - index++; - continue; - } - } - - if (input.hasRemaining()) { - // The input wasn't consumed and no output was produced, so give up for now. - return; - } - - // Get more input from upstream. - index--; - } - } - - @SuppressWarnings("ReferenceEquality") - private boolean writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) - throws WriteException { - if (!buffer.hasRemaining()) { - return true; - } - if (outputBuffer != null) { - Assertions.checkArgument(outputBuffer == buffer); - } else { - outputBuffer = buffer; - if (Util.SDK_INT < 21) { - int bytesRemaining = buffer.remaining(); - if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) { - preV21OutputBuffer = new byte[bytesRemaining]; - } - int originalPosition = buffer.position(); - buffer.get(preV21OutputBuffer, 0, bytesRemaining); - buffer.position(originalPosition); - preV21OutputBufferOffset = 0; - } - } - int bytesRemaining = buffer.remaining(); - int bytesWritten = 0; - if (Util.SDK_INT < 21) { // passthrough == false - // Work out how many bytes we can write without the risk of blocking. - int bytesPending = - (int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize)); - int bytesToWrite = bufferSize - bytesPending; - if (bytesToWrite > 0) { - bytesToWrite = Math.min(bytesRemaining, bytesToWrite); - bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite); - if (bytesWritten > 0) { - preV21OutputBufferOffset += bytesWritten; - buffer.position(buffer.position() + bytesWritten); - } - } - } else if (tunneling) { - Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET); - bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, - avSyncPresentationTimeUs); - } else { - bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining); - } - - lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); - - if (bytesWritten < 0) { - throw new WriteException(bytesWritten); - } - - if (!passthrough) { - writtenPcmBytes += bytesWritten; - } - if (bytesWritten == bytesRemaining) { - if (passthrough) { - writtenEncodedFrames += framesPerEncodedSample; - } - outputBuffer = null; - return true; - } - return false; - } - - /** - * Plays out remaining audio. {@link #isEnded()} will return {@code true} when playback has ended. - * - * @throws WriteException If an error occurs draining data to the track. - */ - public void playToEndOfStream() throws WriteException { - if (handledEndOfStream || !isInitialized()) { - return; - } - - if (drainAudioProcessorsToEndOfStream()) { - // The audio processors have drained, so drain the underlying audio track. - audioTrackUtil.handleEndOfStream(getWrittenFrames()); - bytesUntilNextAvSync = 0; - handledEndOfStream = true; - } - } - - private boolean drainAudioProcessorsToEndOfStream() throws WriteException { - boolean audioProcessorNeedsEndOfStream = false; - if (drainingAudioProcessorIndex == C.INDEX_UNSET) { - drainingAudioProcessorIndex = passthrough ? audioProcessors.length : 0; - audioProcessorNeedsEndOfStream = true; - } - while (drainingAudioProcessorIndex < audioProcessors.length) { - AudioProcessor audioProcessor = audioProcessors[drainingAudioProcessorIndex]; - if (audioProcessorNeedsEndOfStream) { - audioProcessor.queueEndOfStream(); - } - processBuffers(C.TIME_UNSET); - if (!audioProcessor.isEnded()) { - return false; - } - audioProcessorNeedsEndOfStream = true; - drainingAudioProcessorIndex++; - } - - // Finish writing any remaining output to the track. - if (outputBuffer != null) { - writeBuffer(outputBuffer, C.TIME_UNSET); - if (outputBuffer != null) { - return false; - } - } - drainingAudioProcessorIndex = C.INDEX_UNSET; - return true; - } - - /** - * Returns whether all buffers passed to {@link #handleBuffer(ByteBuffer, long)} have been - * completely processed and played. - */ - public boolean isEnded() { - return !isInitialized() || (handledEndOfStream && !hasPendingData()); - } - - /** - * Returns whether the audio track has more data pending that will be played back. - */ - public boolean hasPendingData() { - return isInitialized() - && (getWrittenFrames() > audioTrackUtil.getPlaybackHeadPosition() - || overrideHasPendingData()); - } - - /** - * Attempts to set the playback parameters and returns the active playback parameters, which may - * differ from those passed in. - * - * @param playbackParameters The new playback parameters to attempt to set. - * @return The active playback parameters. - */ - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - if (passthrough) { - // The playback parameters are always the default in passthrough mode. - this.playbackParameters = PlaybackParameters.DEFAULT; - return this.playbackParameters; - } - playbackParameters = new PlaybackParameters( - sonicAudioProcessor.setSpeed(playbackParameters.speed), - sonicAudioProcessor.setPitch(playbackParameters.pitch)); - PlaybackParameters lastSetPlaybackParameters = - drainingPlaybackParameters != null ? drainingPlaybackParameters - : !playbackParametersCheckpoints.isEmpty() - ? playbackParametersCheckpoints.getLast().playbackParameters - : this.playbackParameters; - if (!playbackParameters.equals(lastSetPlaybackParameters)) { - if (isInitialized()) { - // Drain the audio processors so we can determine the frame position at which the new - // parameters apply. - drainingPlaybackParameters = playbackParameters; - } else { - this.playbackParameters = playbackParameters; - } - } - return this.playbackParameters; - } - - /** - * Gets the {@link PlaybackParameters}. - */ - public PlaybackParameters getPlaybackParameters() { - return playbackParameters; - } - - /** - * Sets the stream type for audio track. If the stream type has changed and if the audio track - * is not configured for use with tunneling, then the audio track is reset and the audio session - * id is cleared. - *

- * If the audio track is configured for use with tunneling then the stream type is ignored, the - * audio track is not reset and the audio session id is not cleared. The passed stream type will - * be used if the audio track is later re-configured into non-tunneled mode. - * - * @param streamType The {@link C.StreamType} to use for audio output. - */ - public void setStreamType(@C.StreamType int streamType) { - if (this.streamType == streamType) { - return; - } - this.streamType = streamType; - if (tunneling) { - // The stream type is ignored in tunneling mode, so no need to reset. - return; - } - reset(); - audioSessionId = C.AUDIO_SESSION_ID_UNSET; - } - - /** - * Sets the audio session id. The audio track is reset if the audio session id has changed. - */ - public void setAudioSessionId(int audioSessionId) { - if (this.audioSessionId != audioSessionId) { - this.audioSessionId = audioSessionId; - reset(); - } - } - - /** - * Enables tunneling. The audio track is reset if tunneling was previously disabled or if the - * audio session id has changed. Enabling tunneling requires platform API version 21 onwards. - *

- * If this instance has {@link AudioProcessor}s and tunneling is enabled, care must be taken that - * audio processors do not output buffers with a different duration than their input, and buffer - * processors must produce output corresponding to their last input immediately after that input - * is queued. - * - * @param tunnelingAudioSessionId The audio session id to use. - * @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21. - */ - public void enableTunnelingV21(int tunnelingAudioSessionId) { - Assertions.checkState(Util.SDK_INT >= 21); - if (!tunneling || audioSessionId != tunnelingAudioSessionId) { - tunneling = true; - audioSessionId = tunnelingAudioSessionId; - reset(); - } - } - - /** - * Disables tunneling. If tunneling was previously enabled then the audio track is reset and the - * audio session id is cleared. - */ - public void disableTunneling() { - if (tunneling) { - tunneling = false; - audioSessionId = C.AUDIO_SESSION_ID_UNSET; - reset(); - } - } - - /** - * Sets the playback volume. - * - * @param volume A volume in the range [0.0, 1.0]. - */ - public void setVolume(float volume) { - if (this.volume != volume) { - this.volume = volume; - setVolumeInternal(); - } - } - - private void setVolumeInternal() { - if (!isInitialized()) { - // Do nothing. - } else if (Util.SDK_INT >= 21) { - setVolumeInternalV21(audioTrack, volume); - } else { - setVolumeInternalV3(audioTrack, volume); - } - } - - /** - * Pauses playback. - */ - public void pause() { - playing = false; - if (isInitialized()) { - resetSyncParams(); - audioTrackUtil.pause(); - } - } - - /** - * Releases the underlying audio track asynchronously. - *

- * Calling {@link #handleBuffer(ByteBuffer, long)} will block until the audio track has been - * released, so it is safe to use the audio track immediately after a reset. The audio session may - * remain active until {@link #release()} is called. - */ - public void reset() { - if (isInitialized()) { - submittedPcmBytes = 0; - submittedEncodedFrames = 0; - writtenPcmBytes = 0; - writtenEncodedFrames = 0; - framesPerEncodedSample = 0; - if (drainingPlaybackParameters != null) { - playbackParameters = drainingPlaybackParameters; - drainingPlaybackParameters = null; - } else if (!playbackParametersCheckpoints.isEmpty()) { - playbackParameters = playbackParametersCheckpoints.getLast().playbackParameters; - } - playbackParametersCheckpoints.clear(); - playbackParametersOffsetUs = 0; - playbackParametersPositionUs = 0; - inputBuffer = null; - outputBuffer = null; - for (int i = 0; i < audioProcessors.length; i++) { - AudioProcessor audioProcessor = audioProcessors[i]; - audioProcessor.flush(); - outputBuffers[i] = audioProcessor.getOutput(); - } - handledEndOfStream = false; - drainingAudioProcessorIndex = C.INDEX_UNSET; - avSyncHeader = null; - bytesUntilNextAvSync = 0; - startMediaTimeState = START_NOT_SET; - latencyUs = 0; - resetSyncParams(); - int playState = audioTrack.getPlayState(); - if (playState == PLAYSTATE_PLAYING) { - audioTrack.pause(); - } - // AudioTrack.release can take some time, so we call it on a background thread. - final android.media.AudioTrack toRelease = audioTrack; - audioTrack = null; - audioTrackUtil.reconfigure(null, false); - releasingConditionVariable.close(); - new Thread() { - @Override - public void run() { - try { - toRelease.flush(); - toRelease.release(); - } finally { - releasingConditionVariable.open(); - } - } - }.start(); - } - } - - /** - * Releases all resources associated with this instance. - */ - public void release() { - reset(); - releaseKeepSessionIdAudioTrack(); - for (AudioProcessor audioProcessor : availableAudioProcessors) { - audioProcessor.reset(); - } - audioSessionId = C.AUDIO_SESSION_ID_UNSET; - playing = false; - } - - /** - * Releases {@link #keepSessionIdAudioTrack} asynchronously, if it is non-{@code null}. - */ - private void releaseKeepSessionIdAudioTrack() { - if (keepSessionIdAudioTrack == null) { - return; - } - - // AudioTrack.release can take some time, so we call it on a background thread. - final android.media.AudioTrack toRelease = keepSessionIdAudioTrack; - keepSessionIdAudioTrack = null; - new Thread() { - @Override - public void run() { - toRelease.release(); - } - }.start(); - } - - /** - * Returns whether {@link #getCurrentPositionUs} can return the current playback position. - */ - private boolean hasCurrentPositionUs() { - return isInitialized() && startMediaTimeState != START_NOT_SET; - } - - /** - * Returns the underlying audio track {@code positionUs} with any applicable speedup applied. - */ - private long applySpeedup(long positionUs) { - while (!playbackParametersCheckpoints.isEmpty() - && positionUs >= playbackParametersCheckpoints.getFirst().positionUs) { - // We are playing (or about to play) media with the new playback parameters, so update them. - PlaybackParametersCheckpoint checkpoint = playbackParametersCheckpoints.remove(); - playbackParameters = checkpoint.playbackParameters; - playbackParametersPositionUs = checkpoint.positionUs; - playbackParametersOffsetUs = checkpoint.mediaTimeUs - startMediaTimeUs; - } - - if (playbackParameters.speed == 1f) { - return positionUs + playbackParametersOffsetUs - playbackParametersPositionUs; - } - - if (playbackParametersCheckpoints.isEmpty() - && sonicAudioProcessor.getOutputByteCount() >= SONIC_MIN_BYTES_FOR_SPEEDUP) { - return playbackParametersOffsetUs - + Util.scaleLargeTimestamp(positionUs - playbackParametersPositionUs, - sonicAudioProcessor.getInputByteCount(), sonicAudioProcessor.getOutputByteCount()); - } - - // We are playing drained data at a previous playback speed, or don't have enough bytes to - // calculate an accurate speedup, so fall back to multiplying by the speed. - return playbackParametersOffsetUs - + (long) ((double) playbackParameters.speed * (positionUs - playbackParametersPositionUs)); - } - - /** - * Updates the audio track latency and playback position parameters. - */ - private void maybeSampleSyncParams() { - long playbackPositionUs = audioTrackUtil.getPositionUs(); - if (playbackPositionUs == 0) { - // The AudioTrack hasn't output anything yet. - return; - } - long systemClockUs = System.nanoTime() / 1000; - if (systemClockUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) { - // Take a new sample and update the smoothed offset between the system clock and the playhead. - playheadOffsets[nextPlayheadOffsetIndex] = playbackPositionUs - systemClockUs; - nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT; - if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) { - playheadOffsetCount++; - } - lastPlayheadSampleTimeUs = systemClockUs; - smoothedPlayheadOffsetUs = 0; - for (int i = 0; i < playheadOffsetCount; i++) { - smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount; - } - } - - if (needsPassthroughWorkarounds()) { - // Don't sample the timestamp and latency if this is an AC-3 passthrough AudioTrack on - // platform API versions 21/22, as incorrect values are returned. See [Internal: b/21145353]. - return; - } - - if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) { - audioTimestampSet = audioTrackUtil.updateTimestamp(); - if (audioTimestampSet) { - // Perform sanity checks on the timestamp. - long audioTimestampUs = audioTrackUtil.getTimestampNanoTime() / 1000; - long audioTimestampFramePosition = audioTrackUtil.getTimestampFramePosition(); - if (audioTimestampUs < resumeSystemTimeUs) { - // The timestamp corresponds to a time before the track was most recently resumed. - audioTimestampSet = false; - } else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) { - // The timestamp time base is probably wrong. - String message = "Spurious audio timestamp (system clock mismatch): " - + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", " - + playbackPositionUs; - if (failOnSpuriousAudioTimestamp) { - throw new InvalidAudioTrackTimestampException(message); - } - Log.w(TAG, message); - audioTimestampSet = false; - } else if (Math.abs(framesToDurationUs(audioTimestampFramePosition) - playbackPositionUs) - > MAX_AUDIO_TIMESTAMP_OFFSET_US) { - // The timestamp frame position is probably wrong. - String message = "Spurious audio timestamp (frame position mismatch): " - + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", " - + playbackPositionUs; - if (failOnSpuriousAudioTimestamp) { - throw new InvalidAudioTrackTimestampException(message); - } - Log.w(TAG, message); - audioTimestampSet = false; - } - } - if (getLatencyMethod != null && !passthrough) { - try { - // Compute the audio track latency, excluding the latency due to the buffer (leaving - // latency due to the mixer and audio hardware driver). - latencyUs = (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L - - bufferSizeUs; - // Sanity check that the latency is non-negative. - latencyUs = Math.max(latencyUs, 0); - // Sanity check that the latency isn't too large. - if (latencyUs > MAX_LATENCY_US) { - Log.w(TAG, "Ignoring impossibly large audio latency: " + latencyUs); - latencyUs = 0; - } - } catch (Exception e) { - // The method existed, but doesn't work. Don't try again. - getLatencyMethod = null; - } - } - lastTimestampSampleTimeUs = systemClockUs; - } - } - - /** - * Checks that {@link #audioTrack} has been successfully initialized. If it has then calling this - * method is a no-op. If it hasn't then {@link #audioTrack} is released and set to null, and an - * exception is thrown. - * - * @throws InitializationException If {@link #audioTrack} has not been successfully initialized. - */ - private void checkAudioTrackInitialized() throws InitializationException { - int state = audioTrack.getState(); - if (state == STATE_INITIALIZED) { - return; - } - // The track is not successfully initialized. Release and null the track. - try { - audioTrack.release(); - } catch (Exception e) { - // The track has already failed to initialize, so it wouldn't be that surprising if release - // were to fail too. Swallow the exception. - } finally { - audioTrack = null; - } - - throw new InitializationException(state, sampleRate, channelConfig, bufferSize); - } - - private boolean isInitialized() { - return audioTrack != null; - } - - private long framesToDurationUs(long frameCount) { - return (frameCount * C.MICROS_PER_SECOND) / sampleRate; - } - - private long durationUsToFrames(long durationUs) { - return (durationUs * sampleRate) / C.MICROS_PER_SECOND; - } - - private long getSubmittedFrames() { - return passthrough ? submittedEncodedFrames : (submittedPcmBytes / pcmFrameSize); - } - - private long getWrittenFrames() { - return passthrough ? writtenEncodedFrames : (writtenPcmBytes / outputPcmFrameSize); - } - - private void resetSyncParams() { - smoothedPlayheadOffsetUs = 0; - playheadOffsetCount = 0; - nextPlayheadOffsetIndex = 0; - lastPlayheadSampleTimeUs = 0; - audioTimestampSet = false; - lastTimestampSampleTimeUs = 0; - } - - /** - * Returns whether to work around problems with passthrough audio tracks. - * See [Internal: b/18899620, b/19187573, b/21145353]. - */ - private boolean needsPassthroughWorkarounds() { - return Util.SDK_INT < 23 - && (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3); - } - - /** - * Returns whether the audio track should behave as though it has pending data. This is to work - * around an issue on platform API versions 21/22 where AC-3 audio tracks can't be paused, so we - * empty their buffers when paused. In this case, they should still behave as if they have - * pending data, otherwise writing will never resume. - */ - private boolean overrideHasPendingData() { - return needsPassthroughWorkarounds() - && audioTrack.getPlayState() == PLAYSTATE_PAUSED - && audioTrack.getPlaybackHeadPosition() == 0; - } - - /** - * Instantiates an {@link android.media.AudioTrack} to be used with tunneling video playback. - */ - @TargetApi(21) - private static android.media.AudioTrack createHwAvSyncAudioTrackV21(int sampleRate, - int channelConfig, int encoding, int bufferSize, int sessionId) { - AudioAttributes attributesBuilder = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_MEDIA) - .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) - .setFlags(AudioAttributes.FLAG_HW_AV_SYNC) - .build(); - AudioFormat format = new AudioFormat.Builder() - .setChannelMask(channelConfig) - .setEncoding(encoding) - .setSampleRate(sampleRate) - .build(); - return new android.media.AudioTrack(attributesBuilder, format, bufferSize, MODE_STREAM, - sessionId); - } - - @C.Encoding - private static int getEncodingForMimeType(String mimeType) { - switch (mimeType) { - case MimeTypes.AUDIO_AC3: - return C.ENCODING_AC3; - case MimeTypes.AUDIO_E_AC3: - return C.ENCODING_E_AC3; - case MimeTypes.AUDIO_DTS: - return C.ENCODING_DTS; - case MimeTypes.AUDIO_DTS_HD: - return C.ENCODING_DTS_HD; - default: - return C.ENCODING_INVALID; - } - } - - private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) { - if (encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD) { - return DtsUtil.parseDtsAudioSampleCount(buffer); - } else if (encoding == C.ENCODING_AC3) { - return Ac3Util.getAc3SyncframeAudioSampleCount(); - } else if (encoding == C.ENCODING_E_AC3) { - return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); - } else { - throw new IllegalStateException("Unexpected audio encoding: " + encoding); - } - } - - @TargetApi(21) - private static int writeNonBlockingV21(android.media.AudioTrack audioTrack, ByteBuffer buffer, - int size) { - return audioTrack.write(buffer, size, WRITE_NON_BLOCKING); - } - - @TargetApi(21) - private int writeNonBlockingWithAvSyncV21(android.media.AudioTrack audioTrack, - ByteBuffer buffer, int size, long presentationTimeUs) { - // TODO: Uncomment this when [Internal ref b/33627517] is clarified or fixed. - // if (Util.SDK_INT >= 23) { - // // The underlying platform AudioTrack writes AV sync headers directly. - // return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000); - // } - if (avSyncHeader == null) { - avSyncHeader = ByteBuffer.allocate(16); - avSyncHeader.order(ByteOrder.BIG_ENDIAN); - avSyncHeader.putInt(0x55550001); - } - if (bytesUntilNextAvSync == 0) { - avSyncHeader.putInt(4, size); - avSyncHeader.putLong(8, presentationTimeUs * 1000); - avSyncHeader.position(0); - bytesUntilNextAvSync = size; - } - int avSyncHeaderBytesRemaining = avSyncHeader.remaining(); - if (avSyncHeaderBytesRemaining > 0) { - int result = audioTrack.write(avSyncHeader, avSyncHeaderBytesRemaining, WRITE_NON_BLOCKING); - if (result < 0) { - bytesUntilNextAvSync = 0; - return result; - } - if (result < avSyncHeaderBytesRemaining) { - return 0; - } - } - int result = writeNonBlockingV21(audioTrack, buffer, size); - if (result < 0) { - bytesUntilNextAvSync = 0; - return result; - } - bytesUntilNextAvSync -= result; - return result; - } - - @TargetApi(21) - private static void setVolumeInternalV21(android.media.AudioTrack audioTrack, float volume) { - audioTrack.setVolume(volume); - } - - @SuppressWarnings("deprecation") - private static void setVolumeInternalV3(android.media.AudioTrack audioTrack, float volume) { - audioTrack.setStereoVolume(volume, volume); - } - - /** - * Wraps an {@link android.media.AudioTrack} to expose useful utility methods. - */ - private static class AudioTrackUtil { - - protected android.media.AudioTrack audioTrack; - private boolean needsPassthroughWorkaround; - private int sampleRate; - private long lastRawPlaybackHeadPosition; - private long rawPlaybackHeadWrapCount; - private long passthroughWorkaroundPauseOffset; - - private long stopTimestampUs; - private long stopPlaybackHeadPosition; - private long endPlaybackHeadPosition; - - /** - * Reconfigures the audio track utility helper to use the specified {@code audioTrack}. - * - * @param audioTrack The audio track to wrap. - * @param needsPassthroughWorkaround Whether to workaround issues with pausing AC-3 passthrough - * audio tracks on platform API version 21/22. - */ - public void reconfigure(android.media.AudioTrack audioTrack, - boolean needsPassthroughWorkaround) { - this.audioTrack = audioTrack; - this.needsPassthroughWorkaround = needsPassthroughWorkaround; - stopTimestampUs = C.TIME_UNSET; - lastRawPlaybackHeadPosition = 0; - rawPlaybackHeadWrapCount = 0; - passthroughWorkaroundPauseOffset = 0; - if (audioTrack != null) { - sampleRate = audioTrack.getSampleRate(); - } - } - - /** - * Stops the audio track in a way that ensures media written to it is played out in full, and - * that {@link #getPlaybackHeadPosition()} and {@link #getPositionUs()} continue to increment as - * the remaining media is played out. - * - * @param writtenFrames The total number of frames that have been written. - */ - public void handleEndOfStream(long writtenFrames) { - stopPlaybackHeadPosition = getPlaybackHeadPosition(); - stopTimestampUs = SystemClock.elapsedRealtime() * 1000; - endPlaybackHeadPosition = writtenFrames; - audioTrack.stop(); - } - - /** - * Pauses the audio track unless the end of the stream has been handled, in which case calling - * this method does nothing. - */ - public void pause() { - if (stopTimestampUs != C.TIME_UNSET) { - // We don't want to knock the audio track back into the paused state. - return; - } - audioTrack.pause(); - } - - /** - * {@link android.media.AudioTrack#getPlaybackHeadPosition()} returns a value intended to be - * interpreted as an unsigned 32 bit integer, which also wraps around periodically. This method - * returns the playback head position as a long that will only wrap around if the value exceeds - * {@link Long#MAX_VALUE} (which in practice will never happen). - * - * @return The playback head position, in frames. - */ - public long getPlaybackHeadPosition() { - if (stopTimestampUs != C.TIME_UNSET) { - // Simulate the playback head position up to the total number of frames submitted. - long elapsedTimeSinceStopUs = (SystemClock.elapsedRealtime() * 1000) - stopTimestampUs; - long framesSinceStop = (elapsedTimeSinceStopUs * sampleRate) / C.MICROS_PER_SECOND; - return Math.min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop); - } - - int state = audioTrack.getPlayState(); - if (state == PLAYSTATE_STOPPED) { - // The audio track hasn't been started. - return 0; - } - - long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition(); - if (needsPassthroughWorkaround) { - // Work around an issue with passthrough/direct AudioTracks on platform API versions 21/22 - // where the playback head position jumps back to zero on paused passthrough/direct audio - // tracks. See [Internal: b/19187573]. - if (state == PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) { - passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition; - } - rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset; - } - if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) { - // The value must have wrapped around. - rawPlaybackHeadWrapCount++; - } - lastRawPlaybackHeadPosition = rawPlaybackHeadPosition; - return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32); - } - - /** - * Returns the duration of played media since reconfiguration, in microseconds. - */ - public long getPositionUs() { - return (getPlaybackHeadPosition() * C.MICROS_PER_SECOND) / sampleRate; - } - - /** - * Updates the values returned by {@link #getTimestampNanoTime()} and - * {@link #getTimestampFramePosition()}. - * - * @return Whether the timestamp values were updated. - */ - public boolean updateTimestamp() { - return false; - } - - /** - * Returns the {@link android.media.AudioTimestamp#nanoTime} obtained during the most recent - * call to {@link #updateTimestamp()} that returned true. - * - * @return The nanoTime obtained during the most recent call to {@link #updateTimestamp()} that - * returned true. - * @throws UnsupportedOperationException If the implementation does not support audio timestamp - * queries. {@link #updateTimestamp()} will always return false in this case. - */ - public long getTimestampNanoTime() { - // Should never be called if updateTimestamp() returned false. - throw new UnsupportedOperationException(); - } - - /** - * Returns the {@link android.media.AudioTimestamp#framePosition} obtained during the most - * recent call to {@link #updateTimestamp()} that returned true. The value is adjusted so that - * wrap around only occurs if the value exceeds {@link Long#MAX_VALUE} (which in practice will - * never happen). - * - * @return The framePosition obtained during the most recent call to {@link #updateTimestamp()} - * that returned true. - * @throws UnsupportedOperationException If the implementation does not support audio timestamp - * queries. {@link #updateTimestamp()} will always return false in this case. - */ - public long getTimestampFramePosition() { - // Should never be called if updateTimestamp() returned false. - throw new UnsupportedOperationException(); - } - - } - - @TargetApi(19) - private static class AudioTrackUtilV19 extends AudioTrackUtil { - - private final AudioTimestamp audioTimestamp; - - private long rawTimestampFramePositionWrapCount; - private long lastRawTimestampFramePosition; - private long lastTimestampFramePosition; - - public AudioTrackUtilV19() { - audioTimestamp = new AudioTimestamp(); - } - - @Override - public void reconfigure(android.media.AudioTrack audioTrack, - boolean needsPassthroughWorkaround) { - super.reconfigure(audioTrack, needsPassthroughWorkaround); - rawTimestampFramePositionWrapCount = 0; - lastRawTimestampFramePosition = 0; - lastTimestampFramePosition = 0; - } - - @Override - public boolean updateTimestamp() { - boolean updated = audioTrack.getTimestamp(audioTimestamp); - if (updated) { - long rawFramePosition = audioTimestamp.framePosition; - if (lastRawTimestampFramePosition > rawFramePosition) { - // The value must have wrapped around. - rawTimestampFramePositionWrapCount++; - } - lastRawTimestampFramePosition = rawFramePosition; - lastTimestampFramePosition = rawFramePosition + (rawTimestampFramePositionWrapCount << 32); - } - return updated; - } - - @Override - public long getTimestampNanoTime() { - return audioTimestamp.nanoTime; - } - - @Override - public long getTimestampFramePosition() { - return lastTimestampFramePosition; - } - - } - - /** - * Stores playback parameters with the position and media time at which they apply. - */ - private static final class PlaybackParametersCheckpoint { - - private final PlaybackParameters playbackParameters; - private final long mediaTimeUs; - private final long positionUs; - - private PlaybackParametersCheckpoint(PlaybackParameters playbackParameters, long mediaTimeUs, - long positionUs) { - this.playbackParameters = playbackParameters; - this.mediaTimeUs = mediaTimeUs; - this.positionUs = positionUs; - } - - } - -} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java new file mode 100644 index 000000000000..e62e8cf2c53a --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -0,0 +1,545 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.media.AudioTimestamp; +import android.media.AudioTrack; +import android.os.SystemClock; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; + +/** + * Wraps an {@link AudioTrack}, exposing a position based on {@link + * AudioTrack#getPlaybackHeadPosition()} and {@link AudioTrack#getTimestamp(AudioTimestamp)}. + * + *

Call {@link #setAudioTrack(AudioTrack, int, int, int)} to set the audio track to wrap. Call + * {@link #mayHandleBuffer(long)} if there is input data to write to the track. If it returns false, + * the audio track position is stabilizing and no data may be written. Call {@link #start()} + * immediately before calling {@link AudioTrack#play()}. Call {@link #pause()} when pausing the + * track. Call {@link #handleEndOfStream(long)} when no more data will be written to the track. When + * the audio track will no longer be used, call {@link #reset()}. + */ +/* package */ final class AudioTrackPositionTracker { + + /** Listener for position tracker events. */ + public interface Listener { + + /** + * Called when the frame position is too far from the expected frame position. + * + * @param audioTimestampPositionFrames The frame position of the last known audio track + * timestamp. + * @param audioTimestampSystemTimeUs The system time associated with the last known audio track + * timestamp, in microseconds. + * @param systemTimeUs The current time. + * @param playbackPositionUs The current playback head position in microseconds. + */ + void onPositionFramesMismatch( + long audioTimestampPositionFrames, + long audioTimestampSystemTimeUs, + long systemTimeUs, + long playbackPositionUs); + + /** + * Called when the system time associated with the last known audio track timestamp is + * unexpectedly far from the current time. + * + * @param audioTimestampPositionFrames The frame position of the last known audio track + * timestamp. + * @param audioTimestampSystemTimeUs The system time associated with the last known audio track + * timestamp, in microseconds. + * @param systemTimeUs The current time. + * @param playbackPositionUs The current playback head position in microseconds. + */ + void onSystemTimeUsMismatch( + long audioTimestampPositionFrames, + long audioTimestampSystemTimeUs, + long systemTimeUs, + long playbackPositionUs); + + /** + * Called when the audio track has provided an invalid latency. + * + * @param latencyUs The reported latency in microseconds. + */ + void onInvalidLatency(long latencyUs); + + /** + * Called when the audio track runs out of data to play. + * + * @param bufferSize The size of the sink's buffer, in bytes. + * @param bufferSizeMs The size of the sink's buffer, in milliseconds, if it is configured for + * PCM output. {@link C#TIME_UNSET} if it is configured for encoded audio output, as the + * buffered media can have a variable bitrate so the duration may be unknown. + */ + void onUnderrun(int bufferSize, long bufferSizeMs); + } + + /** {@link AudioTrack} playback states. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({PLAYSTATE_STOPPED, PLAYSTATE_PAUSED, PLAYSTATE_PLAYING}) + private @interface PlayState {} + /** @see AudioTrack#PLAYSTATE_STOPPED */ + private static final int PLAYSTATE_STOPPED = AudioTrack.PLAYSTATE_STOPPED; + /** @see AudioTrack#PLAYSTATE_PAUSED */ + private static final int PLAYSTATE_PAUSED = AudioTrack.PLAYSTATE_PAUSED; + /** @see AudioTrack#PLAYSTATE_PLAYING */ + private static final int PLAYSTATE_PLAYING = AudioTrack.PLAYSTATE_PLAYING; + + /** + * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more than + * this amount. + * + *

This is a fail safe that should not be required on correctly functioning devices. + */ + private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND; + + /** + * AudioTrack latencies are deemed impossibly large if they are greater than this amount. + * + *

This is a fail safe that should not be required on correctly functioning devices. + */ + private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND; + + private static final long FORCE_RESET_WORKAROUND_TIMEOUT_MS = 200; + + private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10; + private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; + private static final int MIN_LATENCY_SAMPLE_INTERVAL_US = 500000; + + private final Listener listener; + private final long[] playheadOffsets; + + @Nullable private AudioTrack audioTrack; + private int outputPcmFrameSize; + private int bufferSize; + @Nullable private AudioTimestampPoller audioTimestampPoller; + private int outputSampleRate; + private boolean needsPassthroughWorkarounds; + private long bufferSizeUs; + + private long smoothedPlayheadOffsetUs; + private long lastPlayheadSampleTimeUs; + + @Nullable private Method getLatencyMethod; + private long latencyUs; + private boolean hasData; + + private boolean isOutputPcm; + private long lastLatencySampleTimeUs; + private long lastRawPlaybackHeadPosition; + private long rawPlaybackHeadWrapCount; + private long passthroughWorkaroundPauseOffset; + private int nextPlayheadOffsetIndex; + private int playheadOffsetCount; + private long stopTimestampUs; + private long forceResetWorkaroundTimeMs; + private long stopPlaybackHeadPosition; + private long endPlaybackHeadPosition; + + /** + * Creates a new audio track position tracker. + * + * @param listener A listener for position tracking events. + */ + public AudioTrackPositionTracker(Listener listener) { + this.listener = Assertions.checkNotNull(listener); + if (Util.SDK_INT >= 18) { + try { + getLatencyMethod = AudioTrack.class.getMethod("getLatency", (Class[]) null); + } catch (NoSuchMethodException e) { + // There's no guarantee this method exists. Do nothing. + } + } + playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; + } + + /** + * Sets the {@link AudioTrack} to wrap. Subsequent method calls on this instance relate to this + * track's position, until the next call to {@link #reset()}. + * + * @param audioTrack The audio track to wrap. + * @param outputEncoding The encoding of the audio track. + * @param outputPcmFrameSize For PCM output encodings, the frame size. The value is ignored + * otherwise. + * @param bufferSize The audio track buffer size in bytes. + */ + public void setAudioTrack( + AudioTrack audioTrack, + @C.Encoding int outputEncoding, + int outputPcmFrameSize, + int bufferSize) { + this.audioTrack = audioTrack; + this.outputPcmFrameSize = outputPcmFrameSize; + this.bufferSize = bufferSize; + audioTimestampPoller = new AudioTimestampPoller(audioTrack); + outputSampleRate = audioTrack.getSampleRate(); + needsPassthroughWorkarounds = needsPassthroughWorkarounds(outputEncoding); + isOutputPcm = Util.isEncodingLinearPcm(outputEncoding); + bufferSizeUs = isOutputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET; + lastRawPlaybackHeadPosition = 0; + rawPlaybackHeadWrapCount = 0; + passthroughWorkaroundPauseOffset = 0; + hasData = false; + stopTimestampUs = C.TIME_UNSET; + forceResetWorkaroundTimeMs = C.TIME_UNSET; + latencyUs = 0; + } + + public long getCurrentPositionUs(boolean sourceEnded) { + if (Assertions.checkNotNull(this.audioTrack).getPlayState() == PLAYSTATE_PLAYING) { + maybeSampleSyncParams(); + } + + // If the device supports it, use the playback timestamp from AudioTrack.getTimestamp. + // Otherwise, derive a smoothed position by sampling the track's frame position. + long systemTimeUs = System.nanoTime() / 1000; + AudioTimestampPoller audioTimestampPoller = Assertions.checkNotNull(this.audioTimestampPoller); + if (audioTimestampPoller.hasTimestamp()) { + // Calculate the speed-adjusted position using the timestamp (which may be in the future). + long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames(); + long timestampPositionUs = framesToDurationUs(timestampPositionFrames); + if (!audioTimestampPoller.isTimestampAdvancing()) { + return timestampPositionUs; + } + long elapsedSinceTimestampUs = systemTimeUs - audioTimestampPoller.getTimestampSystemTimeUs(); + return timestampPositionUs + elapsedSinceTimestampUs; + } else { + long positionUs; + if (playheadOffsetCount == 0) { + // The AudioTrack has started, but we don't have any samples to compute a smoothed position. + positionUs = getPlaybackHeadPositionUs(); + } else { + // getPlaybackHeadPositionUs() only has a granularity of ~20 ms, so we base the position off + // the system clock (and a smoothed offset between it and the playhead position) so as to + // prevent jitter in the reported positions. + positionUs = systemTimeUs + smoothedPlayheadOffsetUs; + } + if (!sourceEnded) { + positionUs -= latencyUs; + } + return positionUs; + } + } + + /** Starts position tracking. Must be called immediately before {@link AudioTrack#play()}. */ + public void start() { + Assertions.checkNotNull(audioTimestampPoller).reset(); + } + + /** Returns whether the audio track is in the playing state. */ + public boolean isPlaying() { + return Assertions.checkNotNull(audioTrack).getPlayState() == PLAYSTATE_PLAYING; + } + + /** + * Checks the state of the audio track and returns whether the caller can write data to the track. + * Notifies {@link Listener#onUnderrun(int, long)} if the track has underrun. + * + * @param writtenFrames The number of frames that have been written. + * @return Whether the caller can write data to the track. + */ + public boolean mayHandleBuffer(long writtenFrames) { + @PlayState int playState = Assertions.checkNotNull(audioTrack).getPlayState(); + if (needsPassthroughWorkarounds) { + // An AC-3 audio track continues to play data written while it is paused. Stop writing so its + // buffer empties. See [Internal: b/18899620]. + if (playState == PLAYSTATE_PAUSED) { + // We force an underrun to pause the track, so don't notify the listener in this case. + hasData = false; + return false; + } + + // A new AC-3 audio track's playback position continues to increase from the old track's + // position for a short time after is has been released. Avoid writing data until the playback + // head position actually returns to zero. + if (playState == PLAYSTATE_STOPPED && getPlaybackHeadPosition() == 0) { + return false; + } + } + + boolean hadData = hasData; + hasData = hasPendingData(writtenFrames); + if (hadData && !hasData && playState != PLAYSTATE_STOPPED && listener != null) { + listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs)); + } + + return true; + } + + /** + * Returns an estimate of the number of additional bytes that can be written to the audio track's + * buffer without running out of space. + * + *

May only be called if the output encoding is one of the PCM encodings. + * + * @param writtenBytes The number of bytes written to the audio track so far. + * @return An estimate of the number of bytes that can be written. + */ + public int getAvailableBufferSize(long writtenBytes) { + int bytesPending = (int) (writtenBytes - (getPlaybackHeadPosition() * outputPcmFrameSize)); + return bufferSize - bytesPending; + } + + /** Returns whether the track is in an invalid state and must be recreated. */ + public boolean isStalled(long writtenFrames) { + return forceResetWorkaroundTimeMs != C.TIME_UNSET + && writtenFrames > 0 + && SystemClock.elapsedRealtime() - forceResetWorkaroundTimeMs + >= FORCE_RESET_WORKAROUND_TIMEOUT_MS; + } + + /** + * Records the writing position at which the stream ended, so that the reported position can + * continue to increment while remaining data is played out. + * + * @param writtenFrames The number of frames that have been written. + */ + public void handleEndOfStream(long writtenFrames) { + stopPlaybackHeadPosition = getPlaybackHeadPosition(); + stopTimestampUs = SystemClock.elapsedRealtime() * 1000; + endPlaybackHeadPosition = writtenFrames; + } + + /** + * Returns whether the audio track has any pending data to play out at its current position. + * + * @param writtenFrames The number of frames written to the audio track. + * @return Whether the audio track has any pending data to play out. + */ + public boolean hasPendingData(long writtenFrames) { + return writtenFrames > getPlaybackHeadPosition() + || forceHasPendingData(); + } + + /** + * Pauses the audio track position tracker, returning whether the audio track needs to be paused + * to cause playback to pause. If {@code false} is returned the audio track will pause without + * further interaction, as the end of stream has been handled. + */ + public boolean pause() { + resetSyncParams(); + if (stopTimestampUs == C.TIME_UNSET) { + // The audio track is going to be paused, so reset the timestamp poller to ensure it doesn't + // supply an advancing position. + Assertions.checkNotNull(audioTimestampPoller).reset(); + return true; + } + // We've handled the end of the stream already, so there's no need to pause the track. + return false; + } + + /** + * Resets the position tracker. Should be called when the audio track previous passed to {@link + * #setAudioTrack(AudioTrack, int, int, int)} is no longer in use. + */ + public void reset() { + resetSyncParams(); + audioTrack = null; + audioTimestampPoller = null; + } + + private void maybeSampleSyncParams() { + long playbackPositionUs = getPlaybackHeadPositionUs(); + if (playbackPositionUs == 0) { + // The AudioTrack hasn't output anything yet. + return; + } + long systemTimeUs = System.nanoTime() / 1000; + if (systemTimeUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) { + // Take a new sample and update the smoothed offset between the system clock and the playhead. + playheadOffsets[nextPlayheadOffsetIndex] = playbackPositionUs - systemTimeUs; + nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT; + if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) { + playheadOffsetCount++; + } + lastPlayheadSampleTimeUs = systemTimeUs; + smoothedPlayheadOffsetUs = 0; + for (int i = 0; i < playheadOffsetCount; i++) { + smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount; + } + } + + if (needsPassthroughWorkarounds) { + // Don't sample the timestamp and latency if this is an AC-3 passthrough AudioTrack on + // platform API versions 21/22, as incorrect values are returned. See [Internal: b/21145353]. + return; + } + + maybePollAndCheckTimestamp(systemTimeUs, playbackPositionUs); + maybeUpdateLatency(systemTimeUs); + } + + private void maybePollAndCheckTimestamp(long systemTimeUs, long playbackPositionUs) { + AudioTimestampPoller audioTimestampPoller = Assertions.checkNotNull(this.audioTimestampPoller); + if (!audioTimestampPoller.maybePollTimestamp(systemTimeUs)) { + return; + } + + // Perform sanity checks on the timestamp and accept/reject it. + long audioTimestampSystemTimeUs = audioTimestampPoller.getTimestampSystemTimeUs(); + long audioTimestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames(); + if (Math.abs(audioTimestampSystemTimeUs - systemTimeUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) { + listener.onSystemTimeUsMismatch( + audioTimestampPositionFrames, + audioTimestampSystemTimeUs, + systemTimeUs, + playbackPositionUs); + audioTimestampPoller.rejectTimestamp(); + } else if (Math.abs(framesToDurationUs(audioTimestampPositionFrames) - playbackPositionUs) + > MAX_AUDIO_TIMESTAMP_OFFSET_US) { + listener.onPositionFramesMismatch( + audioTimestampPositionFrames, + audioTimestampSystemTimeUs, + systemTimeUs, + playbackPositionUs); + audioTimestampPoller.rejectTimestamp(); + } else { + audioTimestampPoller.acceptTimestamp(); + } + } + + private void maybeUpdateLatency(long systemTimeUs) { + if (isOutputPcm + && getLatencyMethod != null + && systemTimeUs - lastLatencySampleTimeUs >= MIN_LATENCY_SAMPLE_INTERVAL_US) { + try { + // Compute the audio track latency, excluding the latency due to the buffer (leaving + // latency due to the mixer and audio hardware driver). + latencyUs = + castNonNull((Integer) getLatencyMethod.invoke(Assertions.checkNotNull(audioTrack))) + * 1000L + - bufferSizeUs; + // Sanity check that the latency is non-negative. + latencyUs = Math.max(latencyUs, 0); + // Sanity check that the latency isn't too large. + if (latencyUs > MAX_LATENCY_US) { + listener.onInvalidLatency(latencyUs); + latencyUs = 0; + } + } catch (Exception e) { + // The method existed, but doesn't work. Don't try again. + getLatencyMethod = null; + } + lastLatencySampleTimeUs = systemTimeUs; + } + } + + private long framesToDurationUs(long frameCount) { + return (frameCount * C.MICROS_PER_SECOND) / outputSampleRate; + } + + private void resetSyncParams() { + smoothedPlayheadOffsetUs = 0; + playheadOffsetCount = 0; + nextPlayheadOffsetIndex = 0; + lastPlayheadSampleTimeUs = 0; + } + + /** + * If passthrough workarounds are enabled, pausing is implemented by forcing the AudioTrack to + * underrun. In this case, still behave as if we have pending data, otherwise writing won't + * resume. + */ + private boolean forceHasPendingData() { + return needsPassthroughWorkarounds + && Assertions.checkNotNull(audioTrack).getPlayState() == AudioTrack.PLAYSTATE_PAUSED + && getPlaybackHeadPosition() == 0; + } + + /** + * Returns whether to work around problems with passthrough audio tracks. See [Internal: + * b/18899620, b/19187573, b/21145353]. + */ + private static boolean needsPassthroughWorkarounds(@C.Encoding int outputEncoding) { + return Util.SDK_INT < 23 + && (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3); + } + + private long getPlaybackHeadPositionUs() { + return framesToDurationUs(getPlaybackHeadPosition()); + } + + /** + * {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as an + * unsigned 32 bit integer, which also wraps around periodically. This method returns the playback + * head position as a long that will only wrap around if the value exceeds {@link Long#MAX_VALUE} + * (which in practice will never happen). + * + * @return The playback head position, in frames. + */ + private long getPlaybackHeadPosition() { + AudioTrack audioTrack = Assertions.checkNotNull(this.audioTrack); + if (stopTimestampUs != C.TIME_UNSET) { + // Simulate the playback head position up to the total number of frames submitted. + long elapsedTimeSinceStopUs = (SystemClock.elapsedRealtime() * 1000) - stopTimestampUs; + long framesSinceStop = (elapsedTimeSinceStopUs * outputSampleRate) / C.MICROS_PER_SECOND; + return Math.min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop); + } + + int state = audioTrack.getPlayState(); + if (state == PLAYSTATE_STOPPED) { + // The audio track hasn't been started. + return 0; + } + + long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition(); + if (needsPassthroughWorkarounds) { + // Work around an issue with passthrough/direct AudioTracks on platform API versions 21/22 + // where the playback head position jumps back to zero on paused passthrough/direct audio + // tracks. See [Internal: b/19187573]. + if (state == PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) { + passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition; + } + rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset; + } + + if (Util.SDK_INT <= 29) { + if (rawPlaybackHeadPosition == 0 + && lastRawPlaybackHeadPosition > 0 + && state == PLAYSTATE_PLAYING) { + // If connecting a Bluetooth audio device fails, the AudioTrack may be left in a state + // where its Java API is in the playing state, but the native track is stopped. When this + // happens the playback head position gets stuck at zero. In this case, return the old + // playback head position and force the track to be reset after + // {@link #FORCE_RESET_WORKAROUND_TIMEOUT_MS} has elapsed. + if (forceResetWorkaroundTimeMs == C.TIME_UNSET) { + forceResetWorkaroundTimeMs = SystemClock.elapsedRealtime(); + } + return lastRawPlaybackHeadPosition; + } else { + forceResetWorkaroundTimeMs = C.TIME_UNSET; + } + } + + if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) { + // The value must have wrapped around. + rawPlaybackHeadWrapCount++; + } + lastRawPlaybackHeadPosition = rawPlaybackHeadPosition; + return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AuxEffectInfo.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AuxEffectInfo.java new file mode 100644 index 000000000000..6039a8c1a88e --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AuxEffectInfo.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +import android.media.AudioTrack; +import android.media.audiofx.AudioEffect; +import androidx.annotation.Nullable; + +/** + * Represents auxiliary effect information, which can be used to attach an auxiliary effect to an + * underlying {@link AudioTrack}. + * + *

Auxiliary effects can only be applied if the application has the {@code + * android.permission.MODIFY_AUDIO_SETTINGS} permission. Apps are responsible for retaining the + * associated audio effect instance and releasing it when it's no longer needed. See the + * documentation of {@link AudioEffect} for more information. + */ +public final class AuxEffectInfo { + + /** Value for {@link #effectId} representing no auxiliary effect. */ + public static final int NO_AUX_EFFECT_ID = 0; + + /** + * The identifier of the effect, or {@link #NO_AUX_EFFECT_ID} if there is no effect. + * + * @see android.media.AudioTrack#attachAuxEffect(int) + */ + public final int effectId; + /** + * The send level for the effect. + * + * @see android.media.AudioTrack#setAuxEffectSendLevel(float) + */ + public final float sendLevel; + + /** + * Creates an instance with the given effect identifier and send level. + * + * @param effectId The effect identifier. This is the value returned by {@link + * AudioEffect#getId()} on the effect, or {@value NO_AUX_EFFECT_ID} which represents no + * effect. This value is passed to {@link AudioTrack#attachAuxEffect(int)} on the underlying + * audio track. + * @param sendLevel The send level for the effect, where 0 represents no effect and a value of 1 + * is full send. If {@code effectId} is not {@value #NO_AUX_EFFECT_ID}, this value is passed + * to {@link AudioTrack#setAuxEffectSendLevel(float)} on the underlying audio track. + */ + public AuxEffectInfo(int effectId, float sendLevel) { + this.effectId = effectId; + this.sendLevel = sendLevel; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AuxEffectInfo auxEffectInfo = (AuxEffectInfo) o; + return effectId == auxEffectInfo.effectId + && Float.compare(auxEffectInfo.sendLevel, sendLevel) == 0; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + effectId; + result = 31 * result + Float.floatToIntBits(sendLevel); + return result; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/BaseAudioProcessor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/BaseAudioProcessor.java new file mode 100644 index 000000000000..189d8f026574 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/BaseAudioProcessor.java @@ -0,0 +1,143 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +import androidx.annotation.CallSuper; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Base class for audio processors that keep an output buffer and an internal buffer that is reused + * whenever input is queued. Subclasses should override {@link #onConfigure(AudioFormat)} to return + * the output audio format for the processor if it's active. + */ +public abstract class BaseAudioProcessor implements AudioProcessor { + + /** The current input audio format. */ + protected AudioFormat inputAudioFormat; + /** The current output audio format. */ + protected AudioFormat outputAudioFormat; + + private AudioFormat pendingInputAudioFormat; + private AudioFormat pendingOutputAudioFormat; + private ByteBuffer buffer; + private ByteBuffer outputBuffer; + private boolean inputEnded; + + public BaseAudioProcessor() { + buffer = EMPTY_BUFFER; + outputBuffer = EMPTY_BUFFER; + pendingInputAudioFormat = AudioFormat.NOT_SET; + pendingOutputAudioFormat = AudioFormat.NOT_SET; + inputAudioFormat = AudioFormat.NOT_SET; + outputAudioFormat = AudioFormat.NOT_SET; + } + + @Override + public final AudioFormat configure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + pendingInputAudioFormat = inputAudioFormat; + pendingOutputAudioFormat = onConfigure(inputAudioFormat); + return isActive() ? pendingOutputAudioFormat : AudioFormat.NOT_SET; + } + + @Override + public boolean isActive() { + return pendingOutputAudioFormat != AudioFormat.NOT_SET; + } + + @Override + public final void queueEndOfStream() { + inputEnded = true; + onQueueEndOfStream(); + } + + @CallSuper + @Override + public ByteBuffer getOutput() { + ByteBuffer outputBuffer = this.outputBuffer; + this.outputBuffer = EMPTY_BUFFER; + return outputBuffer; + } + + @CallSuper + @SuppressWarnings("ReferenceEquality") + @Override + public boolean isEnded() { + return inputEnded && outputBuffer == EMPTY_BUFFER; + } + + @Override + public final void flush() { + outputBuffer = EMPTY_BUFFER; + inputEnded = false; + inputAudioFormat = pendingInputAudioFormat; + outputAudioFormat = pendingOutputAudioFormat; + onFlush(); + } + + @Override + public final void reset() { + flush(); + buffer = EMPTY_BUFFER; + pendingInputAudioFormat = AudioFormat.NOT_SET; + pendingOutputAudioFormat = AudioFormat.NOT_SET; + inputAudioFormat = AudioFormat.NOT_SET; + outputAudioFormat = AudioFormat.NOT_SET; + onReset(); + } + + /** + * Replaces the current output buffer with a buffer of at least {@code count} bytes and returns + * it. Callers should write to the returned buffer then {@link ByteBuffer#flip()} it so it can be + * read via {@link #getOutput()}. + */ + protected final ByteBuffer replaceOutputBuffer(int count) { + if (buffer.capacity() < count) { + buffer = ByteBuffer.allocateDirect(count).order(ByteOrder.nativeOrder()); + } else { + buffer.clear(); + } + outputBuffer = buffer; + return buffer; + } + + /** Returns whether the current output buffer has any data remaining. */ + protected final boolean hasPendingOutput() { + return outputBuffer.hasRemaining(); + } + + /** Called when the processor is configured for a new input format. */ + protected AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + return AudioFormat.NOT_SET; + } + + /** Called when the end-of-stream is queued to the processor. */ + protected void onQueueEndOfStream() { + // Do nothing. + } + + /** Called when the processor is flushed, directly or as part of resetting. */ + protected void onFlush() { + // Do nothing. + } + + /** Called when the processor is reset. */ + protected void onReset() { + // Do nothing. + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index 1edc108a23d9..e8496d460816 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -15,148 +15,85 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.audio; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; -import org.mozilla.thirdparty.com.google.android.exoplayer2.C.Encoding; -import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.Arrays; /** * An {@link AudioProcessor} that applies a mapping from input channels onto specified output * channels. This can be used to reorder, duplicate or discard channels. */ -/* package */ final class ChannelMappingAudioProcessor implements AudioProcessor { +@SuppressWarnings("nullness:initialization.fields.uninitialized") +/* package */ final class ChannelMappingAudioProcessor extends BaseAudioProcessor { - private int channelCount; - private int sampleRateHz; - private int[] pendingOutputChannels; - - private boolean active; - private int[] outputChannels; - private ByteBuffer buffer; - private ByteBuffer outputBuffer; - private boolean inputEnded; + @Nullable private int[] pendingOutputChannels; + @Nullable private int[] outputChannels; /** - * Creates a new processor that applies a channel mapping. - */ - public ChannelMappingAudioProcessor() { - buffer = EMPTY_BUFFER; - outputBuffer = EMPTY_BUFFER; - channelCount = Format.NO_VALUE; - sampleRateHz = Format.NO_VALUE; - } - - /** - * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)} - * to start using the new channel map. + * Resets the channel mapping. After calling this method, call {@link #configure(AudioFormat)} to + * start using the new channel map. * - * @see AudioTrack#configure(String, int, int, int, int, int[]) + * @param outputChannels The mapping from input to output channel indices, or {@code null} to + * leave the input unchanged. + * @see AudioSink#configure(int, int, int, int, int[], int, int) */ - public void setChannelMap(int[] outputChannels) { + public void setChannelMap(@Nullable int[] outputChannels) { pendingOutputChannels = outputChannels; } @Override - public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding) - throws UnhandledFormatException { - boolean outputChannelsChanged = !Arrays.equals(pendingOutputChannels, outputChannels); - outputChannels = pendingOutputChannels; + public AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + @Nullable int[] outputChannels = pendingOutputChannels; if (outputChannels == null) { - active = false; - return outputChannelsChanged; + return AudioFormat.NOT_SET; } - if (encoding != C.ENCODING_PCM_16BIT) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); - } - if (!outputChannelsChanged && this.sampleRateHz == sampleRateHz - && this.channelCount == channelCount) { - return false; - } - this.sampleRateHz = sampleRateHz; - this.channelCount = channelCount; - active = channelCount != outputChannels.length; + if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) { + throw new UnhandledAudioFormatException(inputAudioFormat); + } + + boolean active = inputAudioFormat.channelCount != outputChannels.length; for (int i = 0; i < outputChannels.length; i++) { int channelIndex = outputChannels[i]; - if (channelIndex >= channelCount) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + if (channelIndex >= inputAudioFormat.channelCount) { + throw new UnhandledAudioFormatException(inputAudioFormat); } active |= (channelIndex != i); } - return true; - } - - @Override - public boolean isActive() { - return active; - } - - @Override - public int getOutputChannelCount() { - return outputChannels == null ? channelCount : outputChannels.length; - } - - @Override - public int getOutputEncoding() { - return C.ENCODING_PCM_16BIT; + return active + ? new AudioFormat(inputAudioFormat.sampleRate, outputChannels.length, C.ENCODING_PCM_16BIT) + : AudioFormat.NOT_SET; } @Override public void queueInput(ByteBuffer inputBuffer) { + int[] outputChannels = Assertions.checkNotNull(this.outputChannels); int position = inputBuffer.position(); int limit = inputBuffer.limit(); - int frameCount = (limit - position) / (2 * channelCount); - int outputSize = frameCount * outputChannels.length * 2; - if (buffer.capacity() < outputSize) { - buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder()); - } else { - buffer.clear(); - } + int frameCount = (limit - position) / inputAudioFormat.bytesPerFrame; + int outputSize = frameCount * outputAudioFormat.bytesPerFrame; + ByteBuffer buffer = replaceOutputBuffer(outputSize); while (position < limit) { for (int channelIndex : outputChannels) { buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex)); } - position += channelCount * 2; + position += inputAudioFormat.bytesPerFrame; } inputBuffer.position(limit); buffer.flip(); - outputBuffer = buffer; } @Override - public void queueEndOfStream() { - inputEnded = true; + protected void onFlush() { + outputChannels = pendingOutputChannels; } @Override - public ByteBuffer getOutput() { - ByteBuffer outputBuffer = this.outputBuffer; - this.outputBuffer = EMPTY_BUFFER; - return outputBuffer; - } - - @SuppressWarnings("ReferenceEquality") - @Override - public boolean isEnded() { - return inputEnded && outputBuffer == EMPTY_BUFFER; - } - - @Override - public void flush() { - outputBuffer = EMPTY_BUFFER; - inputEnded = false; - } - - @Override - public void reset() { - flush(); - buffer = EMPTY_BUFFER; - channelCount = Format.NO_VALUE; - sampleRateHz = Format.NO_VALUE; + protected void onReset() { outputChannels = null; - active = false; + pendingOutputChannels = null; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DefaultAudioSink.java new file mode 100644 index 000000000000..9fc3fbbfd84a --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -0,0 +1,1474 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.media.AudioFormat; +import android.media.AudioManager; +import android.media.AudioTrack; +import android.os.ConditionVariable; +import android.os.SystemClock; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioProcessor.UnhandledAudioFormatException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; + +/** + * Plays audio data. The implementation delegates to an {@link AudioTrack} and handles playback + * position smoothing, non-blocking writes and reconfiguration. + *

+ * If tunneling mode is enabled, care must be taken that audio processors do not output buffers with + * a different duration than their input, and buffer processors must produce output corresponding to + * their last input immediately after that input is queued. This means that, for example, speed + * adjustment is not possible while using tunneling. + */ +public final class DefaultAudioSink implements AudioSink { + + /** + * Thrown when the audio track has provided a spurious timestamp, if {@link + * #failOnSpuriousAudioTimestamp} is set. + */ + public static final class InvalidAudioTrackTimestampException extends RuntimeException { + + /** + * Creates a new invalid timestamp exception with the specified message. + * + * @param message The detail message for this exception. + */ + private InvalidAudioTrackTimestampException(String message) { + super(message); + } + + } + + /** + * Provides a chain of audio processors, which are used for any user-defined processing and + * applying playback parameters (if supported). Because applying playback parameters can skip and + * stretch/compress audio, the sink will query the chain for information on how to transform its + * output position to map it onto a media position, via {@link #getMediaDuration(long)} and {@link + * #getSkippedOutputFrameCount()}. + */ + public interface AudioProcessorChain { + + /** + * Returns the fixed chain of audio processors that will process audio. This method is called + * once during initialization, but audio processors may change state to become active/inactive + * during playback. + */ + AudioProcessor[] getAudioProcessors(); + + /** + * Configures audio processors to apply the specified playback parameters immediately, returning + * the new parameters, which may differ from those passed in. Only called when processors have + * no input pending. + * + * @param playbackParameters The playback parameters to try to apply. + * @return The playback parameters that were actually applied. + */ + PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters); + + /** + * Scales the specified playout duration to take into account speedup due to audio processing, + * returning an input media duration, in arbitrary units. + */ + long getMediaDuration(long playoutDuration); + + /** + * Returns the number of output audio frames skipped since the audio processors were last + * flushed. + */ + long getSkippedOutputFrameCount(); + } + + /** + * The default audio processor chain, which applies a (possibly empty) chain of user-defined audio + * processors followed by {@link SilenceSkippingAudioProcessor} and {@link SonicAudioProcessor}. + */ + public static class DefaultAudioProcessorChain implements AudioProcessorChain { + + private final AudioProcessor[] audioProcessors; + private final SilenceSkippingAudioProcessor silenceSkippingAudioProcessor; + private final SonicAudioProcessor sonicAudioProcessor; + + /** + * Creates a new default chain of audio processors, with the user-defined {@code + * audioProcessors} applied before silence skipping and playback parameters. + */ + public DefaultAudioProcessorChain(AudioProcessor... audioProcessors) { + // The passed-in type may be more specialized than AudioProcessor[], so allocate a new array + // rather than using Arrays.copyOf. + this.audioProcessors = new AudioProcessor[audioProcessors.length + 2]; + System.arraycopy( + /* src= */ audioProcessors, + /* srcPos= */ 0, + /* dest= */ this.audioProcessors, + /* destPos= */ 0, + /* length= */ audioProcessors.length); + silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); + sonicAudioProcessor = new SonicAudioProcessor(); + this.audioProcessors[audioProcessors.length] = silenceSkippingAudioProcessor; + this.audioProcessors[audioProcessors.length + 1] = sonicAudioProcessor; + } + + @Override + public AudioProcessor[] getAudioProcessors() { + return audioProcessors; + } + + @Override + public PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters) { + silenceSkippingAudioProcessor.setEnabled(playbackParameters.skipSilence); + return new PlaybackParameters( + sonicAudioProcessor.setSpeed(playbackParameters.speed), + sonicAudioProcessor.setPitch(playbackParameters.pitch), + playbackParameters.skipSilence); + } + + @Override + public long getMediaDuration(long playoutDuration) { + return sonicAudioProcessor.scaleDurationForSpeedup(playoutDuration); + } + + @Override + public long getSkippedOutputFrameCount() { + return silenceSkippingAudioProcessor.getSkippedFrames(); + } + } + + /** + * A minimum length for the {@link AudioTrack} buffer, in microseconds. + */ + private static final long MIN_BUFFER_DURATION_US = 250000; + /** + * A maximum length for the {@link AudioTrack} buffer, in microseconds. + */ + private static final long MAX_BUFFER_DURATION_US = 750000; + /** + * The length for passthrough {@link AudioTrack} buffers, in microseconds. + */ + private static final long PASSTHROUGH_BUFFER_DURATION_US = 250000; + /** + * A multiplication factor to apply to the minimum buffer size requested by the underlying + * {@link AudioTrack}. + */ + private static final int BUFFER_MULTIPLICATION_FACTOR = 4; + + /** To avoid underruns on some devices (e.g., Broadcom 7271), scale up the AC3 buffer duration. */ + private static final int AC3_BUFFER_MULTIPLICATION_FACTOR = 2; + + /** + * @see AudioTrack#ERROR_BAD_VALUE + */ + private static final int ERROR_BAD_VALUE = AudioTrack.ERROR_BAD_VALUE; + /** + * @see AudioTrack#MODE_STATIC + */ + private static final int MODE_STATIC = AudioTrack.MODE_STATIC; + /** + * @see AudioTrack#MODE_STREAM + */ + private static final int MODE_STREAM = AudioTrack.MODE_STREAM; + /** + * @see AudioTrack#STATE_INITIALIZED + */ + private static final int STATE_INITIALIZED = AudioTrack.STATE_INITIALIZED; + /** + * @see AudioTrack#WRITE_NON_BLOCKING + */ + @SuppressLint("InlinedApi") + private static final int WRITE_NON_BLOCKING = AudioTrack.WRITE_NON_BLOCKING; + + private static final String TAG = "AudioTrack"; + + /** Represents states of the {@link #startMediaTimeUs} value. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({START_NOT_SET, START_IN_SYNC, START_NEED_SYNC}) + private @interface StartMediaTimeState {} + + private static final int START_NOT_SET = 0; + private static final int START_IN_SYNC = 1; + private static final int START_NEED_SYNC = 2; + + /** + * Whether to enable a workaround for an issue where an audio effect does not keep its session + * active across releasing/initializing a new audio track, on platform builds where + * {@link Util#SDK_INT} < 21. + *

+ * The flag must be set before creating a player. + */ + public static boolean enablePreV21AudioSessionWorkaround = false; + + /** + * Whether to throw an {@link InvalidAudioTrackTimestampException} when a spurious timestamp is + * reported from {@link AudioTrack#getTimestamp}. + *

+ * The flag must be set before creating a player. Should be set to {@code true} for testing and + * debugging purposes only. + */ + public static boolean failOnSpuriousAudioTimestamp = false; + + @Nullable private final AudioCapabilities audioCapabilities; + private final AudioProcessorChain audioProcessorChain; + private final boolean enableFloatOutput; + private final ChannelMappingAudioProcessor channelMappingAudioProcessor; + private final TrimmingAudioProcessor trimmingAudioProcessor; + private final AudioProcessor[] toIntPcmAvailableAudioProcessors; + private final AudioProcessor[] toFloatPcmAvailableAudioProcessors; + private final ConditionVariable releasingConditionVariable; + private final AudioTrackPositionTracker audioTrackPositionTracker; + private final ArrayDeque playbackParametersCheckpoints; + + @Nullable private Listener listener; + /** Used to keep the audio session active on pre-V21 builds (see {@link #initialize(long)}). */ + @Nullable private AudioTrack keepSessionIdAudioTrack; + + @Nullable private Configuration pendingConfiguration; + private Configuration configuration; + private AudioTrack audioTrack; + + private AudioAttributes audioAttributes; + @Nullable private PlaybackParameters afterDrainPlaybackParameters; + private PlaybackParameters playbackParameters; + private long playbackParametersOffsetUs; + private long playbackParametersPositionUs; + + @Nullable private ByteBuffer avSyncHeader; + private int bytesUntilNextAvSync; + + private long submittedPcmBytes; + private long submittedEncodedFrames; + private long writtenPcmBytes; + private long writtenEncodedFrames; + private int framesPerEncodedSample; + private @StartMediaTimeState int startMediaTimeState; + private long startMediaTimeUs; + private float volume; + + private AudioProcessor[] activeAudioProcessors; + private ByteBuffer[] outputBuffers; + @Nullable private ByteBuffer inputBuffer; + @Nullable private ByteBuffer outputBuffer; + private byte[] preV21OutputBuffer; + private int preV21OutputBufferOffset; + private int drainingAudioProcessorIndex; + private boolean handledEndOfStream; + private boolean stoppedAudioTrack; + + private boolean playing; + private int audioSessionId; + private AuxEffectInfo auxEffectInfo; + private boolean tunneling; + private long lastFeedElapsedRealtimeMs; + + /** + * Creates a new default audio sink. + * + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before + * output. May be empty. + */ + public DefaultAudioSink( + @Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors) { + this(audioCapabilities, audioProcessors, /* enableFloatOutput= */ false); + } + + /** + * Creates a new default audio sink, optionally using float output for high resolution PCM. + * + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before + * output. May be empty. + * @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float + * output will be used if the input is 32-bit float, and also if the input is high resolution + * (24-bit or 32-bit) integer PCM. Audio processing (for example, speed adjustment) will not + * be available when float output is in use. + */ + public DefaultAudioSink( + @Nullable AudioCapabilities audioCapabilities, + AudioProcessor[] audioProcessors, + boolean enableFloatOutput) { + this(audioCapabilities, new DefaultAudioProcessorChain(audioProcessors), enableFloatOutput); + } + + /** + * Creates a new default audio sink, optionally using float output for high resolution PCM and + * with the specified {@code audioProcessorChain}. + * + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param audioProcessorChain An {@link AudioProcessorChain} which is used to apply playback + * parameters adjustments. The instance passed in must not be reused in other sinks. + * @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float + * output will be used if the input is 32-bit float, and also if the input is high resolution + * (24-bit or 32-bit) integer PCM. Audio processing (for example, speed adjustment) will not + * be available when float output is in use. + */ + public DefaultAudioSink( + @Nullable AudioCapabilities audioCapabilities, + AudioProcessorChain audioProcessorChain, + boolean enableFloatOutput) { + this.audioCapabilities = audioCapabilities; + this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain); + this.enableFloatOutput = enableFloatOutput; + releasingConditionVariable = new ConditionVariable(true); + audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); + channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); + trimmingAudioProcessor = new TrimmingAudioProcessor(); + ArrayList toIntPcmAudioProcessors = new ArrayList<>(); + Collections.addAll( + toIntPcmAudioProcessors, + new ResamplingAudioProcessor(), + channelMappingAudioProcessor, + trimmingAudioProcessor); + Collections.addAll(toIntPcmAudioProcessors, audioProcessorChain.getAudioProcessors()); + toIntPcmAvailableAudioProcessors = toIntPcmAudioProcessors.toArray(new AudioProcessor[0]); + toFloatPcmAvailableAudioProcessors = new AudioProcessor[] {new FloatResamplingAudioProcessor()}; + volume = 1.0f; + startMediaTimeState = START_NOT_SET; + audioAttributes = AudioAttributes.DEFAULT; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + auxEffectInfo = new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, 0f); + playbackParameters = PlaybackParameters.DEFAULT; + drainingAudioProcessorIndex = C.INDEX_UNSET; + activeAudioProcessors = new AudioProcessor[0]; + outputBuffers = new ByteBuffer[0]; + playbackParametersCheckpoints = new ArrayDeque<>(); + } + + // AudioSink implementation. + + @Override + public void setListener(Listener listener) { + this.listener = listener; + } + + @Override + public boolean supportsOutput(int channelCount, @C.Encoding int encoding) { + if (Util.isEncodingLinearPcm(encoding)) { + // AudioTrack supports 16-bit integer PCM output in all platform API versions, and float + // output from platform API version 21 only. Other integer PCM encodings are resampled by this + // sink to 16-bit PCM. We assume that the audio framework will downsample any number of + // channels to the output device's required number of channels. + return encoding != C.ENCODING_PCM_FLOAT || Util.SDK_INT >= 21; + } else { + return audioCapabilities != null + && audioCapabilities.supportsEncoding(encoding) + && (channelCount == Format.NO_VALUE + || channelCount <= audioCapabilities.getMaxChannelCount()); + } + } + + @Override + public long getCurrentPositionUs(boolean sourceEnded) { + if (!isInitialized() || startMediaTimeState == START_NOT_SET) { + return CURRENT_POSITION_NOT_SET; + } + long positionUs = audioTrackPositionTracker.getCurrentPositionUs(sourceEnded); + positionUs = Math.min(positionUs, configuration.framesToDurationUs(getWrittenFrames())); + return startMediaTimeUs + applySkipping(applySpeedup(positionUs)); + } + + @Override + public void configure( + @C.Encoding int inputEncoding, + int inputChannelCount, + int inputSampleRate, + int specifiedBufferSize, + @Nullable int[] outputChannels, + int trimStartFrames, + int trimEndFrames) + throws ConfigurationException { + if (Util.SDK_INT < 21 && inputChannelCount == 8 && outputChannels == null) { + // AudioTrack doesn't support 8 channel output before Android L. Discard the last two (side) + // channels to give a 6 channel stream that is supported. + outputChannels = new int[6]; + for (int i = 0; i < outputChannels.length; i++) { + outputChannels[i] = i; + } + } + + boolean isInputPcm = Util.isEncodingLinearPcm(inputEncoding); + boolean processingEnabled = isInputPcm; + int sampleRate = inputSampleRate; + int channelCount = inputChannelCount; + @C.Encoding int encoding = inputEncoding; + boolean useFloatOutput = + enableFloatOutput + && supportsOutput(inputChannelCount, C.ENCODING_PCM_FLOAT) + && Util.isEncodingHighResolutionPcm(inputEncoding); + AudioProcessor[] availableAudioProcessors = + useFloatOutput ? toFloatPcmAvailableAudioProcessors : toIntPcmAvailableAudioProcessors; + if (processingEnabled) { + trimmingAudioProcessor.setTrimFrameCount(trimStartFrames, trimEndFrames); + channelMappingAudioProcessor.setChannelMap(outputChannels); + AudioProcessor.AudioFormat outputFormat = + new AudioProcessor.AudioFormat(sampleRate, channelCount, encoding); + for (AudioProcessor audioProcessor : availableAudioProcessors) { + try { + AudioProcessor.AudioFormat nextFormat = audioProcessor.configure(outputFormat); + if (audioProcessor.isActive()) { + outputFormat = nextFormat; + } + } catch (UnhandledAudioFormatException e) { + throw new ConfigurationException(e); + } + } + sampleRate = outputFormat.sampleRate; + channelCount = outputFormat.channelCount; + encoding = outputFormat.encoding; + } + + int outputChannelConfig = getChannelConfig(channelCount, isInputPcm); + if (outputChannelConfig == AudioFormat.CHANNEL_INVALID) { + throw new ConfigurationException("Unsupported channel count: " + channelCount); + } + + int inputPcmFrameSize = + isInputPcm ? Util.getPcmFrameSize(inputEncoding, inputChannelCount) : C.LENGTH_UNSET; + int outputPcmFrameSize = + isInputPcm ? Util.getPcmFrameSize(encoding, channelCount) : C.LENGTH_UNSET; + boolean canApplyPlaybackParameters = processingEnabled && !useFloatOutput; + Configuration pendingConfiguration = + new Configuration( + isInputPcm, + inputPcmFrameSize, + inputSampleRate, + outputPcmFrameSize, + sampleRate, + outputChannelConfig, + encoding, + specifiedBufferSize, + processingEnabled, + canApplyPlaybackParameters, + availableAudioProcessors); + if (isInitialized()) { + this.pendingConfiguration = pendingConfiguration; + } else { + configuration = pendingConfiguration; + } + } + + private void setupAudioProcessors() { + AudioProcessor[] audioProcessors = configuration.availableAudioProcessors; + ArrayList newAudioProcessors = new ArrayList<>(); + for (AudioProcessor audioProcessor : audioProcessors) { + if (audioProcessor.isActive()) { + newAudioProcessors.add(audioProcessor); + } else { + audioProcessor.flush(); + } + } + int count = newAudioProcessors.size(); + activeAudioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]); + outputBuffers = new ByteBuffer[count]; + flushAudioProcessors(); + } + + private void flushAudioProcessors() { + for (int i = 0; i < activeAudioProcessors.length; i++) { + AudioProcessor audioProcessor = activeAudioProcessors[i]; + audioProcessor.flush(); + outputBuffers[i] = audioProcessor.getOutput(); + } + } + + private void initialize(long presentationTimeUs) throws InitializationException { + // If we're asynchronously releasing a previous audio track then we block 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(); + + audioTrack = + Assertions.checkNotNull(configuration) + .buildAudioTrack(tunneling, audioAttributes, audioSessionId); + int audioSessionId = audioTrack.getAudioSessionId(); + if (enablePreV21AudioSessionWorkaround) { + if (Util.SDK_INT < 21) { + // The workaround creates an audio track with a two byte buffer on the same session, and + // does not release it until this object is released, which keeps the session active. + if (keepSessionIdAudioTrack != null + && audioSessionId != keepSessionIdAudioTrack.getAudioSessionId()) { + releaseKeepSessionIdAudioTrack(); + } + if (keepSessionIdAudioTrack == null) { + keepSessionIdAudioTrack = initializeKeepSessionIdAudioTrack(audioSessionId); + } + } + } + if (this.audioSessionId != audioSessionId) { + this.audioSessionId = audioSessionId; + if (listener != null) { + listener.onAudioSessionId(audioSessionId); + } + } + + applyPlaybackParameters(playbackParameters, presentationTimeUs); + + audioTrackPositionTracker.setAudioTrack( + audioTrack, + configuration.outputEncoding, + configuration.outputPcmFrameSize, + configuration.bufferSize); + setVolumeInternal(); + + if (auxEffectInfo.effectId != AuxEffectInfo.NO_AUX_EFFECT_ID) { + audioTrack.attachAuxEffect(auxEffectInfo.effectId); + audioTrack.setAuxEffectSendLevel(auxEffectInfo.sendLevel); + } + } + + @Override + public void play() { + playing = true; + if (isInitialized()) { + audioTrackPositionTracker.start(); + audioTrack.play(); + } + } + + @Override + public void handleDiscontinuity() { + // Force resynchronization after a skipped buffer. + if (startMediaTimeState == START_IN_SYNC) { + startMediaTimeState = START_NEED_SYNC; + } + } + + @Override + @SuppressWarnings("ReferenceEquality") + public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) + throws InitializationException, WriteException { + Assertions.checkArgument(inputBuffer == null || buffer == inputBuffer); + + if (pendingConfiguration != null) { + if (!drainAudioProcessorsToEndOfStream()) { + // There's still pending data in audio processors to write to the track. + return false; + } else if (!pendingConfiguration.canReuseAudioTrack(configuration)) { + playPendingData(); + if (hasPendingData()) { + // We're waiting for playout on the current audio track to finish. + return false; + } + flush(); + } else { + // The current audio track can be reused for the new configuration. + configuration = pendingConfiguration; + pendingConfiguration = null; + } + // Re-apply playback parameters. + applyPlaybackParameters(playbackParameters, presentationTimeUs); + } + + if (!isInitialized()) { + initialize(presentationTimeUs); + if (playing) { + play(); + } + } + + if (!audioTrackPositionTracker.mayHandleBuffer(getWrittenFrames())) { + return false; + } + + if (inputBuffer == null) { + // We are seeing this buffer for the first time. + if (!buffer.hasRemaining()) { + // The buffer is empty. + return true; + } + + if (!configuration.isInputPcm && framesPerEncodedSample == 0) { + // If this is the first encoded sample, calculate the sample size in frames. + framesPerEncodedSample = getFramesPerEncodedSample(configuration.outputEncoding, buffer); + if (framesPerEncodedSample == 0) { + // We still don't know the number of frames per sample, so drop the buffer. + // For TrueHD this can occur after some seek operations, as not every sample starts with + // a syncframe header. If we chunked samples together so the extracted samples always + // started with a syncframe header, the chunks would be too large. + return true; + } + } + + if (afterDrainPlaybackParameters != null) { + if (!drainAudioProcessorsToEndOfStream()) { + // Don't process any more input until draining completes. + return false; + } + PlaybackParameters newPlaybackParameters = afterDrainPlaybackParameters; + afterDrainPlaybackParameters = null; + applyPlaybackParameters(newPlaybackParameters, presentationTimeUs); + } + + if (startMediaTimeState == START_NOT_SET) { + startMediaTimeUs = Math.max(0, presentationTimeUs); + startMediaTimeState = START_IN_SYNC; + } else { + // Sanity check that presentationTimeUs is consistent with the expected value. + long expectedPresentationTimeUs = + startMediaTimeUs + + configuration.inputFramesToDurationUs( + getSubmittedFrames() - trimmingAudioProcessor.getTrimmedFrameCount()); + if (startMediaTimeState == START_IN_SYNC + && Math.abs(expectedPresentationTimeUs - presentationTimeUs) > 200000) { + Log.e(TAG, "Discontinuity detected [expected " + expectedPresentationTimeUs + ", got " + + presentationTimeUs + "]"); + startMediaTimeState = START_NEED_SYNC; + } + if (startMediaTimeState == START_NEED_SYNC) { + // Adjust startMediaTimeUs to be consistent with the current buffer's start time and the + // number of bytes submitted. + long adjustmentUs = presentationTimeUs - expectedPresentationTimeUs; + startMediaTimeUs += adjustmentUs; + startMediaTimeState = START_IN_SYNC; + if (listener != null && adjustmentUs != 0) { + listener.onPositionDiscontinuity(); + } + } + } + + if (configuration.isInputPcm) { + submittedPcmBytes += buffer.remaining(); + } else { + submittedEncodedFrames += framesPerEncodedSample; + } + + inputBuffer = buffer; + } + + if (configuration.processingEnabled) { + processBuffers(presentationTimeUs); + } else { + writeBuffer(inputBuffer, presentationTimeUs); + } + + if (!inputBuffer.hasRemaining()) { + inputBuffer = null; + return true; + } + + if (audioTrackPositionTracker.isStalled(getWrittenFrames())) { + Log.w(TAG, "Resetting stalled audio track"); + flush(); + return true; + } + + return false; + } + + private void processBuffers(long avSyncPresentationTimeUs) throws WriteException { + int count = activeAudioProcessors.length; + int index = count; + while (index >= 0) { + ByteBuffer input = index > 0 ? outputBuffers[index - 1] + : (inputBuffer != null ? inputBuffer : AudioProcessor.EMPTY_BUFFER); + if (index == count) { + writeBuffer(input, avSyncPresentationTimeUs); + } else { + AudioProcessor audioProcessor = activeAudioProcessors[index]; + audioProcessor.queueInput(input); + ByteBuffer output = audioProcessor.getOutput(); + outputBuffers[index] = output; + if (output.hasRemaining()) { + // Handle the output as input to the next audio processor or the AudioTrack. + index++; + continue; + } + } + + if (input.hasRemaining()) { + // The input wasn't consumed and no output was produced, so give up for now. + return; + } + + // Get more input from upstream. + index--; + } + } + + @SuppressWarnings("ReferenceEquality") + private void writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) throws WriteException { + if (!buffer.hasRemaining()) { + return; + } + if (outputBuffer != null) { + Assertions.checkArgument(outputBuffer == buffer); + } else { + outputBuffer = buffer; + if (Util.SDK_INT < 21) { + int bytesRemaining = buffer.remaining(); + if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) { + preV21OutputBuffer = new byte[bytesRemaining]; + } + int originalPosition = buffer.position(); + buffer.get(preV21OutputBuffer, 0, bytesRemaining); + buffer.position(originalPosition); + preV21OutputBufferOffset = 0; + } + } + int bytesRemaining = buffer.remaining(); + int bytesWritten = 0; + if (Util.SDK_INT < 21) { // isInputPcm == true + // Work out how many bytes we can write without the risk of blocking. + int bytesToWrite = audioTrackPositionTracker.getAvailableBufferSize(writtenPcmBytes); + if (bytesToWrite > 0) { + bytesToWrite = Math.min(bytesRemaining, bytesToWrite); + bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite); + if (bytesWritten > 0) { + preV21OutputBufferOffset += bytesWritten; + buffer.position(buffer.position() + bytesWritten); + } + } + } else if (tunneling) { + Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET); + bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, + avSyncPresentationTimeUs); + } else { + bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining); + } + + lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); + + if (bytesWritten < 0) { + throw new WriteException(bytesWritten); + } + + if (configuration.isInputPcm) { + writtenPcmBytes += bytesWritten; + } + if (bytesWritten == bytesRemaining) { + if (!configuration.isInputPcm) { + writtenEncodedFrames += framesPerEncodedSample; + } + outputBuffer = null; + } + } + + @Override + public void playToEndOfStream() throws WriteException { + if (!handledEndOfStream && isInitialized() && drainAudioProcessorsToEndOfStream()) { + playPendingData(); + handledEndOfStream = true; + } + } + + private boolean drainAudioProcessorsToEndOfStream() throws WriteException { + boolean audioProcessorNeedsEndOfStream = false; + if (drainingAudioProcessorIndex == C.INDEX_UNSET) { + drainingAudioProcessorIndex = + configuration.processingEnabled ? 0 : activeAudioProcessors.length; + audioProcessorNeedsEndOfStream = true; + } + while (drainingAudioProcessorIndex < activeAudioProcessors.length) { + AudioProcessor audioProcessor = activeAudioProcessors[drainingAudioProcessorIndex]; + if (audioProcessorNeedsEndOfStream) { + audioProcessor.queueEndOfStream(); + } + processBuffers(C.TIME_UNSET); + if (!audioProcessor.isEnded()) { + return false; + } + audioProcessorNeedsEndOfStream = true; + drainingAudioProcessorIndex++; + } + + // Finish writing any remaining output to the track. + if (outputBuffer != null) { + writeBuffer(outputBuffer, C.TIME_UNSET); + if (outputBuffer != null) { + return false; + } + } + drainingAudioProcessorIndex = C.INDEX_UNSET; + return true; + } + + @Override + public boolean isEnded() { + return !isInitialized() || (handledEndOfStream && !hasPendingData()); + } + + @Override + public boolean hasPendingData() { + return isInitialized() && audioTrackPositionTracker.hasPendingData(getWrittenFrames()); + } + + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + if (configuration != null && !configuration.canApplyPlaybackParameters) { + this.playbackParameters = PlaybackParameters.DEFAULT; + return; + } + PlaybackParameters lastSetPlaybackParameters = getPlaybackParameters(); + if (!playbackParameters.equals(lastSetPlaybackParameters)) { + if (isInitialized()) { + // Drain the audio processors so we can determine the frame position at which the new + // parameters apply. + afterDrainPlaybackParameters = playbackParameters; + } else { + // Update the playback parameters now. They will be applied to the audio processors during + // initialization. + this.playbackParameters = playbackParameters; + } + } + } + + @Override + public PlaybackParameters getPlaybackParameters() { + // Mask the already set parameters. + return afterDrainPlaybackParameters != null + ? afterDrainPlaybackParameters + : !playbackParametersCheckpoints.isEmpty() + ? playbackParametersCheckpoints.getLast().playbackParameters + : playbackParameters; + } + + @Override + public void setAudioAttributes(AudioAttributes audioAttributes) { + if (this.audioAttributes.equals(audioAttributes)) { + return; + } + this.audioAttributes = audioAttributes; + if (tunneling) { + // The audio attributes are ignored in tunneling mode, so no need to reset. + return; + } + flush(); + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + } + + @Override + public void setAudioSessionId(int audioSessionId) { + if (this.audioSessionId != audioSessionId) { + this.audioSessionId = audioSessionId; + flush(); + } + } + + @Override + public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) { + if (this.auxEffectInfo.equals(auxEffectInfo)) { + return; + } + int effectId = auxEffectInfo.effectId; + float sendLevel = auxEffectInfo.sendLevel; + if (audioTrack != null) { + if (this.auxEffectInfo.effectId != effectId) { + audioTrack.attachAuxEffect(effectId); + } + if (effectId != AuxEffectInfo.NO_AUX_EFFECT_ID) { + audioTrack.setAuxEffectSendLevel(sendLevel); + } + } + this.auxEffectInfo = auxEffectInfo; + } + + @Override + public void enableTunnelingV21(int tunnelingAudioSessionId) { + Assertions.checkState(Util.SDK_INT >= 21); + if (!tunneling || audioSessionId != tunnelingAudioSessionId) { + tunneling = true; + audioSessionId = tunnelingAudioSessionId; + flush(); + } + } + + @Override + public void disableTunneling() { + if (tunneling) { + tunneling = false; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + flush(); + } + } + + @Override + public void setVolume(float volume) { + if (this.volume != volume) { + this.volume = volume; + setVolumeInternal(); + } + } + + private void setVolumeInternal() { + if (!isInitialized()) { + // Do nothing. + } else if (Util.SDK_INT >= 21) { + setVolumeInternalV21(audioTrack, volume); + } else { + setVolumeInternalV3(audioTrack, volume); + } + } + + @Override + public void pause() { + playing = false; + if (isInitialized() && audioTrackPositionTracker.pause()) { + audioTrack.pause(); + } + } + + @Override + public void flush() { + if (isInitialized()) { + submittedPcmBytes = 0; + submittedEncodedFrames = 0; + writtenPcmBytes = 0; + writtenEncodedFrames = 0; + framesPerEncodedSample = 0; + if (afterDrainPlaybackParameters != null) { + playbackParameters = afterDrainPlaybackParameters; + afterDrainPlaybackParameters = null; + } else if (!playbackParametersCheckpoints.isEmpty()) { + playbackParameters = playbackParametersCheckpoints.getLast().playbackParameters; + } + playbackParametersCheckpoints.clear(); + playbackParametersOffsetUs = 0; + playbackParametersPositionUs = 0; + trimmingAudioProcessor.resetTrimmedFrameCount(); + flushAudioProcessors(); + inputBuffer = null; + outputBuffer = null; + stoppedAudioTrack = false; + handledEndOfStream = false; + drainingAudioProcessorIndex = C.INDEX_UNSET; + avSyncHeader = null; + bytesUntilNextAvSync = 0; + startMediaTimeState = START_NOT_SET; + if (audioTrackPositionTracker.isPlaying()) { + audioTrack.pause(); + } + // AudioTrack.release can take some time, so we call it on a background thread. + final AudioTrack toRelease = audioTrack; + audioTrack = null; + if (pendingConfiguration != null) { + configuration = pendingConfiguration; + pendingConfiguration = null; + } + audioTrackPositionTracker.reset(); + releasingConditionVariable.close(); + new Thread() { + @Override + public void run() { + try { + toRelease.flush(); + toRelease.release(); + } finally { + releasingConditionVariable.open(); + } + } + }.start(); + } + } + + @Override + public void reset() { + flush(); + releaseKeepSessionIdAudioTrack(); + for (AudioProcessor audioProcessor : toIntPcmAvailableAudioProcessors) { + audioProcessor.reset(); + } + for (AudioProcessor audioProcessor : toFloatPcmAvailableAudioProcessors) { + audioProcessor.reset(); + } + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + playing = false; + } + + /** + * Releases {@link #keepSessionIdAudioTrack} asynchronously, if it is non-{@code null}. + */ + private void releaseKeepSessionIdAudioTrack() { + if (keepSessionIdAudioTrack == null) { + return; + } + + // AudioTrack.release can take some time, so we call it on a background thread. + final AudioTrack toRelease = keepSessionIdAudioTrack; + keepSessionIdAudioTrack = null; + new Thread() { + @Override + public void run() { + toRelease.release(); + } + }.start(); + } + + private void applyPlaybackParameters( + PlaybackParameters playbackParameters, long presentationTimeUs) { + PlaybackParameters newPlaybackParameters = + configuration.canApplyPlaybackParameters + ? audioProcessorChain.applyPlaybackParameters(playbackParameters) + : PlaybackParameters.DEFAULT; + // Store the position and corresponding media time from which the parameters will apply. + playbackParametersCheckpoints.add( + new PlaybackParametersCheckpoint( + newPlaybackParameters, + /* mediaTimeUs= */ Math.max(0, presentationTimeUs), + /* positionUs= */ configuration.framesToDurationUs(getWrittenFrames()))); + setupAudioProcessors(); + } + + private long applySpeedup(long positionUs) { + @Nullable PlaybackParametersCheckpoint checkpoint = null; + while (!playbackParametersCheckpoints.isEmpty() + && positionUs >= playbackParametersCheckpoints.getFirst().positionUs) { + checkpoint = playbackParametersCheckpoints.remove(); + } + if (checkpoint != null) { + // We are playing (or about to play) media with the new playback parameters, so update them. + playbackParameters = checkpoint.playbackParameters; + playbackParametersPositionUs = checkpoint.positionUs; + playbackParametersOffsetUs = checkpoint.mediaTimeUs - startMediaTimeUs; + } + + if (playbackParameters.speed == 1f) { + return positionUs + playbackParametersOffsetUs - playbackParametersPositionUs; + } + + if (playbackParametersCheckpoints.isEmpty()) { + return playbackParametersOffsetUs + + audioProcessorChain.getMediaDuration(positionUs - playbackParametersPositionUs); + } + + // We are playing data at a previous playback speed, so fall back to multiplying by the speed. + return playbackParametersOffsetUs + + Util.getMediaDurationForPlayoutDuration( + positionUs - playbackParametersPositionUs, playbackParameters.speed); + } + + private long applySkipping(long positionUs) { + return positionUs + + configuration.framesToDurationUs(audioProcessorChain.getSkippedOutputFrameCount()); + } + + private boolean isInitialized() { + return audioTrack != null; + } + + private long getSubmittedFrames() { + return configuration.isInputPcm + ? (submittedPcmBytes / configuration.inputPcmFrameSize) + : submittedEncodedFrames; + } + + private long getWrittenFrames() { + return configuration.isInputPcm + ? (writtenPcmBytes / configuration.outputPcmFrameSize) + : writtenEncodedFrames; + } + + private static AudioTrack initializeKeepSessionIdAudioTrack(int audioSessionId) { + int sampleRate = 4000; // Equal to private AudioTrack.MIN_SAMPLE_RATE. + int channelConfig = AudioFormat.CHANNEL_OUT_MONO; + @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT; + int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback. + return new AudioTrack(C.STREAM_TYPE_DEFAULT, sampleRate, channelConfig, encoding, bufferSize, + MODE_STATIC, audioSessionId); + } + + private static int getChannelConfig(int channelCount, boolean isInputPcm) { + if (Util.SDK_INT <= 28 && !isInputPcm) { + // In passthrough mode the channel count used to configure the audio track doesn't affect how + // the stream is handled, except that some devices do overly-strict channel configuration + // checks. Therefore we override the channel count so that a known-working channel + // configuration is chosen in all cases. See [Internal: b/29116190]. + if (channelCount == 7) { + channelCount = 8; + } else if (channelCount == 3 || channelCount == 4 || channelCount == 5) { + channelCount = 6; + } + } + + // Workaround for Nexus Player not reporting support for mono passthrough. + // (See [Internal: b/34268671].) + if (Util.SDK_INT <= 26 && "fugu".equals(Util.DEVICE) && !isInputPcm && channelCount == 1) { + channelCount = 2; + } + + return Util.getAudioTrackChannelConfig(channelCount); + } + + private static int getMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) { + switch (encoding) { + case C.ENCODING_AC3: + return 640 * 1000 / 8; + case C.ENCODING_E_AC3: + case C.ENCODING_E_AC3_JOC: + return 6144 * 1000 / 8; + case C.ENCODING_AC4: + return 2688 * 1000 / 8; + case C.ENCODING_DTS: + // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. + return 1536 * 1000 / 8; + case C.ENCODING_DTS_HD: + return 18000 * 1000 / 8; + case C.ENCODING_DOLBY_TRUEHD: + return 24500 * 1000 / 8; + case C.ENCODING_INVALID: + case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_24BIT: + case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_8BIT: + case C.ENCODING_PCM_FLOAT: + case Format.NO_VALUE: + default: + throw new IllegalArgumentException(); + } + } + + private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) { + switch (encoding) { + case C.ENCODING_MP3: + return MpegAudioHeader.getFrameSampleCount(buffer.get(buffer.position())); + case C.ENCODING_DTS: + case C.ENCODING_DTS_HD: + return DtsUtil.parseDtsAudioSampleCount(buffer); + case C.ENCODING_AC3: + case C.ENCODING_E_AC3: + case C.ENCODING_E_AC3_JOC: + return Ac3Util.parseAc3SyncframeAudioSampleCount(buffer); + case C.ENCODING_AC4: + return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); + case C.ENCODING_DOLBY_TRUEHD: + int syncframeOffset = Ac3Util.findTrueHdSyncframeOffset(buffer); + return syncframeOffset == C.INDEX_UNSET + ? 0 + : (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset) + * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT); + default: + throw new IllegalStateException("Unexpected audio encoding: " + encoding); + } + } + + @TargetApi(21) + private static int writeNonBlockingV21(AudioTrack audioTrack, ByteBuffer buffer, int size) { + return audioTrack.write(buffer, size, WRITE_NON_BLOCKING); + } + + @TargetApi(21) + private int writeNonBlockingWithAvSyncV21(AudioTrack audioTrack, ByteBuffer buffer, int size, + long presentationTimeUs) { + if (Util.SDK_INT >= 26) { + // The underlying platform AudioTrack writes AV sync headers directly. + return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000); + } + if (avSyncHeader == null) { + avSyncHeader = ByteBuffer.allocate(16); + avSyncHeader.order(ByteOrder.BIG_ENDIAN); + avSyncHeader.putInt(0x55550001); + } + if (bytesUntilNextAvSync == 0) { + avSyncHeader.putInt(4, size); + avSyncHeader.putLong(8, presentationTimeUs * 1000); + avSyncHeader.position(0); + bytesUntilNextAvSync = size; + } + int avSyncHeaderBytesRemaining = avSyncHeader.remaining(); + if (avSyncHeaderBytesRemaining > 0) { + int result = audioTrack.write(avSyncHeader, avSyncHeaderBytesRemaining, WRITE_NON_BLOCKING); + if (result < 0) { + bytesUntilNextAvSync = 0; + return result; + } + if (result < avSyncHeaderBytesRemaining) { + return 0; + } + } + int result = writeNonBlockingV21(audioTrack, buffer, size); + if (result < 0) { + bytesUntilNextAvSync = 0; + return result; + } + bytesUntilNextAvSync -= result; + return result; + } + + @TargetApi(21) + private static void setVolumeInternalV21(AudioTrack audioTrack, float volume) { + audioTrack.setVolume(volume); + } + + private static void setVolumeInternalV3(AudioTrack audioTrack, float volume) { + audioTrack.setStereoVolume(volume, volume); + } + + private void playPendingData() { + if (!stoppedAudioTrack) { + stoppedAudioTrack = true; + audioTrackPositionTracker.handleEndOfStream(getWrittenFrames()); + audioTrack.stop(); + bytesUntilNextAvSync = 0; + } + } + + /** Stores playback parameters with the position and media time at which they apply. */ + private static final class PlaybackParametersCheckpoint { + + private final PlaybackParameters playbackParameters; + private final long mediaTimeUs; + private final long positionUs; + + private PlaybackParametersCheckpoint(PlaybackParameters playbackParameters, long mediaTimeUs, + long positionUs) { + this.playbackParameters = playbackParameters; + this.mediaTimeUs = mediaTimeUs; + this.positionUs = positionUs; + } + + } + + private final class PositionTrackerListener implements AudioTrackPositionTracker.Listener { + + @Override + public void onPositionFramesMismatch( + long audioTimestampPositionFrames, + long audioTimestampSystemTimeUs, + long systemTimeUs, + long playbackPositionUs) { + String message = + "Spurious audio timestamp (frame position mismatch): " + + audioTimestampPositionFrames + + ", " + + audioTimestampSystemTimeUs + + ", " + + systemTimeUs + + ", " + + playbackPositionUs + + ", " + + getSubmittedFrames() + + ", " + + getWrittenFrames(); + if (failOnSpuriousAudioTimestamp) { + throw new InvalidAudioTrackTimestampException(message); + } + Log.w(TAG, message); + } + + @Override + public void onSystemTimeUsMismatch( + long audioTimestampPositionFrames, + long audioTimestampSystemTimeUs, + long systemTimeUs, + long playbackPositionUs) { + String message = + "Spurious audio timestamp (system clock mismatch): " + + audioTimestampPositionFrames + + ", " + + audioTimestampSystemTimeUs + + ", " + + systemTimeUs + + ", " + + playbackPositionUs + + ", " + + getSubmittedFrames() + + ", " + + getWrittenFrames(); + if (failOnSpuriousAudioTimestamp) { + throw new InvalidAudioTrackTimestampException(message); + } + Log.w(TAG, message); + } + + @Override + public void onInvalidLatency(long latencyUs) { + Log.w(TAG, "Ignoring impossibly large audio latency: " + latencyUs); + } + + @Override + public void onUnderrun(int bufferSize, long bufferSizeMs) { + if (listener != null) { + long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; + listener.onUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + } + } + } + + /** Stores configuration relating to the audio format. */ + private static final class Configuration { + + public final boolean isInputPcm; + public final int inputPcmFrameSize; + public final int inputSampleRate; + public final int outputPcmFrameSize; + public final int outputSampleRate; + public final int outputChannelConfig; + @C.Encoding public final int outputEncoding; + public final int bufferSize; + public final boolean processingEnabled; + public final boolean canApplyPlaybackParameters; + public final AudioProcessor[] availableAudioProcessors; + + public Configuration( + boolean isInputPcm, + int inputPcmFrameSize, + int inputSampleRate, + int outputPcmFrameSize, + int outputSampleRate, + int outputChannelConfig, + int outputEncoding, + int specifiedBufferSize, + boolean processingEnabled, + boolean canApplyPlaybackParameters, + AudioProcessor[] availableAudioProcessors) { + this.isInputPcm = isInputPcm; + this.inputPcmFrameSize = inputPcmFrameSize; + this.inputSampleRate = inputSampleRate; + this.outputPcmFrameSize = outputPcmFrameSize; + this.outputSampleRate = outputSampleRate; + this.outputChannelConfig = outputChannelConfig; + this.outputEncoding = outputEncoding; + this.bufferSize = specifiedBufferSize != 0 ? specifiedBufferSize : getDefaultBufferSize(); + this.processingEnabled = processingEnabled; + this.canApplyPlaybackParameters = canApplyPlaybackParameters; + this.availableAudioProcessors = availableAudioProcessors; + } + + public boolean canReuseAudioTrack(Configuration audioTrackConfiguration) { + return audioTrackConfiguration.outputEncoding == outputEncoding + && audioTrackConfiguration.outputSampleRate == outputSampleRate + && audioTrackConfiguration.outputChannelConfig == outputChannelConfig; + } + + public long inputFramesToDurationUs(long frameCount) { + return (frameCount * C.MICROS_PER_SECOND) / inputSampleRate; + } + + public long framesToDurationUs(long frameCount) { + return (frameCount * C.MICROS_PER_SECOND) / outputSampleRate; + } + + public long durationUsToFrames(long durationUs) { + return (durationUs * outputSampleRate) / C.MICROS_PER_SECOND; + } + + public AudioTrack buildAudioTrack( + boolean tunneling, AudioAttributes audioAttributes, int audioSessionId) + throws InitializationException { + AudioTrack audioTrack; + if (Util.SDK_INT >= 21) { + audioTrack = createAudioTrackV21(tunneling, audioAttributes, audioSessionId); + } else { + int streamType = Util.getStreamTypeForAudioUsage(audioAttributes.usage); + if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { + audioTrack = + new AudioTrack( + streamType, + outputSampleRate, + outputChannelConfig, + outputEncoding, + bufferSize, + MODE_STREAM); + } else { + // Re-attach to the same audio session. + audioTrack = + new AudioTrack( + streamType, + outputSampleRate, + outputChannelConfig, + outputEncoding, + bufferSize, + MODE_STREAM, + audioSessionId); + } + } + + int state = audioTrack.getState(); + if (state != STATE_INITIALIZED) { + try { + audioTrack.release(); + } catch (Exception e) { + // The track has already failed to initialize, so it wouldn't be that surprising if + // release were to fail too. Swallow the exception. + } + throw new InitializationException(state, outputSampleRate, outputChannelConfig, bufferSize); + } + return audioTrack; + } + + @TargetApi(21) + private AudioTrack createAudioTrackV21( + boolean tunneling, AudioAttributes audioAttributes, int audioSessionId) { + android.media.AudioAttributes attributes; + if (tunneling) { + attributes = + new android.media.AudioAttributes.Builder() + .setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE) + .setFlags(android.media.AudioAttributes.FLAG_HW_AV_SYNC) + .setUsage(android.media.AudioAttributes.USAGE_MEDIA) + .build(); + } else { + attributes = audioAttributes.getAudioAttributesV21(); + } + AudioFormat format = + new AudioFormat.Builder() + .setChannelMask(outputChannelConfig) + .setEncoding(outputEncoding) + .setSampleRate(outputSampleRate) + .build(); + return new AudioTrack( + attributes, + format, + bufferSize, + MODE_STREAM, + audioSessionId != C.AUDIO_SESSION_ID_UNSET + ? audioSessionId + : AudioManager.AUDIO_SESSION_ID_GENERATE); + } + + private int getDefaultBufferSize() { + if (isInputPcm) { + int minBufferSize = + AudioTrack.getMinBufferSize(outputSampleRate, outputChannelConfig, outputEncoding); + Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); + int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; + int minAppBufferSize = + (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize; + int maxAppBufferSize = + (int) + Math.max( + minBufferSize, durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize); + return Util.constrainValue(multipliedBufferSize, minAppBufferSize, maxAppBufferSize); + } else { + int rate = getMaximumEncodedRateBytesPerSecond(outputEncoding); + if (outputEncoding == C.ENCODING_AC3) { + rate *= AC3_BUFFER_MULTIPLICATION_FACTOR; + } + return (int) (PASSTHROUGH_BUFFER_DURATION_US * rate / C.MICROS_PER_SECOND); + } + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DtsUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DtsUtil.java index ef310accfa69..6e5d749fdffd 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DtsUtil.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DtsUtil.java @@ -15,17 +15,28 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.audio; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; import java.nio.ByteBuffer; +import java.util.Arrays; /** * Utility methods for parsing DTS frames. */ public final class DtsUtil { + private static final int SYNC_VALUE_BE = 0x7FFE8001; + private static final int SYNC_VALUE_14B_BE = 0x1FFFE800; + private static final int SYNC_VALUE_LE = 0xFE7F0180; + private static final int SYNC_VALUE_14B_LE = 0xFF1F00E8; + private static final byte FIRST_BYTE_BE = (byte) (SYNC_VALUE_BE >>> 24); + private static final byte FIRST_BYTE_14B_BE = (byte) (SYNC_VALUE_14B_BE >>> 24); + private static final byte FIRST_BYTE_LE = (byte) (SYNC_VALUE_LE >>> 24); + private static final byte FIRST_BYTE_14B_LE = (byte) (SYNC_VALUE_14B_LE >>> 24); + /** * Maps AMODE to the number of channels. See ETSI TS 102 114 table 5.4. */ @@ -45,20 +56,34 @@ public final class DtsUtil { 384, 448, 512, 640, 768, 896, 1024, 1152, 1280, 1536, 1920, 2048, 2304, 2560, 2688, 2816, 2823, 2944, 3072, 3840, 4096, 6144, 7680}; + /** + * Returns whether a given integer matches a DTS sync word. Synchronization and storage modes are + * defined in ETSI TS 102 114 V1.1.1 (2002-08), Section 5.3. + * + * @param word An integer. + * @return Whether a given integer matches a DTS sync word. + */ + public static boolean isSyncWord(int word) { + return word == SYNC_VALUE_BE + || word == SYNC_VALUE_LE + || word == SYNC_VALUE_14B_BE + || word == SYNC_VALUE_14B_LE; + } + /** * Returns the DTS format given {@code data} containing the DTS frame according to ETSI TS 102 114 * subsections 5.3/5.4. * * @param frame The DTS frame to parse. - * @param trackId The track identifier to set on the format, or null. + * @param trackId The track identifier to set on the format. * @param language The language to set on the format. * @param drmInitData {@link DrmInitData} to be included in the format. * @return The DTS format parsed from data in the header. */ - public static Format parseDtsFormat(byte[] frame, String trackId, String language, - DrmInitData drmInitData) { - ParsableBitArray frameBits = new ParsableBitArray(frame); - frameBits.skipBits(4 * 8 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE + public static Format parseDtsFormat( + byte[] frame, String trackId, @Nullable String language, @Nullable DrmInitData drmInitData) { + ParsableBitArray frameBits = getNormalizedFrameHeader(frame); + frameBits.skipBits(32 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE int amode = frameBits.readBits(6); int channelCount = CHANNELS_BY_AMODE[amode]; int sfreq = frameBits.readBits(4); @@ -79,8 +104,21 @@ public final class DtsUtil { * @return The number of audio samples represented by the frame. */ public static int parseDtsAudioSampleCount(byte[] data) { - // See ETSI TS 102 114 subsection 5.4.1. - int nblks = ((data[4] & 0x01) << 6) | ((data[5] & 0xFC) >> 2); + int nblks; + switch (data[0]) { + case FIRST_BYTE_LE: + nblks = ((data[5] & 0x01) << 6) | ((data[4] & 0xFC) >> 2); + break; + case FIRST_BYTE_14B_LE: + nblks = ((data[4] & 0x07) << 4) | ((data[7] & 0x3C) >> 2); + break; + case FIRST_BYTE_14B_BE: + nblks = ((data[5] & 0x07) << 4) | ((data[6] & 0x3C) >> 2); + break; + default: + // We blindly assume FIRST_BYTE_BE if none of the others match. + nblks = ((data[4] & 0x01) << 6) | ((data[5] & 0xFC) >> 2); + } return (nblks + 1) * 32; } @@ -94,8 +132,21 @@ public final class DtsUtil { public static int parseDtsAudioSampleCount(ByteBuffer buffer) { // See ETSI TS 102 114 subsection 5.4.1. int position = buffer.position(); - int nblks = ((buffer.get(position + 4) & 0x01) << 6) - | ((buffer.get(position + 5) & 0xFC) >> 2); + int nblks; + switch (buffer.get(position)) { + case FIRST_BYTE_LE: + nblks = ((buffer.get(position + 5) & 0x01) << 6) | ((buffer.get(position + 4) & 0xFC) >> 2); + break; + case FIRST_BYTE_14B_LE: + nblks = ((buffer.get(position + 4) & 0x07) << 4) | ((buffer.get(position + 7) & 0x3C) >> 2); + break; + case FIRST_BYTE_14B_BE: + nblks = ((buffer.get(position + 5) & 0x07) << 4) | ((buffer.get(position + 6) & 0x3C) >> 2); + break; + default: + // We blindly assume FIRST_BYTE_BE if none of the others match. + nblks = ((buffer.get(position + 4) & 0x01) << 6) | ((buffer.get(position + 5) & 0xFC) >> 2); + } return (nblks + 1) * 32; } @@ -106,9 +157,59 @@ public final class DtsUtil { * @return The frame's size in bytes. */ public static int getDtsFrameSize(byte[] data) { - return (((data[5] & 0x02) << 12) - | ((data[6] & 0xFF) << 4) - | ((data[7] & 0xF0) >> 4)) + 1; + int fsize; + boolean uses14BitPerWord = false; + switch (data[0]) { + case FIRST_BYTE_14B_BE: + fsize = (((data[6] & 0x03) << 12) | ((data[7] & 0xFF) << 4) | ((data[8] & 0x3C) >> 2)) + 1; + uses14BitPerWord = true; + break; + case FIRST_BYTE_LE: + fsize = (((data[4] & 0x03) << 12) | ((data[7] & 0xFF) << 4) | ((data[6] & 0xF0) >> 4)) + 1; + break; + case FIRST_BYTE_14B_LE: + fsize = (((data[7] & 0x03) << 12) | ((data[6] & 0xFF) << 4) | ((data[9] & 0x3C) >> 2)) + 1; + uses14BitPerWord = true; + break; + default: + // We blindly assume FIRST_BYTE_BE if none of the others match. + fsize = (((data[5] & 0x03) << 12) | ((data[6] & 0xFF) << 4) | ((data[7] & 0xF0) >> 4)) + 1; + } + + // If the frame is stored in 14-bit mode, adjust the frame size to reflect the actual byte size. + return uses14BitPerWord ? fsize * 16 / 14 : fsize; + } + + private static ParsableBitArray getNormalizedFrameHeader(byte[] frameHeader) { + if (frameHeader[0] == FIRST_BYTE_BE) { + // The frame is already 16-bit mode, big endian. + return new ParsableBitArray(frameHeader); + } + // Data is not normalized, but we don't want to modify frameHeader. + frameHeader = Arrays.copyOf(frameHeader, frameHeader.length); + if (isLittleEndianFrameHeader(frameHeader)) { + // Change endianness. + for (int i = 0; i < frameHeader.length - 1; i += 2) { + byte temp = frameHeader[i]; + frameHeader[i] = frameHeader[i + 1]; + frameHeader[i + 1] = temp; + } + } + ParsableBitArray frameBits = new ParsableBitArray(frameHeader); + if (frameHeader[0] == (byte) (SYNC_VALUE_14B_BE >> 24)) { + // Discard the 2 most significant bits of each 16 bit word. + ParsableBitArray scratchBits = new ParsableBitArray(frameHeader); + while (scratchBits.bitsLeft() >= 16) { + scratchBits.skipBits(2); + frameBits.putInt(scratchBits.readBits(14), 14); + } + } + frameBits.reset(frameHeader); + return frameBits; + } + + private static boolean isLittleEndianFrameHeader(byte[] frameHeader) { + return frameHeader[0] == FIRST_BYTE_LE || frameHeader[0] == FIRST_BYTE_14B_LE; } private DtsUtil() {} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java new file mode 100644 index 000000000000..c2eb62a0ad98 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.nio.ByteBuffer; + +/** + * An {@link AudioProcessor} that converts high resolution PCM audio to 32-bit float. The following + * encodings are supported as input: + * + *

+ */ +/* package */ final class FloatResamplingAudioProcessor extends BaseAudioProcessor { + + private static final int FLOAT_NAN_AS_INT = Float.floatToIntBits(Float.NaN); + private static final double PCM_32_BIT_INT_TO_PCM_32_BIT_FLOAT_FACTOR = 1.0 / 0x7FFFFFFF; + + @Override + public AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + @C.PcmEncoding int encoding = inputAudioFormat.encoding; + if (!Util.isEncodingHighResolutionPcm(encoding)) { + throw new UnhandledAudioFormatException(inputAudioFormat); + } + return encoding != C.ENCODING_PCM_FLOAT + ? new AudioFormat( + inputAudioFormat.sampleRate, inputAudioFormat.channelCount, C.ENCODING_PCM_FLOAT) + : AudioFormat.NOT_SET; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + int position = inputBuffer.position(); + int limit = inputBuffer.limit(); + int size = limit - position; + + ByteBuffer buffer; + switch (inputAudioFormat.encoding) { + case C.ENCODING_PCM_24BIT: + buffer = replaceOutputBuffer((size / 3) * 4); + for (int i = position; i < limit; i += 3) { + int pcm32BitInteger = + ((inputBuffer.get(i) & 0xFF) << 8) + | ((inputBuffer.get(i + 1) & 0xFF) << 16) + | ((inputBuffer.get(i + 2) & 0xFF) << 24); + writePcm32BitFloat(pcm32BitInteger, buffer); + } + break; + case C.ENCODING_PCM_32BIT: + buffer = replaceOutputBuffer(size); + for (int i = position; i < limit; i += 4) { + int pcm32BitInteger = + (inputBuffer.get(i) & 0xFF) + | ((inputBuffer.get(i + 1) & 0xFF) << 8) + | ((inputBuffer.get(i + 2) & 0xFF) << 16) + | ((inputBuffer.get(i + 3) & 0xFF) << 24); + writePcm32BitFloat(pcm32BitInteger, buffer); + } + break; + case C.ENCODING_PCM_8BIT: + case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + case C.ENCODING_PCM_FLOAT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); + } + + inputBuffer.position(inputBuffer.limit()); + buffer.flip(); + } + + /** + * Converts the provided 32-bit integer to a 32-bit float value and writes it to {@code buffer}. + * + * @param pcm32BitInt The 32-bit integer value to convert to 32-bit float in [-1.0, 1.0]. + * @param buffer The output buffer. + */ + private static void writePcm32BitFloat(int pcm32BitInt, ByteBuffer buffer) { + float pcm32BitFloat = (float) (PCM_32_BIT_INT_TO_PCM_32_BIT_FLOAT_FACTOR * pcm32BitInt); + int floatBits = Float.floatToIntBits(pcm32BitFloat); + if (floatBits == FLOAT_NAN_AS_INT) { + floatBits = Float.floatToIntBits((float) 0.0); + } + buffer.putInt(floatBits); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ForwardingAudioSink.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ForwardingAudioSink.java new file mode 100644 index 000000000000..4e7f9d69f919 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ForwardingAudioSink.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters; +import java.nio.ByteBuffer; + +/** An overridable {@link AudioSink} implementation forwarding all methods to another sink. */ +public class ForwardingAudioSink implements AudioSink { + + private final AudioSink sink; + + public ForwardingAudioSink(AudioSink sink) { + this.sink = sink; + } + + @Override + public void setListener(Listener listener) { + sink.setListener(listener); + } + + @Override + public boolean supportsOutput(int channelCount, int encoding) { + return sink.supportsOutput(channelCount, encoding); + } + + @Override + public long getCurrentPositionUs(boolean sourceEnded) { + return sink.getCurrentPositionUs(sourceEnded); + } + + @Override + public void configure( + int inputEncoding, + int inputChannelCount, + int inputSampleRate, + int specifiedBufferSize, + @Nullable int[] outputChannels, + int trimStartFrames, + int trimEndFrames) + throws ConfigurationException { + sink.configure( + inputEncoding, + inputChannelCount, + inputSampleRate, + specifiedBufferSize, + outputChannels, + trimStartFrames, + trimEndFrames); + } + + @Override + public void play() { + sink.play(); + } + + @Override + public void handleDiscontinuity() { + sink.handleDiscontinuity(); + } + + @Override + public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) + throws InitializationException, WriteException { + return sink.handleBuffer(buffer, presentationTimeUs); + } + + @Override + public void playToEndOfStream() throws WriteException { + sink.playToEndOfStream(); + } + + @Override + public boolean isEnded() { + return sink.isEnded(); + } + + @Override + public boolean hasPendingData() { + return sink.hasPendingData(); + } + + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + sink.setPlaybackParameters(playbackParameters); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return sink.getPlaybackParameters(); + } + + @Override + public void setAudioAttributes(AudioAttributes audioAttributes) { + sink.setAudioAttributes(audioAttributes); + } + + @Override + public void setAudioSessionId(int audioSessionId) { + sink.setAudioSessionId(audioSessionId); + } + + @Override + public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) { + sink.setAuxEffectInfo(auxEffectInfo); + } + + @Override + public void enableTunnelingV21(int tunnelingAudioSessionId) { + sink.enableTunnelingV21(tunnelingAudioSessionId); + } + + @Override + public void disableTunneling() { + sink.disableTunneling(); + } + + @Override + public void setVolume(float volume) { + sink.setVolume(volume); + } + + @Override + public void pause() { + sink.pause(); + } + + @Override + public void flush() { + sink.flush(); + } + + @Override + public void reset() { + sink.reset(); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index e0de5cfe983a..42f7e99b7826 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -15,53 +15,108 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.audio; -import android.annotation.TargetApi; +import android.annotation.SuppressLint; +import android.content.Context; import android.media.MediaCodec; import android.media.MediaCrypto; import android.media.MediaFormat; import android.media.audiofx.Virtualizer; import android.os.Handler; +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.PlayerMessage.Target; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities; import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecSelector; +import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaFormatUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MediaClock; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** - * Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}. + * Decodes and renders audio using {@link MediaCodec} and an {@link AudioSink}. + * + *

This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)} + * on the playback thread: + * + *

*/ -@TargetApi(16) public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock { - private final EventDispatcher eventDispatcher; - private final AudioTrack audioTrack; + /** + * Maximum number of tracked pending stream change times. Generally there is zero or one pending + * stream change. We track more to allow for pending changes that have fewer samples than the + * codec latency. + */ + private static final int MAX_PENDING_STREAM_CHANGE_COUNT = 10; + private static final String TAG = "MediaCodecAudioRenderer"; + /** + * Custom key used to indicate bits per sample by some decoders on Vivo devices. For example + * OMX.vivo.alac.decoder on the Vivo Z1 Pro. + */ + private static final String VIVO_BITS_PER_SAMPLE_KEY = "v-bits-per-sample"; + + private final Context context; + private final EventDispatcher eventDispatcher; + private final AudioSink audioSink; + private final long[] pendingStreamChangeTimesUs; + + private int codecMaxInputSize; private boolean passthroughEnabled; private boolean codecNeedsDiscardChannelsWorkaround; + private boolean codecNeedsEosBufferTimestampWorkaround; private android.media.MediaFormat passthroughMediaFormat; - private int pcmEncoding; - private int channelCount; + @Nullable private Format inputFormat; private long currentPositionUs; + private boolean allowFirstBufferPositionDiscontinuity; private boolean allowPositionDiscontinuity; + private long lastInputTimeUs; + private int pendingStreamChangeCount; /** + * @param context A context. * @param mediaCodecSelector A decoder selector. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector) { - this(mediaCodecSelector, null, true); + @SuppressWarnings("deprecation") + public MediaCodecAudioRenderer(Context context, MediaCodecSelector mediaCodecSelector) { + this( + context, + mediaCodecSelector, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false); } /** + * @param context A context. * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. @@ -70,25 +125,50 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * begin in parallel with key acquisition. This parameter specifies whether the renderer is * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. + * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, + * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, - DrmSessionManager drmSessionManager, + @Deprecated + @SuppressWarnings("deprecation") + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { - this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, null, null); + this( + context, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + /* eventHandler= */ null, + /* eventListener= */ null); } /** + * @param context A context. * @param mediaCodecSelector A decoder selector. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, Handler eventHandler, - AudioRendererEventListener eventListener) { - this(mediaCodecSelector, null, true, eventHandler, eventListener); + @SuppressWarnings("deprecation") + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener) { + this( + context, + mediaCodecSelector, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + eventHandler, + eventListener); } /** + * @param context A context. * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. @@ -100,16 +180,31 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, + * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, - DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, Handler eventHandler, - AudioRendererEventListener eventListener) { - this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, - eventListener, null); + @Deprecated + @SuppressWarnings("deprecation") + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener) { + this( + context, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + eventHandler, + eventListener, + (AudioCapabilities) null); } /** + * @param context A context. * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. @@ -125,89 +220,331 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * default capabilities (no encoded audio passthrough support) should be assumed. * @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before * output. + * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, + * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, - DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, Handler eventHandler, - AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, + @Deprecated + @SuppressWarnings("deprecation") + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + @Nullable AudioCapabilities audioCapabilities, AudioProcessor... audioProcessors) { - super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); - audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener()); + this( + context, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + eventHandler, + eventListener, + new DefaultAudioSink(audioCapabilities, audioProcessors)); + } + + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioSink The sink to which audio will be output. + * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, + * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. + */ + @Deprecated + @SuppressWarnings("deprecation") + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + AudioSink audioSink) { + this( + context, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + /* enableDecoderFallback= */ false, + eventHandler, + eventListener, + audioSink); + } + + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is slower/less efficient than + * the primary decoder. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioSink The sink to which audio will be output. + */ + @SuppressWarnings("deprecation") + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + boolean enableDecoderFallback, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + AudioSink audioSink) { + this( + context, + mediaCodecSelector, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + enableDecoderFallback, + eventHandler, + eventListener, + audioSink); + } + + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is slower/less efficient than + * the primary decoder. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioSink The sink to which audio will be output. + * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, + * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. + */ + @Deprecated + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + AudioSink audioSink) { + super( + C.TRACK_TYPE_AUDIO, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + enableDecoderFallback, + /* assumedMinimumCodecOperatingRate= */ 44100); + this.context = context.getApplicationContext(); + this.audioSink = audioSink; + lastInputTimeUs = C.TIME_UNSET; + pendingStreamChangeTimesUs = new long[MAX_PENDING_STREAM_CHANGE_COUNT]; eventDispatcher = new EventDispatcher(eventHandler, eventListener); + audioSink.setListener(new AudioSinkListener()); } @Override - protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) + @Capabilities + protected int supportsFormat( + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + Format format) throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isAudio(mimeType)) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } + @TunnelingSupport int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; - if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) { - return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED; + boolean supportsFormatDrm = + format.drmInitData == null + || FrameworkMediaCrypto.class.equals(format.exoMediaCryptoType) + || (format.exoMediaCryptoType == null + && supportsFormatDrm(drmSessionManager, format.drmInitData)); + if (supportsFormatDrm + && allowPassthrough(format.channelCount, mimeType) + && mediaCodecSelector.getPassthroughDecoderInfo() != null) { + return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport); } - MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false); - if (decoderInfo == null) { - return FORMAT_UNSUPPORTED_SUBTYPE; + if ((MimeTypes.AUDIO_RAW.equals(mimeType) + && !audioSink.supportsOutput(format.channelCount, format.pcmEncoding)) + || !audioSink.supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) { + // Assume the decoder outputs 16-bit PCM, unless the input is raw. + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } - // Note: We assume support for unknown sampleRate and channelCount. - boolean decoderCapable = Util.SDK_INT < 21 - || ((format.sampleRate == Format.NO_VALUE - || decoderInfo.isAudioSampleRateSupportedV21(format.sampleRate)) - && (format.channelCount == Format.NO_VALUE - || decoderInfo.isAudioChannelCountSupportedV21(format.channelCount))); - int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; - return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport; + List decoderInfos = + getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false); + if (decoderInfos.isEmpty()) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); + } + if (!supportsFormatDrm) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); + } + // Check capabilities for the first decoder in the list, which takes priority. + MediaCodecInfo decoderInfo = decoderInfos.get(0); + boolean isFormatSupported = decoderInfo.isFormatSupported(format); + @AdaptiveSupport + int adaptiveSupport = + isFormatSupported && decoderInfo.isSeamlessAdaptationSupported(format) + ? ADAPTIVE_SEAMLESS + : ADAPTIVE_NOT_SEAMLESS; + @FormatSupport + int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; + return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport); } @Override - protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector, - Format format, boolean requiresSecureDecoder) throws DecoderQueryException { - if (allowPassthrough(format.sampleMimeType)) { + protected List getDecoderInfos( + MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder) + throws DecoderQueryException { + @Nullable String mimeType = format.sampleMimeType; + if (mimeType == null) { + return Collections.emptyList(); + } + if (allowPassthrough(format.channelCount, mimeType)) { + @Nullable MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo(); if (passthroughDecoderInfo != null) { - passthroughEnabled = true; - return passthroughDecoderInfo; + return Collections.singletonList(passthroughDecoderInfo); } } - passthroughEnabled = false; - return super.getDecoderInfo(mediaCodecSelector, format, requiresSecureDecoder); + List decoderInfos = + mediaCodecSelector.getDecoderInfos( + mimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); + decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format); + if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) { + // E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D. + List decoderInfosWithEac3 = new ArrayList<>(decoderInfos); + decoderInfosWithEac3.addAll( + mediaCodecSelector.getDecoderInfos( + MimeTypes.AUDIO_E_AC3, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false)); + decoderInfos = decoderInfosWithEac3; + } + return Collections.unmodifiableList(decoderInfos); } /** * Returns whether encoded audio passthrough should be used for playing back the input format. - * This implementation returns true if the {@link AudioTrack}'s audio capabilities indicate that - * passthrough is supported. + * This implementation returns true if the {@link AudioSink} indicates that encoded audio output + * is supported. * + * @param channelCount The number of channels in the input media, or {@link Format#NO_VALUE} if + * not known. * @param mimeType The type of input media. - * @return Whether passthrough playback should be used. + * @return Whether passthrough playback is supported. */ - protected boolean allowPassthrough(String mimeType) { - return audioTrack.isPassthroughSupported(mimeType); + protected boolean allowPassthrough(int channelCount, String mimeType) { + return getPassthroughEncoding(channelCount, mimeType) != C.ENCODING_INVALID; } @Override - protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, - MediaCrypto crypto) { + protected void configureCodec( + MediaCodecInfo codecInfo, + MediaCodec codec, + Format format, + @Nullable MediaCrypto crypto, + float codecOperatingRate) { + codecMaxInputSize = getCodecMaxInputSize(codecInfo, format, getStreamFormats()); codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); + codecNeedsEosBufferTimestampWorkaround = codecNeedsEosBufferTimestampWorkaround(codecInfo.name); + passthroughEnabled = codecInfo.passthrough; + String codecMimeType = passthroughEnabled ? MimeTypes.AUDIO_RAW : codecInfo.codecMimeType; + MediaFormat mediaFormat = + getMediaFormat(format, codecMimeType, codecMaxInputSize, codecOperatingRate); + codec.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0); if (passthroughEnabled) { - // Override the MIME type used to configure the codec if we are using a passthrough decoder. - passthroughMediaFormat = format.getFrameworkMediaFormatV16(); - passthroughMediaFormat.setString(MediaFormat.KEY_MIME, MimeTypes.AUDIO_RAW); - codec.configure(passthroughMediaFormat, null, crypto, 0); + // Store the input MIME type if we're using the passthrough codec. + passthroughMediaFormat = mediaFormat; passthroughMediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType); } else { - codec.configure(format.getFrameworkMediaFormatV16(), null, crypto, 0); passthroughMediaFormat = null; } } @Override + protected @KeepCodecResult int canKeepCodec( + MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + // TODO: We currently rely on recreating the codec when encoder delay or padding is non-zero. + // Re-creating the codec is necessary to guarantee that onOutputFormatChanged is called, which + // is where encoder delay and padding are propagated to the sink. We should find a better way to + // propagate these values, and then allow the codec to be re-used in cases where this would + // otherwise be possible. + if (getCodecMaxInputSize(codecInfo, newFormat) > codecMaxInputSize + || oldFormat.encoderDelay != 0 + || oldFormat.encoderPadding != 0 + || newFormat.encoderDelay != 0 + || newFormat.encoderPadding != 0) { + return KEEP_CODEC_RESULT_NO; + } else if (codecInfo.isSeamlessAdaptationSupported( + oldFormat, newFormat, /* isNewFormatComplete= */ true)) { + return KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION; + } else if (canKeepCodecWithFlush(oldFormat, newFormat)) { + return KEEP_CODEC_RESULT_YES_WITH_FLUSH; + } else { + return KEEP_CODEC_RESULT_NO; + } + } + + /** + * Returns whether the codec can be flushed and reused when switching to a new format. Reuse is + * generally possible when the codec would be configured in an identical way after the format + * change (excluding {@link MediaFormat#KEY_MAX_INPUT_SIZE} and configuration that does not come + * from the {@link Format}). + * + * @param oldFormat The first format. + * @param newFormat The second format. + * @return Whether the codec can be flushed and reused when switching to a new format. + */ + protected boolean canKeepCodecWithFlush(Format oldFormat, Format newFormat) { + // Flush and reuse the codec if the audio format and initialization data matches. For Opus, we + // don't flush and reuse the codec because the decoder may discard samples after flushing, which + // would result in audio being dropped just after a stream change (see [Internal: b/143450854]). + return Util.areEqual(oldFormat.sampleMimeType, newFormat.sampleMimeType) + && oldFormat.channelCount == newFormat.channelCount + && oldFormat.sampleRate == newFormat.sampleRate + && oldFormat.pcmEncoding == newFormat.pcmEncoding + && oldFormat.initializationDataEquals(newFormat) + && !MimeTypes.AUDIO_OPUS.equals(oldFormat.sampleMimeType); + } + + @Override + @Nullable public MediaClock getMediaClock() { return this; } + @Override + protected float getCodecOperatingRateV23( + float operatingRate, Format format, Format[] streamFormats) { + // Use the highest known stream sample-rate up front, to avoid having to reconfigure the codec + // should an adaptive switch to that stream occur. + int maxSampleRate = -1; + for (Format streamFormat : streamFormats) { + int streamSampleRate = streamFormat.sampleRate; + if (streamSampleRate != Format.NO_VALUE) { + maxSampleRate = Math.max(maxSampleRate, streamSampleRate); + } + } + return maxSampleRate == -1 ? CODEC_OPERATING_RATE_UNSET : (maxSampleRate * operatingRate); + } + @Override protected void onCodecInitialized(String name, long initializedTimestampMs, long initializationDurationMs) { @@ -215,29 +552,37 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { - super.onInputFormatChanged(newFormat); - eventDispatcher.inputFormatChanged(newFormat); - // If the input format is anything other than PCM then we assume that the audio decoder will - // output 16-bit PCM. - pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding - : C.ENCODING_PCM_16BIT; - channelCount = newFormat.channelCount; + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + super.onInputFormatChanged(formatHolder); + inputFormat = formatHolder.format; + eventDispatcher.inputFormatChanged(inputFormat); } @Override - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputMediaFormat) throws ExoPlaybackException { - boolean passthrough = passthroughMediaFormat != null; - String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME) - : MimeTypes.AUDIO_RAW; - MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; - int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); - int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); + @C.Encoding int encoding; + MediaFormat mediaFormat; + if (passthroughMediaFormat != null) { + mediaFormat = passthroughMediaFormat; + encoding = + getPassthroughEncoding( + mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT), + mediaFormat.getString(MediaFormat.KEY_MIME)); + } else { + mediaFormat = outputMediaFormat; + if (outputMediaFormat.containsKey(VIVO_BITS_PER_SAMPLE_KEY)) { + encoding = Util.getPcmEncoding(outputMediaFormat.getInteger(VIVO_BITS_PER_SAMPLE_KEY)); + } else { + encoding = getPcmEncoding(inputFormat); + } + } + int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); + int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); int[] channelMap; - if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && this.channelCount < 6) { - channelMap = new int[this.channelCount]; - for (int i = 0; i < this.channelCount; i++) { + if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && inputFormat.channelCount < 6) { + channelMap = new int[inputFormat.channelCount]; + for (int i = 0; i < inputFormat.channelCount; i++) { channelMap[i] = i; } } else { @@ -245,9 +590,40 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } try { - audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap); - } catch (AudioTrack.ConfigurationException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + audioSink.configure( + encoding, + channelCount, + sampleRate, + 0, + channelMap, + inputFormat.encoderDelay, + inputFormat.encoderPadding); + } catch (AudioSink.ConfigurationException e) { + // TODO(internal: b/145658993) Use outputFormat instead. + throw createRendererException(e, inputFormat); + } + } + + /** + * Returns the {@link C.Encoding} constant to use for passthrough of the given format, or {@link + * C#ENCODING_INVALID} if passthrough is not possible. + */ + @C.Encoding + protected int getPassthroughEncoding(int channelCount, String mimeType) { + if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) { + // E-AC3 JOC is object-based so the output channel count is arbitrary. + if (audioSink.supportsOutput(/* channelCount= */ Format.NO_VALUE, C.ENCODING_E_AC3_JOC)) { + return MimeTypes.getEncoding(MimeTypes.AUDIO_E_AC3_JOC); + } + // E-AC3 receivers can decode JOC streams, but in 2-D rather than 3-D, so try to fall back. + mimeType = MimeTypes.AUDIO_E_AC3; + } + + @C.Encoding int encoding = MimeTypes.getEncoding(mimeType); + if (audioSink.supportsOutput(channelCount, encoding)) { + return encoding; + } else { + return C.ENCODING_INVALID; } } @@ -257,21 +633,21 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances * should be released in {@link #onDisabled()} (if not before). * - * @see AudioTrack.Listener#onAudioSessionId(int) + * @see AudioSink.Listener#onAudioSessionId(int) */ protected void onAudioSessionId(int audioSessionId) { // Do nothing. } /** - * @see AudioTrack.Listener#onPositionDiscontinuity() + * @see AudioSink.Listener#onPositionDiscontinuity() */ protected void onAudioTrackPositionDiscontinuity() { // Do nothing. } /** - * @see AudioTrack.Listener#onUnderrun(int, long, long) + * @see AudioSink.Listener#onUnderrun(int, long, long) */ protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { @@ -284,102 +660,175 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media eventDispatcher.enabled(decoderCounters); int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId; if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { - audioTrack.enableTunnelingV21(tunnelingAudioSessionId); + audioSink.enableTunnelingV21(tunnelingAudioSessionId); } else { - audioTrack.disableTunneling(); + audioSink.disableTunneling(); + } + } + + @Override + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + super.onStreamChanged(formats, offsetUs); + if (lastInputTimeUs != C.TIME_UNSET) { + if (pendingStreamChangeCount == pendingStreamChangeTimesUs.length) { + Log.w( + TAG, + "Too many stream changes, so dropping change at " + + pendingStreamChangeTimesUs[pendingStreamChangeCount - 1]); + } else { + pendingStreamChangeCount++; + } + pendingStreamChangeTimesUs[pendingStreamChangeCount - 1] = lastInputTimeUs; } } @Override protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { super.onPositionReset(positionUs, joining); - audioTrack.reset(); + audioSink.flush(); currentPositionUs = positionUs; + allowFirstBufferPositionDiscontinuity = true; allowPositionDiscontinuity = true; + lastInputTimeUs = C.TIME_UNSET; + pendingStreamChangeCount = 0; } @Override protected void onStarted() { super.onStarted(); - audioTrack.play(); + audioSink.play(); } @Override protected void onStopped() { - audioTrack.pause(); + updateCurrentPosition(); + audioSink.pause(); super.onStopped(); } @Override protected void onDisabled() { try { - audioTrack.release(); + lastInputTimeUs = C.TIME_UNSET; + pendingStreamChangeCount = 0; + audioSink.flush(); } finally { try { super.onDisabled(); } finally { - decoderCounters.ensureUpdated(); eventDispatcher.disabled(decoderCounters); } } } + @Override + protected void onReset() { + try { + super.onReset(); + } finally { + audioSink.reset(); + } + } + @Override public boolean isEnded() { - return super.isEnded() && audioTrack.isEnded(); + return super.isEnded() && audioSink.isEnded(); } @Override public boolean isReady() { - return audioTrack.hasPendingData() || super.isReady(); + return audioSink.hasPendingData() || super.isReady(); } @Override public long getPositionUs() { - long newCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded()); - if (newCurrentPositionUs != AudioTrack.CURRENT_POSITION_NOT_SET) { - currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs - : Math.max(currentPositionUs, newCurrentPositionUs); - allowPositionDiscontinuity = false; + if (getState() == STATE_STARTED) { + updateCurrentPosition(); } return currentPositionUs; } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - return audioTrack.setPlaybackParameters(playbackParameters); + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + audioSink.setPlaybackParameters(playbackParameters); } @Override public PlaybackParameters getPlaybackParameters() { - return audioTrack.getPlaybackParameters(); + return audioSink.getPlaybackParameters(); } @Override - protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, - ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip) throws ExoPlaybackException { + protected void onQueueInputBuffer(DecoderInputBuffer buffer) { + if (allowFirstBufferPositionDiscontinuity && !buffer.isDecodeOnly()) { + // TODO: Remove this hack once we have a proper fix for [Internal: b/71876314]. + // Allow the position to jump if the first presentable input buffer has a timestamp that + // differs significantly from what was expected. + if (Math.abs(buffer.timeUs - currentPositionUs) > 500000) { + currentPositionUs = buffer.timeUs; + } + allowFirstBufferPositionDiscontinuity = false; + } + lastInputTimeUs = Math.max(buffer.timeUs, lastInputTimeUs); + } + + @CallSuper + @Override + protected void onProcessedOutputBuffer(long presentationTimeUs) { + while (pendingStreamChangeCount != 0 && presentationTimeUs >= pendingStreamChangeTimesUs[0]) { + audioSink.handleDiscontinuity(); + pendingStreamChangeCount--; + System.arraycopy( + pendingStreamChangeTimesUs, + /* srcPos= */ 1, + pendingStreamChangeTimesUs, + /* destPos= */ 0, + pendingStreamChangeCount); + } + } + + @Override + protected boolean processOutputBuffer( + long positionUs, + long elapsedRealtimeUs, + MediaCodec codec, + ByteBuffer buffer, + int bufferIndex, + int bufferFlags, + long bufferPresentationTimeUs, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, + Format format) + throws ExoPlaybackException { + if (codecNeedsEosBufferTimestampWorkaround + && bufferPresentationTimeUs == 0 + && (bufferFlags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0 + && lastInputTimeUs != C.TIME_UNSET) { + bufferPresentationTimeUs = lastInputTimeUs; + } + if (passthroughEnabled && (bufferFlags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { // Discard output buffers from the passthrough (raw) decoder containing codec specific data. codec.releaseOutputBuffer(bufferIndex, false); return true; } - if (shouldSkip) { + if (isDecodeOnlyBuffer) { codec.releaseOutputBuffer(bufferIndex, false); decoderCounters.skippedOutputBufferCount++; - audioTrack.handleDiscontinuity(); + audioSink.handleDiscontinuity(); return true; } try { - if (audioTrack.handleBuffer(buffer, bufferPresentationTimeUs)) { + if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) { codec.releaseOutputBuffer(bufferIndex, false); decoderCounters.renderedOutputBufferCount++; return true; } - } catch (AudioTrack.InitializationException | AudioTrack.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + } catch (AudioSink.InitializationException | AudioSink.WriteException e) { + // TODO(internal: b/145658993) Use outputFormat instead. + throw createRendererException(e, inputFormat); } return false; } @@ -387,21 +836,26 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void renderToEndOfStream() throws ExoPlaybackException { try { - audioTrack.playToEndOfStream(); - } catch (AudioTrack.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + audioSink.playToEndOfStream(); + } catch (AudioSink.WriteException e) { + // TODO(internal: b/145658993) Use outputFormat instead. + throw createRendererException(e, inputFormat); } } @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { switch (messageType) { case C.MSG_SET_VOLUME: - audioTrack.setVolume((Float) message); + audioSink.setVolume((Float) message); break; - case C.MSG_SET_STREAM_TYPE: - @C.StreamType int streamType = (Integer) message; - audioTrack.setStreamType(streamType); + case C.MSG_SET_AUDIO_ATTRIBUTES: + AudioAttributes audioAttributes = (AudioAttributes) message; + audioSink.setAudioAttributes(audioAttributes); + break; + case C.MSG_SET_AUX_EFFECT_INFO: + AuxEffectInfo auxEffectInfo = (AuxEffectInfo) message; + audioSink.setAuxEffectInfo(auxEffectInfo); break; default: super.handleMessage(messageType, message); @@ -409,6 +863,112 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + /** + * Returns a maximum input size suitable for configuring a codec for {@code format} in a way that + * will allow possible adaptation to other compatible formats in {@code streamFormats}. + * + * @param codecInfo A {@link MediaCodecInfo} describing the decoder. + * @param format The {@link Format} for which the codec is being configured. + * @param streamFormats The possible stream formats. + * @return A suitable maximum input size. + */ + protected int getCodecMaxInputSize( + MediaCodecInfo codecInfo, Format format, Format[] streamFormats) { + int maxInputSize = getCodecMaxInputSize(codecInfo, format); + if (streamFormats.length == 1) { + // The single entry in streamFormats must correspond to the format for which the codec is + // being configured. + return maxInputSize; + } + for (Format streamFormat : streamFormats) { + if (codecInfo.isSeamlessAdaptationSupported( + format, streamFormat, /* isNewFormatComplete= */ false)) { + maxInputSize = Math.max(maxInputSize, getCodecMaxInputSize(codecInfo, streamFormat)); + } + } + return maxInputSize; + } + + /** + * Returns a maximum input buffer size for a given {@link Format}. + * + * @param codecInfo A {@link MediaCodecInfo} describing the decoder. + * @param format The {@link Format}. + * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not + * be determined. + */ + private int getCodecMaxInputSize(MediaCodecInfo codecInfo, Format format) { + if ("OMX.google.raw.decoder".equals(codecInfo.name)) { + // OMX.google.raw.decoder didn't resize its output buffers correctly prior to N, except on + // Android TV running M, so there's no point requesting a non-default input size. Doing so may + // cause a native crash, whereas not doing so will cause a more controlled failure when + // attempting to fill an input buffer. See: https://github.com/google/ExoPlayer/issues/4057. + if (Util.SDK_INT < 24 && !(Util.SDK_INT == 23 && Util.isTv(context))) { + return Format.NO_VALUE; + } + } + return format.maxInputSize; + } + + /** + * Returns the framework {@link MediaFormat} that can be used to configure a {@link MediaCodec} + * for decoding the given {@link Format} for playback. + * + * @param format The {@link Format} of the media. + * @param codecMimeType The MIME type handled by the codec. + * @param codecMaxInputSize The maximum input size supported by the codec. + * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if + * no codec operating rate should be set. + * @return The framework {@link MediaFormat}. + */ + @SuppressLint("InlinedApi") + protected MediaFormat getMediaFormat( + Format format, String codecMimeType, int codecMaxInputSize, float codecOperatingRate) { + MediaFormat mediaFormat = new MediaFormat(); + // Set format parameters that should always be set. + mediaFormat.setString(MediaFormat.KEY_MIME, codecMimeType); + mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, format.channelCount); + mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, format.sampleRate); + MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); + // Set codec max values. + MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxInputSize); + // Set codec configuration values. + if (Util.SDK_INT >= 23) { + mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */); + if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET && !deviceDoesntSupportOperatingRate()) { + mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate); + } + } + if (Util.SDK_INT <= 28 && MimeTypes.AUDIO_AC4.equals(format.sampleMimeType)) { + // On some older builds, the AC-4 decoder expects to receive samples formatted as raw frames + // not sync frames. Set a format key to override this. + mediaFormat.setInteger("ac4-is-sync", 1); + } + return mediaFormat; + } + + private void updateCurrentPosition() { + long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded()); + if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) { + currentPositionUs = + allowPositionDiscontinuity + ? newCurrentPositionUs + : Math.max(currentPositionUs, newCurrentPositionUs); + allowPositionDiscontinuity = false; + } + } + + /** + * Returns whether the device's decoders are known to not support setting the codec operating + * rate. + * + *

See GitHub issue #5821. + */ + private static boolean deviceDoesntSupportOperatingRate() { + return Util.SDK_INT == 23 + && ("ZTE B2017G".equals(Util.MODEL) || "AXON 7 mini".equals(Util.MODEL)); + } + /** * Returns whether the decoder is known to output six audio channels when provided with input with * fewer than six channels. @@ -423,7 +983,34 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media || Util.DEVICE.startsWith("heroqlte")); } - private final class AudioTrackListener implements AudioTrack.Listener { + /** + * Returns whether the decoder may output a non-empty buffer with timestamp 0 as the end of stream + * buffer. + * + *

See GitHub issue #5045. + */ + private static boolean codecNeedsEosBufferTimestampWorkaround(String codecName) { + return Util.SDK_INT < 21 + && "OMX.SEC.mp3.dec".equals(codecName) + && "samsung".equals(Util.MANUFACTURER) + && (Util.DEVICE.startsWith("baffin") + || Util.DEVICE.startsWith("grand") + || Util.DEVICE.startsWith("fortuna") + || Util.DEVICE.startsWith("gprimelte") + || Util.DEVICE.startsWith("j2y18lte") + || Util.DEVICE.startsWith("ms01")); + } + + @C.Encoding + private static int getPcmEncoding(Format format) { + // If the format is anything other than PCM then we assume that the audio decoder will output + // 16-bit PCM. + return MimeTypes.AUDIO_RAW.equals(format.sampleMimeType) + ? format.pcmEncoding + : C.ENCODING_PCM_16BIT; + } + + private final class AudioSinkListener implements AudioSink.Listener { @Override public void onAudioSessionId(int audioSessionId) { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index a22055891b07..efd8a30d6125 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -18,65 +18,38 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.audio; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import java.nio.ByteBuffer; -import java.nio.ByteOrder; /** - * An {@link AudioProcessor} that converts audio data to {@link C#ENCODING_PCM_16BIT}. + * An {@link AudioProcessor} that converts different PCM audio encodings to 16-bit integer PCM. The + * following encodings are supported as input: + * + *

    + *
  • {@link C#ENCODING_PCM_8BIT} + *
  • {@link C#ENCODING_PCM_16BIT} ({@link #isActive()} will return {@code false}) + *
  • {@link C#ENCODING_PCM_16BIT_BIG_ENDIAN} + *
  • {@link C#ENCODING_PCM_24BIT} + *
  • {@link C#ENCODING_PCM_32BIT} + *
  • {@link C#ENCODING_PCM_FLOAT} + *
*/ -/* package */ final class ResamplingAudioProcessor implements AudioProcessor { - - private int sampleRateHz; - private int channelCount; - @C.PcmEncoding - private int encoding; - private ByteBuffer buffer; - private ByteBuffer outputBuffer; - private boolean inputEnded; - - /** - * Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_16BIT}. - */ - public ResamplingAudioProcessor() { - sampleRateHz = Format.NO_VALUE; - channelCount = Format.NO_VALUE; - encoding = C.ENCODING_INVALID; - buffer = EMPTY_BUFFER; - outputBuffer = EMPTY_BUFFER; - } +/* package */ final class ResamplingAudioProcessor extends BaseAudioProcessor { @Override - public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) - throws UnhandledFormatException { - if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT - && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + public AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + @C.PcmEncoding int encoding = inputAudioFormat.encoding; + if (encoding != C.ENCODING_PCM_8BIT + && encoding != C.ENCODING_PCM_16BIT + && encoding != C.ENCODING_PCM_16BIT_BIG_ENDIAN + && encoding != C.ENCODING_PCM_24BIT + && encoding != C.ENCODING_PCM_32BIT + && encoding != C.ENCODING_PCM_FLOAT) { + throw new UnhandledAudioFormatException(inputAudioFormat); } - if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount - && this.encoding == encoding) { - return false; - } - this.sampleRateHz = sampleRateHz; - this.channelCount = channelCount; - this.encoding = encoding; - if (encoding == C.ENCODING_PCM_16BIT) { - buffer = EMPTY_BUFFER; - } - return true; - } - - @Override - public boolean isActive() { - return encoding != C.ENCODING_INVALID && encoding != C.ENCODING_PCM_16BIT; - } - - @Override - public int getOutputChannelCount() { - return channelCount; - } - - @Override - public int getOutputEncoding() { - return C.ENCODING_PCM_16BIT; + return encoding != C.ENCODING_PCM_16BIT + ? new AudioFormat( + inputAudioFormat.sampleRate, inputAudioFormat.channelCount, C.ENCODING_PCM_16BIT) + : AudioFormat.NOT_SET; } @Override @@ -86,14 +59,18 @@ import java.nio.ByteOrder; int limit = inputBuffer.limit(); int size = limit - position; int resampledSize; - switch (encoding) { + switch (inputAudioFormat.encoding) { case C.ENCODING_PCM_8BIT: resampledSize = size * 2; break; + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + resampledSize = size; + break; case C.ENCODING_PCM_24BIT: resampledSize = (size / 3) * 2; break; case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_FLOAT: resampledSize = size / 2; break; case C.ENCODING_PCM_16BIT: @@ -102,35 +79,47 @@ import java.nio.ByteOrder; default: throw new IllegalStateException(); } - if (buffer.capacity() < resampledSize) { - buffer = ByteBuffer.allocateDirect(resampledSize).order(ByteOrder.nativeOrder()); - } else { - buffer.clear(); - } // Resample the little endian input and update the input/output buffers. - switch (encoding) { + ByteBuffer buffer = replaceOutputBuffer(resampledSize); + switch (inputAudioFormat.encoding) { case C.ENCODING_PCM_8BIT: - // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. + // 8 -> 16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. for (int i = position; i < limit; i++) { buffer.put((byte) 0); buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128)); } break; + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + // Big endian to little endian resampling. Swap the byte order. + for (int i = position; i < limit; i += 2) { + buffer.put(inputBuffer.get(i + 1)); + buffer.put(inputBuffer.get(i)); + } + break; case C.ENCODING_PCM_24BIT: - // 24->16 bit resampling. Drop the least significant byte. + // 24 -> 16 bit resampling. Drop the least significant byte. for (int i = position; i < limit; i += 3) { buffer.put(inputBuffer.get(i + 1)); buffer.put(inputBuffer.get(i + 2)); } break; case C.ENCODING_PCM_32BIT: - // 32->16 bit resampling. Drop the two least significant bytes. + // 32 -> 16 bit resampling. Drop the two least significant bytes. for (int i = position; i < limit; i += 4) { buffer.put(inputBuffer.get(i + 2)); buffer.put(inputBuffer.get(i + 3)); } break; + case C.ENCODING_PCM_FLOAT: + // 32 bit floating point -> 16 bit resampling. Floating point values are in the range + // [-1.0, 1.0], so need to be scaled by Short.MAX_VALUE. + for (int i = position; i < limit; i += 4) { + short value = (short) (inputBuffer.getFloat(i) * Short.MAX_VALUE); + buffer.put((byte) (value & 0xFF)); + buffer.put((byte) ((value >> 8) & 0xFF)); + } + break; case C.ENCODING_PCM_16BIT: case C.ENCODING_INVALID: case Format.NO_VALUE: @@ -140,40 +129,6 @@ import java.nio.ByteOrder; } inputBuffer.position(inputBuffer.limit()); buffer.flip(); - outputBuffer = buffer; - } - - @Override - public void queueEndOfStream() { - inputEnded = true; - } - - @Override - public ByteBuffer getOutput() { - ByteBuffer outputBuffer = this.outputBuffer; - this.outputBuffer = EMPTY_BUFFER; - return outputBuffer; - } - - @SuppressWarnings("ReferenceEquality") - @Override - public boolean isEnded() { - return inputEnded && outputBuffer == EMPTY_BUFFER; - } - - @Override - public void flush() { - outputBuffer = EMPTY_BUFFER; - inputEnded = false; - } - - @Override - public void reset() { - flush(); - buffer = EMPTY_BUFFER; - sampleRateHz = Format.NO_VALUE; - channelCount = Format.NO_VALUE; - encoding = C.ENCODING_INVALID; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java new file mode 100644 index 000000000000..6a2c5ae9a64a --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; + +/** + * An {@link AudioProcessor} that skips silence in the input stream. Input and output are 16-bit + * PCM. + */ +public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { + + /** + * The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify + * that part of audio as silent, in microseconds. + */ + private static final long MINIMUM_SILENCE_DURATION_US = 150_000; + /** + * The duration of silence by which to extend non-silent sections, in microseconds. The value must + * not exceed {@link #MINIMUM_SILENCE_DURATION_US}. + */ + private static final long PADDING_SILENCE_US = 20_000; + /** + * The absolute level below which an individual PCM sample is classified as silent. Note: the + * specified value will be rounded so that the threshold check only depends on the more + * significant byte, for efficiency. + */ + private static final short SILENCE_THRESHOLD_LEVEL = 1024; + + /** + * Threshold for classifying an individual PCM sample as silent based on its more significant + * byte. This is {@link #SILENCE_THRESHOLD_LEVEL} divided by 256 with rounding. + */ + private static final byte SILENCE_THRESHOLD_LEVEL_MSB = (SILENCE_THRESHOLD_LEVEL + 128) >> 8; + + /** Trimming states. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + STATE_NOISY, + STATE_MAYBE_SILENT, + STATE_SILENT, + }) + private @interface State {} + /** State when the input is not silent. */ + private static final int STATE_NOISY = 0; + /** State when the input may be silent but we haven't read enough yet to know. */ + private static final int STATE_MAYBE_SILENT = 1; + /** State when the input is silent. */ + private static final int STATE_SILENT = 2; + + private int bytesPerFrame; + + private boolean enabled; + + /** + * Buffers audio data that may be classified as silence while in {@link #STATE_MAYBE_SILENT}. If + * the input becomes noisy before the buffer has filled, it will be output. Otherwise, the buffer + * contents will be dropped and the state will transition to {@link #STATE_SILENT}. + */ + private byte[] maybeSilenceBuffer; + + /** + * Stores the latest part of the input while silent. It will be output as padding if the next + * input is noisy. + */ + private byte[] paddingBuffer; + + @State private int state; + private int maybeSilenceBufferSize; + private int paddingSize; + private boolean hasOutputNoise; + private long skippedFrames; + + /** Creates a new silence trimming audio processor. */ + public SilenceSkippingAudioProcessor() { + maybeSilenceBuffer = Util.EMPTY_BYTE_ARRAY; + paddingBuffer = Util.EMPTY_BYTE_ARRAY; + } + + /** + * Sets whether to skip silence in the input. This method may only be called after draining data + * through the processor. The value returned by {@link #isActive()} may change, and the processor + * must be {@link #flush() flushed} before queueing more data. + * + * @param enabled Whether to skip silence in the input. + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * Returns the total number of frames of input audio that were skipped due to being classified as + * silence since the last call to {@link #flush()}. + */ + public long getSkippedFrames() { + return skippedFrames; + } + + // AudioProcessor implementation. + + @Override + public AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) { + throw new UnhandledAudioFormatException(inputAudioFormat); + } + return enabled ? inputAudioFormat : AudioFormat.NOT_SET; + } + + @Override + public boolean isActive() { + return enabled; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + while (inputBuffer.hasRemaining() && !hasPendingOutput()) { + switch (state) { + case STATE_NOISY: + processNoisy(inputBuffer); + break; + case STATE_MAYBE_SILENT: + processMaybeSilence(inputBuffer); + break; + case STATE_SILENT: + processSilence(inputBuffer); + break; + default: + throw new IllegalStateException(); + } + } + } + + @Override + protected void onQueueEndOfStream() { + if (maybeSilenceBufferSize > 0) { + // We haven't received enough silence to transition to the silent state, so output the buffer. + output(maybeSilenceBuffer, maybeSilenceBufferSize); + } + if (!hasOutputNoise) { + skippedFrames += paddingSize / bytesPerFrame; + } + } + + @Override + protected void onFlush() { + if (enabled) { + bytesPerFrame = inputAudioFormat.bytesPerFrame; + int maybeSilenceBufferSize = durationUsToFrames(MINIMUM_SILENCE_DURATION_US) * bytesPerFrame; + if (maybeSilenceBuffer.length != maybeSilenceBufferSize) { + maybeSilenceBuffer = new byte[maybeSilenceBufferSize]; + } + paddingSize = durationUsToFrames(PADDING_SILENCE_US) * bytesPerFrame; + if (paddingBuffer.length != paddingSize) { + paddingBuffer = new byte[paddingSize]; + } + } + state = STATE_NOISY; + skippedFrames = 0; + maybeSilenceBufferSize = 0; + hasOutputNoise = false; + } + + @Override + protected void onReset() { + enabled = false; + paddingSize = 0; + maybeSilenceBuffer = Util.EMPTY_BYTE_ARRAY; + paddingBuffer = Util.EMPTY_BYTE_ARRAY; + } + + // Internal methods. + + /** + * Incrementally processes new input from {@code inputBuffer} while in {@link #STATE_NOISY}, + * updating the state if needed. + */ + private void processNoisy(ByteBuffer inputBuffer) { + int limit = inputBuffer.limit(); + + // Check if there's any noise within the maybe silence buffer duration. + inputBuffer.limit(Math.min(limit, inputBuffer.position() + maybeSilenceBuffer.length)); + int noiseLimit = findNoiseLimit(inputBuffer); + if (noiseLimit == inputBuffer.position()) { + // The buffer contains the start of possible silence. + state = STATE_MAYBE_SILENT; + } else { + inputBuffer.limit(noiseLimit); + output(inputBuffer); + } + + // Restore the limit. + inputBuffer.limit(limit); + } + + /** + * Incrementally processes new input from {@code inputBuffer} while in {@link + * #STATE_MAYBE_SILENT}, updating the state if needed. + */ + private void processMaybeSilence(ByteBuffer inputBuffer) { + int limit = inputBuffer.limit(); + int noisePosition = findNoisePosition(inputBuffer); + int maybeSilenceInputSize = noisePosition - inputBuffer.position(); + int maybeSilenceBufferRemaining = maybeSilenceBuffer.length - maybeSilenceBufferSize; + if (noisePosition < limit && maybeSilenceInputSize < maybeSilenceBufferRemaining) { + // The maybe silence buffer isn't full, so output it and switch back to the noisy state. + output(maybeSilenceBuffer, maybeSilenceBufferSize); + maybeSilenceBufferSize = 0; + state = STATE_NOISY; + } else { + // Fill as much of the maybe silence buffer as possible. + int bytesToWrite = Math.min(maybeSilenceInputSize, maybeSilenceBufferRemaining); + inputBuffer.limit(inputBuffer.position() + bytesToWrite); + inputBuffer.get(maybeSilenceBuffer, maybeSilenceBufferSize, bytesToWrite); + maybeSilenceBufferSize += bytesToWrite; + if (maybeSilenceBufferSize == maybeSilenceBuffer.length) { + // We've reached a period of silence, so skip it, taking in to account padding for both + // the noisy to silent transition and any future silent to noisy transition. + if (hasOutputNoise) { + output(maybeSilenceBuffer, paddingSize); + skippedFrames += (maybeSilenceBufferSize - paddingSize * 2) / bytesPerFrame; + } else { + skippedFrames += (maybeSilenceBufferSize - paddingSize) / bytesPerFrame; + } + updatePaddingBuffer(inputBuffer, maybeSilenceBuffer, maybeSilenceBufferSize); + maybeSilenceBufferSize = 0; + state = STATE_SILENT; + } + + // Restore the limit. + inputBuffer.limit(limit); + } + } + + /** + * Incrementally processes new input from {@code inputBuffer} while in {@link #STATE_SILENT}, + * updating the state if needed. + */ + private void processSilence(ByteBuffer inputBuffer) { + int limit = inputBuffer.limit(); + int noisyPosition = findNoisePosition(inputBuffer); + inputBuffer.limit(noisyPosition); + skippedFrames += inputBuffer.remaining() / bytesPerFrame; + updatePaddingBuffer(inputBuffer, paddingBuffer, paddingSize); + if (noisyPosition < limit) { + // Output the padding, which may include previous input as well as new input, then transition + // back to the noisy state. + output(paddingBuffer, paddingSize); + state = STATE_NOISY; + + // Restore the limit. + inputBuffer.limit(limit); + } + } + + /** + * Copies {@code length} elements from {@code data} to populate a new output buffer from the + * processor. + */ + private void output(byte[] data, int length) { + replaceOutputBuffer(length).put(data, 0, length).flip(); + if (length > 0) { + hasOutputNoise = true; + } + } + + /** + * Copies remaining bytes from {@code data} to populate a new output buffer from the processor. + */ + private void output(ByteBuffer data) { + int length = data.remaining(); + replaceOutputBuffer(length).put(data).flip(); + if (length > 0) { + hasOutputNoise = true; + } + } + + /** + * Fills {@link #paddingBuffer} using data from {@code input}, plus any additional buffered data + * at the end of {@code buffer} (up to its {@code size}) required to fill it, advancing the input + * position. + */ + private void updatePaddingBuffer(ByteBuffer input, byte[] buffer, int size) { + int fromInputSize = Math.min(input.remaining(), paddingSize); + int fromBufferSize = paddingSize - fromInputSize; + System.arraycopy( + /* src= */ buffer, + /* srcPos= */ size - fromBufferSize, + /* dest= */ paddingBuffer, + /* destPos= */ 0, + /* length= */ fromBufferSize); + input.position(input.limit() - fromInputSize); + input.get(paddingBuffer, fromBufferSize, fromInputSize); + } + + /** + * Returns the number of input frames corresponding to {@code durationUs} microseconds of audio. + */ + private int durationUsToFrames(long durationUs) { + return (int) ((durationUs * inputAudioFormat.sampleRate) / C.MICROS_PER_SECOND); + } + + /** + * Returns the earliest byte position in [position, limit) of {@code buffer} that contains a frame + * classified as a noisy frame, or the limit of the buffer if no such frame exists. + */ + private int findNoisePosition(ByteBuffer buffer) { + // The input is in ByteOrder.nativeOrder(), which is little endian on Android. + for (int i = buffer.position() + 1; i < buffer.limit(); i += 2) { + if (Math.abs(buffer.get(i)) > SILENCE_THRESHOLD_LEVEL_MSB) { + // Round to the start of the frame. + return bytesPerFrame * (i / bytesPerFrame); + } + } + return buffer.limit(); + } + + /** + * Returns the earliest byte position in [position, limit) of {@code buffer} such that all frames + * from the byte position to the limit are classified as silent. + */ + private int findNoiseLimit(ByteBuffer buffer) { + // The input is in ByteOrder.nativeOrder(), which is little endian on Android. + for (int i = buffer.limit() - 1; i >= buffer.position(); i -= 2) { + if (Math.abs(buffer.get(i)) > SILENCE_THRESHOLD_LEVEL_MSB) { + // Return the start of the next frame. + return bytesPerFrame * (i / bytesPerFrame) + bytesPerFrame; + } + } + return buffer.position(); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 1a4ee446137e..5e86e0ad781f 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -17,21 +17,25 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.audio; import android.media.audiofx.Virtualizer; import android.os.Handler; -import android.os.Looper; import android.os.SystemClock; -import android.support.annotation.IntDef; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.BaseRenderer; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.PlayerMessage.Target; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities; import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderCounters; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.SimpleDecoder; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaCrypto; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; @@ -39,17 +43,36 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MediaClock; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TraceUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * Decodes and renders audio using a {@link SimpleDecoder}. + * + *

This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)} + * on the playback thread: + * + *

    + *
  • Message with type {@link C#MSG_SET_VOLUME} to set the volume. The message payload should be + * a {@link Float} with 0 being silence and 1 being unity gain. + *
  • Message with type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to set the audio attributes. The + * message payload should be an {@link org.mozilla.thirdparty.com.google.android.exoplayer2audio.AudioAttributes} + * instance that will configure the underlying audio track. + *
  • Message with type {@link C#MSG_SET_AUX_EFFECT_INFO} to set the auxiliary effect. The + * message payload should be an {@link AuxEffectInfo} instance that will configure the + * underlying audio track. + *
*/ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock { + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM, - REINITIALIZATION_STATE_WAIT_END_OF_STREAM}) + @IntDef({ + REINITIALIZATION_STATE_NONE, + REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM, + REINITIALIZATION_STATE_WAIT_END_OF_STREAM + }) private @interface ReinitializationState {} /** * The decoder does not need to be re-initialized. @@ -71,31 +94,34 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements private final DrmSessionManager drmSessionManager; private final boolean playClearSamplesWithoutKeys; private final EventDispatcher eventDispatcher; - private final AudioTrack audioTrack; - private final FormatHolder formatHolder; + private final AudioSink audioSink; private final DecoderInputBuffer flagsOnlyBuffer; + private boolean drmResourcesAcquired; private DecoderCounters decoderCounters; private Format inputFormat; + private int encoderDelay; + private int encoderPadding; private SimpleDecoder decoder; private DecoderInputBuffer inputBuffer; private SimpleOutputBuffer outputBuffer; - private DrmSession drmSession; - private DrmSession pendingDrmSession; + @Nullable private DrmSession decoderDrmSession; + @Nullable private DrmSession sourceDrmSession; @ReinitializationState private int decoderReinitializationState; private boolean decoderReceivedBuffers; private boolean audioTrackNeedsConfigure; private long currentPositionUs; + private boolean allowFirstBufferPositionDiscontinuity; private boolean allowPositionDiscontinuity; private boolean inputStreamEnded; private boolean outputStreamEnded; private boolean waitingForKeys; public SimpleDecoderAudioRenderer() { - this(null, null); + this(/* eventHandler= */ null, /* eventListener= */ null); } /** @@ -104,9 +130,17 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ - public SimpleDecoderAudioRenderer(Handler eventHandler, - AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) { - this(eventHandler, eventListener, null, null, false, audioProcessors); + public SimpleDecoderAudioRenderer( + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + AudioProcessor... audioProcessors) { + this( + eventHandler, + eventListener, + /* audioCapabilities= */ null, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + audioProcessors); } /** @@ -116,9 +150,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. */ - public SimpleDecoderAudioRenderer(Handler eventHandler, - AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) { - this(eventHandler, eventListener, audioCapabilities, null, false); + public SimpleDecoderAudioRenderer( + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + @Nullable AudioCapabilities audioCapabilities) { + this( + eventHandler, + eventListener, + audioCapabilities, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false); } /** @@ -136,52 +177,95 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * has obtained the keys necessary to decrypt encrypted regions of the media. * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ - public SimpleDecoderAudioRenderer(Handler eventHandler, - AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, - DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + public SimpleDecoderAudioRenderer( + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + @Nullable AudioCapabilities audioCapabilities, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, AudioProcessor... audioProcessors) { + this(eventHandler, eventListener, drmSessionManager, + playClearSamplesWithoutKeys, new DefaultAudioSink(audioCapabilities, audioProcessors)); + } + + /** + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param drmSessionManager For use with encrypted media. May be null if support for encrypted + * media is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param audioSink The sink to which audio will be output. + */ + public SimpleDecoderAudioRenderer( + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + AudioSink audioSink) { super(C.TRACK_TYPE_AUDIO); this.drmSessionManager = drmSessionManager; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; eventDispatcher = new EventDispatcher(eventHandler, eventListener); - audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener()); - formatHolder = new FormatHolder(); + this.audioSink = audioSink; + audioSink.setListener(new AudioSinkListener()); flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); decoderReinitializationState = REINITIALIZATION_STATE_NONE; audioTrackNeedsConfigure = true; } @Override + @Nullable public MediaClock getMediaClock() { return this; } @Override + @Capabilities public final int supportsFormat(Format format) { - int formatSupport = supportsFormatInternal(format); - if (formatSupport == FORMAT_UNSUPPORTED_TYPE || formatSupport == FORMAT_UNSUPPORTED_SUBTYPE) { - return formatSupport; + if (!MimeTypes.isAudio(format.sampleMimeType)) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } + @FormatSupport int formatSupport = supportsFormatInternal(drmSessionManager, format); + if (formatSupport <= FORMAT_UNSUPPORTED_DRM) { + return RendererCapabilities.create(formatSupport); + } + @TunnelingSupport int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; - return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport; + return RendererCapabilities.create(formatSupport, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport); } /** - * Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for - * {@link #supportsFormat(Format)}. + * Returns the {@link FormatSupport} for the given {@link Format}. * - * @param format The format. - * @return The extent to which the renderer supports the format itself. + * @param drmSessionManager The renderer's {@link DrmSessionManager}. + * @param format The format, which has an audio {@link Format#sampleMimeType}. + * @return The {@link FormatSupport} for this {@link Format}. */ - protected abstract int supportsFormatInternal(Format format); + @FormatSupport + protected abstract int supportsFormatInternal( + @Nullable DrmSessionManager drmSessionManager, Format format); + + /** + * Returns whether the sink supports the audio format. + * + * @see AudioSink#supportsOutput(int, int) + */ + protected final boolean supportsOutput(int channelCount, @C.Encoding int encoding) { + return audioSink.supportsOutput(channelCount, encoding); + } @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (outputStreamEnded) { try { - audioTrack.playToEndOfStream(); - } catch (AudioTrack.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + audioSink.playToEndOfStream(); + } catch (AudioSink.WriteException e) { + throw createRendererException(e, inputFormat); } return; } @@ -189,10 +273,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements // Try and read a format if we don't have one already. if (inputFormat == null) { // We don't have a format yet, so try and read one. + FormatHolder formatHolder = getFormatHolder(); flagsOnlyBuffer.clear(); int result = readSource(formatHolder, flagsOnlyBuffer, true); if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); + onInputFormatChanged(formatHolder); } else if (result == C.RESULT_BUFFER_READ) { // End of stream read having not read a format. Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); @@ -215,9 +300,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements while (drainOutputBuffer()) {} while (feedInputBuffer()) {} TraceUtil.endSection(); - } catch (AudioDecoderException | AudioTrack.ConfigurationException - | AudioTrack.InitializationException | AudioTrack.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + } catch (AudioDecoderException | AudioSink.ConfigurationException + | AudioSink.InitializationException | AudioSink.WriteException e) { + throw createRendererException(e, inputFormat); } decoderCounters.ensureUpdated(); } @@ -229,21 +314,21 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances * should be released in {@link #onDisabled()} (if not before). * - * @see AudioTrack.Listener#onAudioSessionId(int) + * @see AudioSink.Listener#onAudioSessionId(int) */ protected void onAudioSessionId(int audioSessionId) { // Do nothing. } /** - * @see AudioTrack.Listener#onPositionDiscontinuity() + * @see AudioSink.Listener#onPositionDiscontinuity() */ protected void onAudioTrackPositionDiscontinuity() { // Do nothing. } /** - * @see AudioTrack.Listener#onUnderrun(int, long, long) + * @see AudioSink.Listener#onUnderrun(int, long, long) */ protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { @@ -259,32 +344,40 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @return The decoder. * @throws AudioDecoderException If an error occurred creating a suitable decoder. */ - protected abstract SimpleDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) - throws AudioDecoderException; + protected abstract SimpleDecoder< + DecoderInputBuffer, ? extends SimpleOutputBuffer, ? extends AudioDecoderException> + createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) + throws AudioDecoderException; /** * Returns the format of audio buffers output by the decoder. Will not be called until the first * output buffer has been dequeued, so the decoder may use input data to determine the format. - *

- * The default implementation returns a 16-bit PCM format with the same channel count and sample - * rate as the input. */ - protected Format getOutputFormat() { - return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE, - Format.NO_VALUE, inputFormat.channelCount, inputFormat.sampleRate, C.ENCODING_PCM_16BIT, - null, null, 0, null); + protected abstract Format getOutputFormat(); + + /** + * Returns whether the existing decoder can be kept for a new format. + * + * @param oldFormat The previous format. + * @param newFormat The new format. + * @return True if the existing decoder can be kept. + */ + protected boolean canKeepCodec(Format oldFormat, Format newFormat) { + return false; } private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException, - AudioTrack.ConfigurationException, AudioTrack.InitializationException, - AudioTrack.WriteException { + AudioSink.ConfigurationException, AudioSink.InitializationException, + AudioSink.WriteException { if (outputBuffer == null) { outputBuffer = decoder.dequeueOutputBuffer(); if (outputBuffer == null) { return false; } - decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount; + if (outputBuffer.skippedOutputBufferCount > 0) { + decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount; + audioSink.handleDiscontinuity(); + } } if (outputBuffer.isEndOfStream()) { @@ -304,12 +397,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements if (audioTrackNeedsConfigure) { Format outputFormat = getOutputFormat(); - audioTrack.configure(outputFormat.sampleMimeType, outputFormat.channelCount, - outputFormat.sampleRate, outputFormat.pcmEncoding, 0); + audioSink.configure(outputFormat.pcmEncoding, outputFormat.channelCount, + outputFormat.sampleRate, 0, null, encoderDelay, encoderPadding); audioTrackNeedsConfigure = false; } - if (audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) { + if (audioSink.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) { decoderCounters.renderedOutputBufferCount++; outputBuffer.release(); outputBuffer = null; @@ -342,6 +435,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } int result; + FormatHolder formatHolder = getFormatHolder(); if (waitingForKeys) { // We've already read an encrypted sample into buffer, and are waiting for keys. result = C.RESULT_BUFFER_READ; @@ -353,7 +447,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return false; } if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); + onInputFormatChanged(formatHolder); return true; } if (inputBuffer.isEndOfStream()) { @@ -368,6 +462,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return false; } inputBuffer.flip(); + onQueueInputBuffer(inputBuffer); decoder.queueInputBuffer(inputBuffer); decoderReceivedBuffers = true; decoderCounters.inputBufferCount++; @@ -376,23 +471,25 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (drmSession == null) { + if (decoderDrmSession == null + || (!bufferEncrypted + && (playClearSamplesWithoutKeys || decoderDrmSession.playClearSamplesWithoutKeys()))) { return false; } - @DrmSession.State int drmSessionState = drmSession.getState(); + @DrmSession.State int drmSessionState = decoderDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + throw createRendererException(decoderDrmSession.getError(), inputFormat); } - return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS - && (bufferEncrypted || !playClearSamplesWithoutKeys); + return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } private void processEndOfStream() throws ExoPlaybackException { outputStreamEnded = true; try { - audioTrack.playToEndOfStream(); - } catch (AudioTrack.WriteException e) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + audioSink.playToEndOfStream(); + } catch (AudioSink.WriteException e) { + // TODO(internal: b/145658993) Use outputFormat for the call from drainOutputBuffer. + throw createRendererException(e, inputFormat); } } @@ -414,52 +511,54 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override public boolean isEnded() { - return outputStreamEnded && audioTrack.isEnded(); + return outputStreamEnded && audioSink.isEnded(); } @Override public boolean isReady() { - return audioTrack.hasPendingData() + return audioSink.hasPendingData() || (inputFormat != null && !waitingForKeys && (isSourceReady() || outputBuffer != null)); } @Override public long getPositionUs() { - long newCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded()); - if (newCurrentPositionUs != AudioTrack.CURRENT_POSITION_NOT_SET) { - currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs - : Math.max(currentPositionUs, newCurrentPositionUs); - allowPositionDiscontinuity = false; + if (getState() == STATE_STARTED) { + updateCurrentPosition(); } return currentPositionUs; } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - return audioTrack.setPlaybackParameters(playbackParameters); + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + audioSink.setPlaybackParameters(playbackParameters); } @Override public PlaybackParameters getPlaybackParameters() { - return audioTrack.getPlaybackParameters(); + return audioSink.getPlaybackParameters(); } @Override protected void onEnabled(boolean joining) throws ExoPlaybackException { + if (drmSessionManager != null && !drmResourcesAcquired) { + drmResourcesAcquired = true; + drmSessionManager.prepare(); + } decoderCounters = new DecoderCounters(); eventDispatcher.enabled(decoderCounters); int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId; if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { - audioTrack.enableTunnelingV21(tunnelingAudioSessionId); + audioSink.enableTunnelingV21(tunnelingAudioSessionId); } else { - audioTrack.disableTunneling(); + audioSink.disableTunneling(); } } @Override protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { - audioTrack.reset(); + audioSink.flush(); currentPositionUs = positionUs; + allowFirstBufferPositionDiscontinuity = true; allowPositionDiscontinuity = true; inputStreamEnded = false; outputStreamEnded = false; @@ -470,12 +569,13 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override protected void onStarted() { - audioTrack.play(); + audioSink.play(); } @Override protected void onStopped() { - audioTrack.pause(); + updateCurrentPosition(); + audioSink.pause(); } @Override @@ -484,25 +584,39 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements audioTrackNeedsConfigure = true; waitingForKeys = false; try { + setSourceDrmSession(null); releaseDecoder(); - audioTrack.release(); + audioSink.reset(); } finally { - try { - if (drmSession != null) { - drmSessionManager.releaseSession(drmSession); - } - } finally { - try { - if (pendingDrmSession != null && pendingDrmSession != drmSession) { - drmSessionManager.releaseSession(pendingDrmSession); - } - } finally { - drmSession = null; - pendingDrmSession = null; - decoderCounters.ensureUpdated(); - eventDispatcher.disabled(decoderCounters); - } - } + eventDispatcher.disabled(decoderCounters); + } + } + + @Override + protected void onReset() { + if (drmSessionManager != null && drmResourcesAcquired) { + drmResourcesAcquired = false; + drmSessionManager.release(); + } + } + + @Override + public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { + switch (messageType) { + case C.MSG_SET_VOLUME: + audioSink.setVolume((Float) message); + break; + case C.MSG_SET_AUDIO_ATTRIBUTES: + AudioAttributes audioAttributes = (AudioAttributes) message; + audioSink.setAudioAttributes(audioAttributes); + break; + case C.MSG_SET_AUX_EFFECT_INFO: + AuxEffectInfo auxEffectInfo = (AuxEffectInfo) message; + audioSink.setAuxEffectInfo(auxEffectInfo); + break; + default: + super.handleMessage(messageType, message); + break; } } @@ -511,18 +625,20 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return; } - drmSession = pendingDrmSession; + setDecoderDrmSession(sourceDrmSession); + ExoMediaCrypto mediaCrypto = null; - if (drmSession != null) { - @DrmSession.State int drmSessionState = drmSession.getState(); - if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); - } else if (drmSessionState == DrmSession.STATE_OPENED - || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) { - mediaCrypto = drmSession.getMediaCrypto(); - } else { - // The drm session isn't open yet. - return; + if (decoderDrmSession != null) { + mediaCrypto = decoderDrmSession.getMediaCrypto(); + if (mediaCrypto == null) { + DrmSessionException drmError = decoderDrmSession.getError(); + if (drmError != null) { + // Continue for now. We may be able to avoid failure if the session recovers, or if a new + // input format causes the session to be replaced before it's used. + } else { + // The drm session isn't open yet. + return; + } } } @@ -536,76 +652,87 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements codecInitializedTimestamp - codecInitializingTimestamp); decoderCounters.decoderInitCount++; } catch (AudioDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } } private void releaseDecoder() { - if (decoder == null) { - return; - } - inputBuffer = null; outputBuffer = null; - decoder.release(); - decoder = null; - decoderCounters.decoderReleaseCount++; decoderReinitializationState = REINITIALIZATION_STATE_NONE; decoderReceivedBuffers = false; + if (decoder != null) { + decoder.release(); + decoder = null; + decoderCounters.decoderReleaseCount++; + } + setDecoderDrmSession(null); } - private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { + private void setSourceDrmSession(@Nullable DrmSession session) { + DrmSession.replaceSession(sourceDrmSession, session); + sourceDrmSession = session; + } + + private void setDecoderDrmSession(@Nullable DrmSession session) { + DrmSession.replaceSession(decoderDrmSession, session); + decoderDrmSession = session; + } + + @SuppressWarnings("unchecked") + private void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + Format newFormat = Assertions.checkNotNull(formatHolder.format); + if (formatHolder.includesDrmSession) { + setSourceDrmSession((DrmSession) formatHolder.drmSession); + } else { + sourceDrmSession = + getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession); + } Format oldFormat = inputFormat; inputFormat = newFormat; - boolean drmInitDataChanged = !Util.areEqual(inputFormat.drmInitData, oldFormat == null ? null - : oldFormat.drmInitData); - if (drmInitDataChanged) { - if (inputFormat.drmInitData != null) { - if (drmSessionManager == null) { - throw ExoPlaybackException.createForRenderer( - new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); - } - pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), - inputFormat.drmInitData); - if (pendingDrmSession == drmSession) { - drmSessionManager.releaseSession(pendingDrmSession); - } + if (!canKeepCodec(oldFormat, inputFormat)) { + if (decoderReceivedBuffers) { + // Signal end of stream and wait for any final output buffers before re-initialization. + decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; } else { - pendingDrmSession = null; + // There aren't any final output buffers, so release the decoder immediately. + releaseDecoder(); + maybeInitDecoder(); + audioTrackNeedsConfigure = true; } } - if (decoderReceivedBuffers) { - // Signal end of stream and wait for any final output buffers before re-initialization. - decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; - } else { - // There aren't any final output buffers, so release the decoder immediately. - releaseDecoder(); - maybeInitDecoder(); - audioTrackNeedsConfigure = true; - } + encoderDelay = inputFormat.encoderDelay; + encoderPadding = inputFormat.encoderPadding; - eventDispatcher.inputFormatChanged(newFormat); + eventDispatcher.inputFormatChanged(inputFormat); } - @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - switch (messageType) { - case C.MSG_SET_VOLUME: - audioTrack.setVolume((Float) message); - break; - case C.MSG_SET_STREAM_TYPE: - @C.StreamType int streamType = (Integer) message; - audioTrack.setStreamType(streamType); - break; - default: - super.handleMessage(messageType, message); - break; + private void onQueueInputBuffer(DecoderInputBuffer buffer) { + if (allowFirstBufferPositionDiscontinuity && !buffer.isDecodeOnly()) { + // TODO: Remove this hack once we have a proper fix for [Internal: b/71876314]. + // Allow the position to jump if the first presentable input buffer has a timestamp that + // differs significantly from what was expected. + if (Math.abs(buffer.timeUs - currentPositionUs) > 500000) { + currentPositionUs = buffer.timeUs; + } + allowFirstBufferPositionDiscontinuity = false; } } - private final class AudioTrackListener implements AudioTrack.Listener { + private void updateCurrentPosition() { + long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded()); + if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) { + currentPositionUs = + allowPositionDiscontinuity + ? newCurrentPositionUs + : Math.max(currentPositionUs, newCurrentPositionUs); + allowPositionDiscontinuity = false; + } + } + + private final class AudioSinkListener implements AudioSink.Listener { @Override public void onAudioSessionId(int audioSessionId) { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Sonic.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Sonic.java index bce0f12c9de9..1a0dad4b457c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Sonic.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Sonic.java @@ -27,32 +27,30 @@ import java.util.Arrays; */ /* package */ final class Sonic { - private static final boolean USE_CHORD_PITCH = false; private static final int MINIMUM_PITCH = 65; private static final int MAXIMUM_PITCH = 400; private static final int AMDF_FREQUENCY = 4000; + private static final int BYTES_PER_SAMPLE = 2; - private final int sampleRate; - private final int numChannels; + private final int inputSampleRateHz; + private final int channelCount; + private final float speed; + private final float pitch; + private final float rate; private final int minPeriod; private final int maxPeriod; - private final int maxRequired; + private final int maxRequiredFrameCount; private final short[] downSampleBuffer; - private int inputBufferSize; private short[] inputBuffer; - private int outputBufferSize; + private int inputFrameCount; private short[] outputBuffer; - private int pitchBufferSize; + private int outputFrameCount; private short[] pitchBuffer; + private int pitchFrameCount; private int oldRatePosition; private int newRatePosition; - private float speed; - private float pitch; - private int numInputSamples; - private int numOutputSamples; - private int numPitchSamples; - private int remainingInputToCopy; + private int remainingInputToCopyFrameCount; private int prevPeriod; private int prevMinDiff; private int minDiff; @@ -61,55 +59,26 @@ import java.util.Arrays; /** * Creates a new Sonic audio stream processor. * - * @param sampleRate The sample rate of input audio. - * @param numChannels The number of channels in the input audio. + * @param inputSampleRateHz The sample rate of input audio, in hertz. + * @param channelCount The number of channels in the input audio. + * @param speed The speedup factor for output audio. + * @param pitch The pitch factor for output audio. + * @param outputSampleRateHz The sample rate for output audio, in hertz. */ - public Sonic(int sampleRate, int numChannels) { - this.sampleRate = sampleRate; - this.numChannels = numChannels; - minPeriod = sampleRate / MAXIMUM_PITCH; - maxPeriod = sampleRate / MINIMUM_PITCH; - maxRequired = 2 * maxPeriod; - downSampleBuffer = new short[maxRequired]; - inputBufferSize = maxRequired; - inputBuffer = new short[maxRequired * numChannels]; - outputBufferSize = maxRequired; - outputBuffer = new short[maxRequired * numChannels]; - pitchBufferSize = maxRequired; - pitchBuffer = new short[maxRequired * numChannels]; - oldRatePosition = 0; - newRatePosition = 0; - prevPeriod = 0; - speed = 1.0f; - pitch = 1.0f; - } - - /** - * Sets the output speed. - */ - public void setSpeed(float speed) { + public Sonic( + int inputSampleRateHz, int channelCount, float speed, float pitch, int outputSampleRateHz) { + this.inputSampleRateHz = inputSampleRateHz; + this.channelCount = channelCount; this.speed = speed; - } - - /** - * Gets the output speed. - */ - public float getSpeed() { - return speed; - } - - /** - * Sets the output pitch. - */ - public void setPitch(float pitch) { this.pitch = pitch; - } - - /** - * Gets the output pitch. - */ - public float getPitch() { - return pitch; + rate = (float) inputSampleRateHz / outputSampleRateHz; + minPeriod = inputSampleRateHz / MAXIMUM_PITCH; + maxPeriod = inputSampleRateHz / MINIMUM_PITCH; + maxRequiredFrameCount = 2 * maxPeriod; + downSampleBuffer = new short[maxRequiredFrameCount]; + inputBuffer = new short[maxRequiredFrameCount * channelCount]; + outputBuffer = new short[maxRequiredFrameCount * channelCount]; + pitchBuffer = new short[maxRequiredFrameCount * channelCount]; } /** @@ -119,11 +88,11 @@ import java.util.Arrays; * @param buffer A {@link ShortBuffer} containing input data between its position and limit. */ public void queueInput(ShortBuffer buffer) { - int samplesToWrite = buffer.remaining() / numChannels; - int bytesToWrite = samplesToWrite * numChannels * 2; - enlargeInputBufferIfNeeded(samplesToWrite); - buffer.get(inputBuffer, numInputSamples * numChannels, bytesToWrite / 2); - numInputSamples += samplesToWrite; + int framesToWrite = buffer.remaining() / channelCount; + int bytesToWrite = framesToWrite * channelCount * 2; + inputBuffer = ensureSpaceForAdditionalFrames(inputBuffer, inputFrameCount, framesToWrite); + buffer.get(inputBuffer, inputFrameCount * channelCount, bytesToWrite / 2); + inputFrameCount += framesToWrite; processStreamInput(); } @@ -134,11 +103,15 @@ import java.util.Arrays; * @param buffer A {@link ShortBuffer} into which output will be written. */ public void getOutput(ShortBuffer buffer) { - int samplesToRead = Math.min(buffer.remaining() / numChannels, numOutputSamples); - buffer.put(outputBuffer, 0, samplesToRead * numChannels); - numOutputSamples -= samplesToRead; - System.arraycopy(outputBuffer, samplesToRead * numChannels, outputBuffer, 0, - numOutputSamples * numChannels); + int framesToRead = Math.min(buffer.remaining() / channelCount, outputFrameCount); + buffer.put(outputBuffer, 0, framesToRead * channelCount); + outputFrameCount -= framesToRead; + System.arraycopy( + outputBuffer, + framesToRead * channelCount, + outputBuffer, + 0, + outputFrameCount * channelCount); } /** @@ -146,79 +119,105 @@ import java.util.Arrays; * added to the output, but flushing in the middle of words could introduce distortion. */ public void queueEndOfStream() { - int remainingSamples = numInputSamples; + int remainingFrameCount = inputFrameCount; float s = speed / pitch; - int expectedOutputSamples = - numOutputSamples + (int) ((remainingSamples / s + numPitchSamples) / pitch + 0.5f); + float r = rate * pitch; + int expectedOutputFrames = + outputFrameCount + (int) ((remainingFrameCount / s + pitchFrameCount) / r + 0.5f); // Add enough silence to flush both input and pitch buffers. - enlargeInputBufferIfNeeded(remainingSamples + 2 * maxRequired); - for (int xSample = 0; xSample < 2 * maxRequired * numChannels; xSample++) { - inputBuffer[remainingSamples * numChannels + xSample] = 0; + inputBuffer = + ensureSpaceForAdditionalFrames( + inputBuffer, inputFrameCount, remainingFrameCount + 2 * maxRequiredFrameCount); + for (int xSample = 0; xSample < 2 * maxRequiredFrameCount * channelCount; xSample++) { + inputBuffer[remainingFrameCount * channelCount + xSample] = 0; } - numInputSamples += 2 * maxRequired; + inputFrameCount += 2 * maxRequiredFrameCount; processStreamInput(); - // Throw away any extra samples we generated due to the silence we added. - if (numOutputSamples > expectedOutputSamples) { - numOutputSamples = expectedOutputSamples; + // Throw away any extra frames we generated due to the silence we added. + if (outputFrameCount > expectedOutputFrames) { + outputFrameCount = expectedOutputFrames; } // Empty input and pitch buffers. - numInputSamples = 0; - remainingInputToCopy = 0; - numPitchSamples = 0; + inputFrameCount = 0; + remainingInputToCopyFrameCount = 0; + pitchFrameCount = 0; } - /** - * Returns the number of output samples that can be read with {@link #getOutput(ShortBuffer)}. - */ - public int getSamplesAvailable() { - return numOutputSamples; + /** Clears state in preparation for receiving a new stream of input buffers. */ + public void flush() { + inputFrameCount = 0; + outputFrameCount = 0; + pitchFrameCount = 0; + oldRatePosition = 0; + newRatePosition = 0; + remainingInputToCopyFrameCount = 0; + prevPeriod = 0; + prevMinDiff = 0; + minDiff = 0; + maxDiff = 0; + } + + /** Returns the size of output that can be read with {@link #getOutput(ShortBuffer)}, in bytes. */ + public int getOutputSize() { + return outputFrameCount * channelCount * BYTES_PER_SAMPLE; } // Internal methods. - private void enlargeOutputBufferIfNeeded(int numSamples) { - if (numOutputSamples + numSamples > outputBufferSize) { - outputBufferSize += (outputBufferSize / 2) + numSamples; - outputBuffer = Arrays.copyOf(outputBuffer, outputBufferSize * numChannels); + /** + * Returns {@code buffer} or a copy of it, such that there is enough space in the returned buffer + * to store {@code newFrameCount} additional frames. + * + * @param buffer The buffer. + * @param frameCount The number of frames already in the buffer. + * @param additionalFrameCount The number of additional frames that need to be stored in the + * buffer. + * @return A buffer with enough space for the additional frames. + */ + private short[] ensureSpaceForAdditionalFrames( + short[] buffer, int frameCount, int additionalFrameCount) { + int currentCapacityFrames = buffer.length / channelCount; + if (frameCount + additionalFrameCount <= currentCapacityFrames) { + return buffer; + } else { + int newCapacityFrames = 3 * currentCapacityFrames / 2 + additionalFrameCount; + return Arrays.copyOf(buffer, newCapacityFrames * channelCount); } } - private void enlargeInputBufferIfNeeded(int numSamples) { - if (numInputSamples + numSamples > inputBufferSize) { - inputBufferSize += (inputBufferSize / 2) + numSamples; - inputBuffer = Arrays.copyOf(inputBuffer, inputBufferSize * numChannels); - } + private void removeProcessedInputFrames(int positionFrames) { + int remainingFrames = inputFrameCount - positionFrames; + System.arraycopy( + inputBuffer, positionFrames * channelCount, inputBuffer, 0, remainingFrames * channelCount); + inputFrameCount = remainingFrames; } - private void removeProcessedInputSamples(int position) { - int remainingSamples = numInputSamples - position; - System.arraycopy(inputBuffer, position * numChannels, inputBuffer, 0, - remainingSamples * numChannels); - numInputSamples = remainingSamples; + private void copyToOutput(short[] samples, int positionFrames, int frameCount) { + outputBuffer = ensureSpaceForAdditionalFrames(outputBuffer, outputFrameCount, frameCount); + System.arraycopy( + samples, + positionFrames * channelCount, + outputBuffer, + outputFrameCount * channelCount, + frameCount * channelCount); + outputFrameCount += frameCount; } - private void copyToOutput(short[] samples, int position, int numSamples) { - enlargeOutputBufferIfNeeded(numSamples); - System.arraycopy(samples, position * numChannels, outputBuffer, numOutputSamples * numChannels, - numSamples * numChannels); - numOutputSamples += numSamples; - } - - private int copyInputToOutput(int position) { - int numSamples = Math.min(maxRequired, remainingInputToCopy); - copyToOutput(inputBuffer, position, numSamples); - remainingInputToCopy -= numSamples; - return numSamples; + private int copyInputToOutput(int positionFrames) { + int frameCount = Math.min(maxRequiredFrameCount, remainingInputToCopyFrameCount); + copyToOutput(inputBuffer, positionFrames, frameCount); + remainingInputToCopyFrameCount -= frameCount; + return frameCount; } private void downSampleInput(short[] samples, int position, int skip) { // If skip is greater than one, average skip samples together and write them to the down-sample - // buffer. If numChannels is greater than one, mix the channels together as we down sample. - int numSamples = maxRequired / skip; - int samplesPerValue = numChannels * skip; - position *= numChannels; - for (int i = 0; i < numSamples; i++) { + // buffer. If channelCount is greater than one, mix the channels together as we down sample. + int frameCount = maxRequiredFrameCount / skip; + int samplesPerValue = channelCount * skip; + position *= channelCount; + for (int i = 0; i < frameCount; i++) { int value = 0; for (int j = 0; j < samplesPerValue; j++) { value += samples[position + i * samplesPerValue + j]; @@ -235,13 +234,13 @@ import java.util.Arrays; int worstPeriod = 255; int minDiff = 1; int maxDiff = 0; - position *= numChannels; + position *= channelCount; for (int period = minPeriod; period <= maxPeriod; period++) { int diff = 0; for (int i = 0; i < period; i++) { short sVal = samples[position + i]; short pVal = samples[position + period + i]; - diff += sVal >= pVal ? sVal - pVal : pVal - sVal; + diff += Math.abs(sVal - pVal); } // Note that the highest number of samples we add into diff will be less than 256, since we // skip samples. Thus, diff is a 24 bit number, and we can safely multiply by numSamples @@ -264,36 +263,30 @@ import java.util.Arrays; * Returns whether the previous pitch period estimate is a better approximation, which can occur * at the abrupt end of voiced words. */ - private boolean previousPeriodBetter(int minDiff, int maxDiff, boolean preferNewPeriod) { + private boolean previousPeriodBetter(int minDiff, int maxDiff) { if (minDiff == 0 || prevPeriod == 0) { return false; } - if (preferNewPeriod) { - if (maxDiff > minDiff * 3) { - // Got a reasonable match this period - return false; - } - if (minDiff * 2 <= prevMinDiff * 3) { - // Mismatch is not that much greater this period - return false; - } - } else { - if (minDiff <= prevMinDiff) { - return false; - } + if (maxDiff > minDiff * 3) { + // Got a reasonable match this period. + return false; + } + if (minDiff * 2 <= prevMinDiff * 3) { + // Mismatch is not that much greater this period. + return false; } return true; } - private int findPitchPeriod(short[] samples, int position, boolean preferNewPeriod) { + private int findPitchPeriod(short[] samples, int position) { // Find the pitch period. This is a critical step, and we may have to try multiple ways to get a // good answer. This version uses AMDF. To improve speed, we down sample by an integer factor // get in the 11 kHz range, and then do it again with a narrower frequency range without down // sampling. int period; int retPeriod; - int skip = sampleRate > AMDF_FREQUENCY ? sampleRate / AMDF_FREQUENCY : 1; - if (numChannels == 1 && skip == 1) { + int skip = inputSampleRateHz > AMDF_FREQUENCY ? inputSampleRateHz / AMDF_FREQUENCY : 1; + if (channelCount == 1 && skip == 1) { period = findPitchPeriodInRange(samples, position, minPeriod, maxPeriod); } else { downSampleInput(samples, position, skip); @@ -308,7 +301,7 @@ import java.util.Arrays; if (maxP > maxPeriod) { maxP = maxPeriod; } - if (numChannels == 1) { + if (channelCount == 1) { period = findPitchPeriodInRange(samples, position, minP, maxP); } else { downSampleInput(samples, position, 1); @@ -316,7 +309,7 @@ import java.util.Arrays; } } } - if (previousPeriodBetter(minDiff, maxDiff, preferNewPeriod)) { + if (previousPeriodBetter(minDiff, maxDiff)) { retPeriod = prevPeriod; } else { retPeriod = period; @@ -326,56 +319,35 @@ import java.util.Arrays; return retPeriod; } - private void moveNewSamplesToPitchBuffer(int originalNumOutputSamples) { - int numSamples = numOutputSamples - originalNumOutputSamples; - if (numPitchSamples + numSamples > pitchBufferSize) { - pitchBufferSize += (pitchBufferSize / 2) + numSamples; - pitchBuffer = Arrays.copyOf(pitchBuffer, pitchBufferSize * numChannels); - } - System.arraycopy(outputBuffer, originalNumOutputSamples * numChannels, pitchBuffer, - numPitchSamples * numChannels, numSamples * numChannels); - numOutputSamples = originalNumOutputSamples; - numPitchSamples += numSamples; + private void moveNewSamplesToPitchBuffer(int originalOutputFrameCount) { + int frameCount = outputFrameCount - originalOutputFrameCount; + pitchBuffer = ensureSpaceForAdditionalFrames(pitchBuffer, pitchFrameCount, frameCount); + System.arraycopy( + outputBuffer, + originalOutputFrameCount * channelCount, + pitchBuffer, + pitchFrameCount * channelCount, + frameCount * channelCount); + outputFrameCount = originalOutputFrameCount; + pitchFrameCount += frameCount; } - private void removePitchSamples(int numSamples) { - if (numSamples == 0) { + private void removePitchFrames(int frameCount) { + if (frameCount == 0) { return; } - System.arraycopy(pitchBuffer, numSamples * numChannels, pitchBuffer, 0, - (numPitchSamples - numSamples) * numChannels); - numPitchSamples -= numSamples; - } - - private void adjustPitch(int originalNumOutputSamples) { - // Latency due to pitch changes could be reduced by looking at past samples to determine pitch, - // rather than future. - if (numOutputSamples == originalNumOutputSamples) { - return; - } - moveNewSamplesToPitchBuffer(originalNumOutputSamples); - int position = 0; - while (numPitchSamples - position >= maxRequired) { - int period = findPitchPeriod(pitchBuffer, position, false); - int newPeriod = (int) (period / pitch); - enlargeOutputBufferIfNeeded(newPeriod); - if (pitch >= 1.0f) { - overlapAdd(newPeriod, numChannels, outputBuffer, numOutputSamples, pitchBuffer, position, - pitchBuffer, position + period - newPeriod); - } else { - int separation = newPeriod - period; - overlapAddWithSeparation(period, numChannels, separation, outputBuffer, numOutputSamples, - pitchBuffer, position, pitchBuffer, position); - } - numOutputSamples += newPeriod; - position += period; - } - removePitchSamples(position); + System.arraycopy( + pitchBuffer, + frameCount * channelCount, + pitchBuffer, + 0, + (pitchFrameCount - frameCount) * channelCount); + pitchFrameCount -= frameCount; } private short interpolate(short[] in, int inPos, int oldSampleRate, int newSampleRate) { - short left = in[inPos * numChannels]; - short right = in[inPos * numChannels + numChannels]; + short left = in[inPos]; + short right = in[inPos + channelCount]; int position = newRatePosition * oldSampleRate; int leftPosition = oldRatePosition * newSampleRate; int rightPosition = (oldRatePosition + 1) * newSampleRate; @@ -384,28 +356,30 @@ import java.util.Arrays; return (short) ((ratio * left + (width - ratio) * right) / width); } - private void adjustRate(float rate, int originalNumOutputSamples) { - if (numOutputSamples == originalNumOutputSamples) { + private void adjustRate(float rate, int originalOutputFrameCount) { + if (outputFrameCount == originalOutputFrameCount) { return; } - int newSampleRate = (int) (sampleRate / rate); - int oldSampleRate = sampleRate; + int newSampleRate = (int) (inputSampleRateHz / rate); + int oldSampleRate = inputSampleRateHz; // Set these values to help with the integer math. while (newSampleRate > (1 << 14) || oldSampleRate > (1 << 14)) { newSampleRate /= 2; oldSampleRate /= 2; } - moveNewSamplesToPitchBuffer(originalNumOutputSamples); + moveNewSamplesToPitchBuffer(originalOutputFrameCount); // Leave at least one pitch sample in the buffer. - for (int position = 0; position < numPitchSamples - 1; position++) { + for (int position = 0; position < pitchFrameCount - 1; position++) { while ((oldRatePosition + 1) * newSampleRate > newRatePosition * oldSampleRate) { - enlargeOutputBufferIfNeeded(1); - for (int i = 0; i < numChannels; i++) { - outputBuffer[numOutputSamples * numChannels + i] = - interpolate(pitchBuffer, position + i, oldSampleRate, newSampleRate); + outputBuffer = + ensureSpaceForAdditionalFrames( + outputBuffer, outputFrameCount, /* additionalFrameCount= */ 1); + for (int i = 0; i < channelCount; i++) { + outputBuffer[outputFrameCount * channelCount + i] = + interpolate(pitchBuffer, position * channelCount + i, oldSampleRate, newSampleRate); } newRatePosition++; - numOutputSamples++; + outputFrameCount++; } oldRatePosition++; if (oldRatePosition == oldSampleRate) { @@ -414,119 +388,117 @@ import java.util.Arrays; newRatePosition = 0; } } - removePitchSamples(numPitchSamples - 1); + removePitchFrames(pitchFrameCount - 1); } private int skipPitchPeriod(short[] samples, int position, float speed, int period) { // Skip over a pitch period, and copy period/speed samples to the output. - int newSamples; + int newFrameCount; if (speed >= 2.0f) { - newSamples = (int) (period / (speed - 1.0f)); + newFrameCount = (int) (period / (speed - 1.0f)); } else { - newSamples = period; - remainingInputToCopy = (int) (period * (2.0f - speed) / (speed - 1.0f)); + newFrameCount = period; + remainingInputToCopyFrameCount = (int) (period * (2.0f - speed) / (speed - 1.0f)); } - enlargeOutputBufferIfNeeded(newSamples); - overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples, samples, position, samples, + outputBuffer = ensureSpaceForAdditionalFrames(outputBuffer, outputFrameCount, newFrameCount); + overlapAdd( + newFrameCount, + channelCount, + outputBuffer, + outputFrameCount, + samples, + position, + samples, position + period); - numOutputSamples += newSamples; - return newSamples; + outputFrameCount += newFrameCount; + return newFrameCount; } private int insertPitchPeriod(short[] samples, int position, float speed, int period) { // Insert a pitch period, and determine how much input to copy directly. - int newSamples; + int newFrameCount; if (speed < 0.5f) { - newSamples = (int) (period * speed / (1.0f - speed)); + newFrameCount = (int) (period * speed / (1.0f - speed)); } else { - newSamples = period; - remainingInputToCopy = (int) (period * (2.0f * speed - 1.0f) / (1.0f - speed)); + newFrameCount = period; + remainingInputToCopyFrameCount = (int) (period * (2.0f * speed - 1.0f) / (1.0f - speed)); } - enlargeOutputBufferIfNeeded(period + newSamples); - System.arraycopy(samples, position * numChannels, outputBuffer, numOutputSamples * numChannels, - period * numChannels); - overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples + period, samples, - position + period, samples, position); - numOutputSamples += period + newSamples; - return newSamples; + outputBuffer = + ensureSpaceForAdditionalFrames(outputBuffer, outputFrameCount, period + newFrameCount); + System.arraycopy( + samples, + position * channelCount, + outputBuffer, + outputFrameCount * channelCount, + period * channelCount); + overlapAdd( + newFrameCount, + channelCount, + outputBuffer, + outputFrameCount + period, + samples, + position + period, + samples, + position); + outputFrameCount += period + newFrameCount; + return newFrameCount; } private void changeSpeed(float speed) { - if (numInputSamples < maxRequired) { + if (inputFrameCount < maxRequiredFrameCount) { return; } - int numSamples = numInputSamples; - int position = 0; + int frameCount = inputFrameCount; + int positionFrames = 0; do { - if (remainingInputToCopy > 0) { - position += copyInputToOutput(position); + if (remainingInputToCopyFrameCount > 0) { + positionFrames += copyInputToOutput(positionFrames); } else { - int period = findPitchPeriod(inputBuffer, position, true); + int period = findPitchPeriod(inputBuffer, positionFrames); if (speed > 1.0) { - position += period + skipPitchPeriod(inputBuffer, position, speed, period); + positionFrames += period + skipPitchPeriod(inputBuffer, positionFrames, speed, period); } else { - position += insertPitchPeriod(inputBuffer, position, speed, period); + positionFrames += insertPitchPeriod(inputBuffer, positionFrames, speed, period); } } - } while (position + maxRequired <= numSamples); - removeProcessedInputSamples(position); + } while (positionFrames + maxRequiredFrameCount <= frameCount); + removeProcessedInputFrames(positionFrames); } private void processStreamInput() { // Resample as many pitch periods as we have buffered on the input. - int originalNumOutputSamples = numOutputSamples; + int originalOutputFrameCount = outputFrameCount; float s = speed / pitch; + float r = rate * pitch; if (s > 1.00001 || s < 0.99999) { changeSpeed(s); } else { - copyToOutput(inputBuffer, 0, numInputSamples); - numInputSamples = 0; + copyToOutput(inputBuffer, 0, inputFrameCount); + inputFrameCount = 0; } - if (USE_CHORD_PITCH) { - if (pitch != 1.0f) { - adjustPitch(originalNumOutputSamples); - } - } else if (!USE_CHORD_PITCH && pitch != 1.0f) { - adjustRate(pitch, originalNumOutputSamples); + if (r != 1.0f) { + adjustRate(r, originalOutputFrameCount); } } - private static void overlapAdd(int numSamples, int numChannels, short[] out, int outPos, - short[] rampDown, int rampDownPos, short[] rampUp, int rampUpPos) { - for (int i = 0; i < numChannels; i++) { - int o = outPos * numChannels + i; - int u = rampUpPos * numChannels + i; - int d = rampDownPos * numChannels + i; - for (int t = 0; t < numSamples; t++) { - out[o] = (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * t) / numSamples); - o += numChannels; - d += numChannels; - u += numChannels; - } - } - } - - private static void overlapAddWithSeparation(int numSamples, int numChannels, int separation, - short[] out, int outPos, short[] rampDown, int rampDownPos, short[] rampUp, int rampUpPos) { - for (int i = 0; i < numChannels; i++) { - int o = outPos * numChannels + i; - int u = rampUpPos * numChannels + i; - int d = rampDownPos * numChannels + i; - for (int t = 0; t < numSamples + separation; t++) { - if (t < separation) { - out[o] = (short) (rampDown[d] * (numSamples - t) / numSamples); - d += numChannels; - } else if (t < numSamples) { - out[o] = - (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * (t - separation)) - / numSamples); - d += numChannels; - u += numChannels; - } else { - out[o] = (short) (rampUp[u] * (t - separation) / numSamples); - u += numChannels; - } - o += numChannels; + private static void overlapAdd( + int frameCount, + int channelCount, + short[] out, + int outPosition, + short[] rampDown, + int rampDownPosition, + short[] rampUp, + int rampUpPosition) { + for (int i = 0; i < channelCount; i++) { + int o = outPosition * channelCount + i; + int u = rampUpPosition * channelCount + i; + int d = rampDownPosition * channelCount + i; + for (int t = 0; t < frameCount; t++) { + out[o] = (short) ((rampDown[d] * (frameCount - t) + rampUp[u] * t) / frameCount); + o += channelCount; + d += channelCount; + u += channelCount; } } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SonicAudioProcessor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SonicAudioProcessor.java index ceeb91953238..88a4d884bf17 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SonicAudioProcessor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SonicAudioProcessor.java @@ -15,16 +15,17 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.audio; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; -import org.mozilla.thirdparty.com.google.android.exoplayer2.C.Encoding; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.ShortBuffer; /** - * An {@link AudioProcessor} that uses the Sonic library to modify the speed/pitch of audio. + * An {@link AudioProcessor} that uses the Sonic library to modify audio speed/pitch/sample rate. */ public final class SonicAudioProcessor implements AudioProcessor { @@ -44,19 +45,33 @@ public final class SonicAudioProcessor implements AudioProcessor { * The minimum allowed pitch in {@link #setPitch(float)}. */ public static final float MINIMUM_PITCH = 0.1f; + /** + * Indicates that the output sample rate should be the same as the input. + */ + public static final int SAMPLE_RATE_NO_CHANGE = -1; /** * The threshold below which the difference between two pitch/speed factors is negligible. */ private static final float CLOSE_THRESHOLD = 0.01f; - private int channelCount; - private int sampleRateHz; + /** + * The minimum number of output bytes at which the speedup is calculated using the input/output + * byte counts, rather than using the current playback parameters speed. + */ + private static final int MIN_BYTES_FOR_SPEEDUP_CALCULATION = 1024; - private Sonic sonic; + private int pendingOutputSampleRate; private float speed; private float pitch; + private AudioFormat pendingInputAudioFormat; + private AudioFormat pendingOutputAudioFormat; + private AudioFormat inputAudioFormat; + private AudioFormat outputAudioFormat; + + private boolean pendingSonicRecreation; + @Nullable private Sonic sonic; private ByteBuffer buffer; private ShortBuffer shortBuffer; private ByteBuffer outputBuffer; @@ -70,80 +85,110 @@ public final class SonicAudioProcessor implements AudioProcessor { public SonicAudioProcessor() { speed = 1f; pitch = 1f; - channelCount = Format.NO_VALUE; - sampleRateHz = Format.NO_VALUE; + pendingInputAudioFormat = AudioFormat.NOT_SET; + pendingOutputAudioFormat = AudioFormat.NOT_SET; + inputAudioFormat = AudioFormat.NOT_SET; + outputAudioFormat = AudioFormat.NOT_SET; buffer = EMPTY_BUFFER; shortBuffer = buffer.asShortBuffer(); outputBuffer = EMPTY_BUFFER; + pendingOutputSampleRate = SAMPLE_RATE_NO_CHANGE; } /** - * Sets the playback speed. The new speed will take effect after a call to {@link #flush()}. + * Sets the playback speed. This method may only be called after draining data through the + * processor. The value returned by {@link #isActive()} may change, and the processor must be + * {@link #flush() flushed} before queueing more data. * * @param speed The requested new playback speed. * @return The actual new playback speed. */ public float setSpeed(float speed) { - this.speed = Util.constrainValue(speed, MINIMUM_SPEED, MAXIMUM_SPEED); - return this.speed; + speed = Util.constrainValue(speed, MINIMUM_SPEED, MAXIMUM_SPEED); + if (this.speed != speed) { + this.speed = speed; + pendingSonicRecreation = true; + } + return speed; } /** - * Sets the playback pitch. The new pitch will take effect after a call to {@link #flush()}. + * Sets the playback pitch. This method may only be called after draining data through the + * processor. The value returned by {@link #isActive()} may change, and the processor must be + * {@link #flush() flushed} before queueing more data. * * @param pitch The requested new pitch. * @return The actual new pitch. */ public float setPitch(float pitch) { - this.pitch = Util.constrainValue(pitch, MINIMUM_PITCH, MAXIMUM_PITCH); + pitch = Util.constrainValue(pitch, MINIMUM_PITCH, MAXIMUM_PITCH); + if (this.pitch != pitch) { + this.pitch = pitch; + pendingSonicRecreation = true; + } return pitch; } /** - * Returns the number of bytes of input queued since the last call to {@link #flush()}. + * Sets the sample rate for output audio, in Hertz. Pass {@link #SAMPLE_RATE_NO_CHANGE} to output + * audio at the same sample rate as the input. After calling this method, call {@link + * #configure(AudioFormat)} to configure the processor with the new sample rate. + * + * @param sampleRateHz The sample rate for output audio, in Hertz. + * @see #configure(AudioFormat) */ - public long getInputByteCount() { - return inputBytes; + public void setOutputSampleRateHz(int sampleRateHz) { + pendingOutputSampleRate = sampleRateHz; } /** - * Returns the number of bytes of output dequeued since the last call to {@link #flush()}. + * Returns the specified duration scaled to take into account the speedup factor of this instance, + * in the same units as {@code duration}. + * + * @param duration The duration to scale taking into account speedup. + * @return The specified duration scaled to take into account speedup, in the same units as + * {@code duration}. */ - public long getOutputByteCount() { - return outputBytes; + public long scaleDurationForSpeedup(long duration) { + if (outputBytes >= MIN_BYTES_FOR_SPEEDUP_CALCULATION) { + return outputAudioFormat.sampleRate == inputAudioFormat.sampleRate + ? Util.scaleLargeTimestamp(duration, inputBytes, outputBytes) + : Util.scaleLargeTimestamp( + duration, + inputBytes * outputAudioFormat.sampleRate, + outputBytes * inputAudioFormat.sampleRate); + } else { + return (long) ((double) speed * duration); + } } @Override - public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding) - throws UnhandledFormatException { - if (encoding != C.ENCODING_PCM_16BIT) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + public AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException { + if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) { + throw new UnhandledAudioFormatException(inputAudioFormat); } - if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) { - return false; - } - this.sampleRateHz = sampleRateHz; - this.channelCount = channelCount; - return true; + int outputSampleRateHz = + pendingOutputSampleRate == SAMPLE_RATE_NO_CHANGE + ? inputAudioFormat.sampleRate + : pendingOutputSampleRate; + pendingInputAudioFormat = inputAudioFormat; + pendingOutputAudioFormat = + new AudioFormat(outputSampleRateHz, inputAudioFormat.channelCount, C.ENCODING_PCM_16BIT); + pendingSonicRecreation = true; + return pendingOutputAudioFormat; } @Override public boolean isActive() { - return Math.abs(speed - 1f) >= CLOSE_THRESHOLD || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD; - } - - @Override - public int getOutputChannelCount() { - return channelCount; - } - - @Override - public int getOutputEncoding() { - return C.ENCODING_PCM_16BIT; + return pendingOutputAudioFormat.sampleRate != Format.NO_VALUE + && (Math.abs(speed - 1f) >= CLOSE_THRESHOLD + || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD + || pendingOutputAudioFormat.sampleRate != pendingInputAudioFormat.sampleRate); } @Override public void queueInput(ByteBuffer inputBuffer) { + Sonic sonic = Assertions.checkNotNull(this.sonic); if (inputBuffer.hasRemaining()) { ShortBuffer shortBuffer = inputBuffer.asShortBuffer(); int inputSize = inputBuffer.remaining(); @@ -151,7 +196,7 @@ public final class SonicAudioProcessor implements AudioProcessor { sonic.queueInput(shortBuffer); inputBuffer.position(inputBuffer.position() + inputSize); } - int outputSize = sonic.getSamplesAvailable() * channelCount * 2; + int outputSize = sonic.getOutputSize(); if (outputSize > 0) { if (buffer.capacity() < outputSize) { buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder()); @@ -169,7 +214,9 @@ public final class SonicAudioProcessor implements AudioProcessor { @Override public void queueEndOfStream() { - sonic.queueEndOfStream(); + if (sonic != null) { + sonic.queueEndOfStream(); + } inputEnded = true; } @@ -182,14 +229,26 @@ public final class SonicAudioProcessor implements AudioProcessor { @Override public boolean isEnded() { - return inputEnded && (sonic == null || sonic.getSamplesAvailable() == 0); + return inputEnded && (sonic == null || sonic.getOutputSize() == 0); } @Override public void flush() { - sonic = new Sonic(sampleRateHz, channelCount); - sonic.setSpeed(speed); - sonic.setPitch(pitch); + if (isActive()) { + inputAudioFormat = pendingInputAudioFormat; + outputAudioFormat = pendingOutputAudioFormat; + if (pendingSonicRecreation) { + sonic = + new Sonic( + inputAudioFormat.sampleRate, + inputAudioFormat.channelCount, + speed, + pitch, + outputAudioFormat.sampleRate); + } else if (sonic != null) { + sonic.flush(); + } + } outputBuffer = EMPTY_BUFFER; inputBytes = 0; outputBytes = 0; @@ -198,12 +257,18 @@ public final class SonicAudioProcessor implements AudioProcessor { @Override public void reset() { - sonic = null; + speed = 1f; + pitch = 1f; + pendingInputAudioFormat = AudioFormat.NOT_SET; + pendingOutputAudioFormat = AudioFormat.NOT_SET; + inputAudioFormat = AudioFormat.NOT_SET; + outputAudioFormat = AudioFormat.NOT_SET; buffer = EMPTY_BUFFER; shortBuffer = buffer.asShortBuffer(); outputBuffer = EMPTY_BUFFER; - channelCount = Format.NO_VALUE; - sampleRateHz = Format.NO_VALUE; + pendingOutputSampleRate = SAMPLE_RATE_NO_CHANGE; + pendingSonicRecreation = false; + sonic = null; inputBytes = 0; outputBytes = 0; inputEnded = false; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/TeeAudioProcessor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/TeeAudioProcessor.java new file mode 100644 index 000000000000..42f151c5be25 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/TeeAudioProcessor.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Audio processor that outputs its input unmodified and also outputs its input to a given sink. + * This is intended to be used for diagnostics and debugging. + * + *

This audio processor can be inserted into the audio processor chain to access audio data + * before/after particular processing steps have been applied. For example, to get audio output + * after playback speed adjustment and silence skipping have been applied it is necessary to pass a + * custom {@link org.mozilla.thirdparty.com.google.android.exoplayer2audio.DefaultAudioSink.AudioProcessorChain} when + * creating the audio sink, and include this audio processor after all other audio processors. + */ +public final class TeeAudioProcessor extends BaseAudioProcessor { + + /** A sink for audio buffers handled by the audio processor. */ + public interface AudioBufferSink { + + /** Called when the audio processor is flushed with a format of subsequent input. */ + void flush(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding); + + /** + * Called when data is written to the audio processor. + * + * @param buffer A read-only buffer containing input which the audio processor will handle. + */ + void handleBuffer(ByteBuffer buffer); + } + + private final AudioBufferSink audioBufferSink; + + /** + * Creates a new tee audio processor, sending incoming data to the given {@link AudioBufferSink}. + * + * @param audioBufferSink The audio buffer sink that will receive input queued to this audio + * processor. + */ + public TeeAudioProcessor(AudioBufferSink audioBufferSink) { + this.audioBufferSink = Assertions.checkNotNull(audioBufferSink); + } + + @Override + public AudioFormat onConfigure(AudioFormat inputAudioFormat) { + // This processor is always active (if passed to the sink) and outputs its input. + return inputAudioFormat; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + int remaining = inputBuffer.remaining(); + if (remaining == 0) { + return; + } + audioBufferSink.handleBuffer(inputBuffer.asReadOnlyBuffer()); + replaceOutputBuffer(remaining).put(inputBuffer).flip(); + } + + @Override + protected void onQueueEndOfStream() { + flushSinkIfActive(); + } + + @Override + protected void onReset() { + flushSinkIfActive(); + } + + private void flushSinkIfActive() { + if (isActive()) { + audioBufferSink.flush( + inputAudioFormat.sampleRate, inputAudioFormat.channelCount, inputAudioFormat.encoding); + } + } + + /** + * A sink for audio buffers that writes output audio as .wav files with a given path prefix. When + * new audio data is handled after flushing the audio processor, a counter is incremented and its + * value is appended to the output file name. + * + *

Note: if writing to external storage it's necessary to grant the {@code + * WRITE_EXTERNAL_STORAGE} permission. + */ + public static final class WavFileAudioBufferSink implements AudioBufferSink { + + private static final String TAG = "WaveFileAudioBufferSink"; + + private static final int FILE_SIZE_MINUS_8_OFFSET = 4; + private static final int FILE_SIZE_MINUS_44_OFFSET = 40; + private static final int HEADER_LENGTH = 44; + + private final String outputFileNamePrefix; + private final byte[] scratchBuffer; + private final ByteBuffer scratchByteBuffer; + + private int sampleRateHz; + private int channelCount; + @C.PcmEncoding private int encoding; + @Nullable private RandomAccessFile randomAccessFile; + private int counter; + private int bytesWritten; + + /** + * Creates a new audio buffer sink that writes to .wav files with the given prefix. + * + * @param outputFileNamePrefix The prefix for output files. + */ + public WavFileAudioBufferSink(String outputFileNamePrefix) { + this.outputFileNamePrefix = outputFileNamePrefix; + scratchBuffer = new byte[1024]; + scratchByteBuffer = ByteBuffer.wrap(scratchBuffer).order(ByteOrder.LITTLE_ENDIAN); + } + + @Override + public void flush(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) { + try { + reset(); + } catch (IOException e) { + Log.e(TAG, "Error resetting", e); + } + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + this.encoding = encoding; + } + + @Override + public void handleBuffer(ByteBuffer buffer) { + try { + maybePrepareFile(); + writeBuffer(buffer); + } catch (IOException e) { + Log.e(TAG, "Error writing data", e); + } + } + + private void maybePrepareFile() throws IOException { + if (randomAccessFile != null) { + return; + } + RandomAccessFile randomAccessFile = new RandomAccessFile(getNextOutputFileName(), "rw"); + writeFileHeader(randomAccessFile); + this.randomAccessFile = randomAccessFile; + bytesWritten = HEADER_LENGTH; + } + + private void writeFileHeader(RandomAccessFile randomAccessFile) throws IOException { + // Write the start of the header as big endian data. + randomAccessFile.writeInt(WavUtil.RIFF_FOURCC); + randomAccessFile.writeInt(-1); + randomAccessFile.writeInt(WavUtil.WAVE_FOURCC); + randomAccessFile.writeInt(WavUtil.FMT_FOURCC); + + // Write the rest of the header as little endian data. + scratchByteBuffer.clear(); + scratchByteBuffer.putInt(16); + scratchByteBuffer.putShort((short) WavUtil.getTypeForPcmEncoding(encoding)); + scratchByteBuffer.putShort((short) channelCount); + scratchByteBuffer.putInt(sampleRateHz); + int bytesPerSample = Util.getPcmFrameSize(encoding, channelCount); + scratchByteBuffer.putInt(bytesPerSample * sampleRateHz); + scratchByteBuffer.putShort((short) bytesPerSample); + scratchByteBuffer.putShort((short) (8 * bytesPerSample / channelCount)); + randomAccessFile.write(scratchBuffer, 0, scratchByteBuffer.position()); + + // Write the start of the data chunk as big endian data. + randomAccessFile.writeInt(WavUtil.DATA_FOURCC); + randomAccessFile.writeInt(-1); + } + + private void writeBuffer(ByteBuffer buffer) throws IOException { + RandomAccessFile randomAccessFile = Assertions.checkNotNull(this.randomAccessFile); + while (buffer.hasRemaining()) { + int bytesToWrite = Math.min(buffer.remaining(), scratchBuffer.length); + buffer.get(scratchBuffer, 0, bytesToWrite); + randomAccessFile.write(scratchBuffer, 0, bytesToWrite); + bytesWritten += bytesToWrite; + } + } + + private void reset() throws IOException { + RandomAccessFile randomAccessFile = this.randomAccessFile; + if (randomAccessFile == null) { + return; + } + + try { + scratchByteBuffer.clear(); + scratchByteBuffer.putInt(bytesWritten - 8); + randomAccessFile.seek(FILE_SIZE_MINUS_8_OFFSET); + randomAccessFile.write(scratchBuffer, 0, 4); + + scratchByteBuffer.clear(); + scratchByteBuffer.putInt(bytesWritten - 44); + randomAccessFile.seek(FILE_SIZE_MINUS_44_OFFSET); + randomAccessFile.write(scratchBuffer, 0, 4); + } catch (IOException e) { + // The file may still be playable, so just log a warning. + Log.w(TAG, "Error updating file size", e); + } + + try { + randomAccessFile.close(); + } finally { + this.randomAccessFile = null; + } + } + + private String getNextOutputFileName() { + return Util.formatInvariant("%s-%04d.wav", outputFileNamePrefix, counter++); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java new file mode 100644 index 000000000000..1326cf63ee0d --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.nio.ByteBuffer; + +/** Audio processor for trimming samples from the start/end of data. */ +/* package */ final class TrimmingAudioProcessor extends BaseAudioProcessor { + + @C.PcmEncoding private static final int OUTPUT_ENCODING = C.ENCODING_PCM_16BIT; + + private int trimStartFrames; + private int trimEndFrames; + private boolean reconfigurationPending; + + private int pendingTrimStartBytes; + private byte[] endBuffer; + private int endBufferSize; + private long trimmedFrameCount; + + /** Creates a new audio processor for trimming samples from the start/end of data. */ + public TrimmingAudioProcessor() { + endBuffer = Util.EMPTY_BYTE_ARRAY; + } + + /** + * Sets the number of audio frames to trim from the start and end of audio passed to this + * processor. After calling this method, call {@link #configure(AudioFormat)} to apply the new + * trimming frame counts. + * + * @param trimStartFrames The number of audio frames to trim from the start of audio. + * @param trimEndFrames The number of audio frames to trim from the end of audio. + * @see AudioSink#configure(int, int, int, int, int[], int, int) + */ + public void setTrimFrameCount(int trimStartFrames, int trimEndFrames) { + this.trimStartFrames = trimStartFrames; + this.trimEndFrames = trimEndFrames; + } + + /** Sets the trimmed frame count returned by {@link #getTrimmedFrameCount()} to zero. */ + public void resetTrimmedFrameCount() { + trimmedFrameCount = 0; + } + + /** + * Returns the number of audio frames trimmed since the last call to {@link + * #resetTrimmedFrameCount()}. + */ + public long getTrimmedFrameCount() { + return trimmedFrameCount; + } + + @Override + public AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + if (inputAudioFormat.encoding != OUTPUT_ENCODING) { + throw new UnhandledAudioFormatException(inputAudioFormat); + } + reconfigurationPending = true; + return trimStartFrames != 0 || trimEndFrames != 0 ? inputAudioFormat : AudioFormat.NOT_SET; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + int position = inputBuffer.position(); + int limit = inputBuffer.limit(); + int remaining = limit - position; + + if (remaining == 0) { + return; + } + + // Trim any pending start bytes from the input buffer. + int trimBytes = Math.min(remaining, pendingTrimStartBytes); + trimmedFrameCount += trimBytes / inputAudioFormat.bytesPerFrame; + pendingTrimStartBytes -= trimBytes; + inputBuffer.position(position + trimBytes); + if (pendingTrimStartBytes > 0) { + // Nothing to output yet. + return; + } + remaining -= trimBytes; + + // endBuffer must be kept as full as possible, so that we trim the right amount of media if we + // don't receive any more input. After taking into account the number of bytes needed to keep + // endBuffer as full as possible, the output should be any surplus bytes currently in endBuffer + // followed by any surplus bytes in the new inputBuffer. + int remainingBytesToOutput = endBufferSize + remaining - endBuffer.length; + ByteBuffer buffer = replaceOutputBuffer(remainingBytesToOutput); + + // Output from endBuffer. + int endBufferBytesToOutput = Util.constrainValue(remainingBytesToOutput, 0, endBufferSize); + buffer.put(endBuffer, 0, endBufferBytesToOutput); + remainingBytesToOutput -= endBufferBytesToOutput; + + // Output from inputBuffer, restoring its limit afterwards. + int inputBufferBytesToOutput = Util.constrainValue(remainingBytesToOutput, 0, remaining); + inputBuffer.limit(inputBuffer.position() + inputBufferBytesToOutput); + buffer.put(inputBuffer); + inputBuffer.limit(limit); + remaining -= inputBufferBytesToOutput; + + // Compact endBuffer, then repopulate it using the new input. + endBufferSize -= endBufferBytesToOutput; + System.arraycopy(endBuffer, endBufferBytesToOutput, endBuffer, 0, endBufferSize); + inputBuffer.get(endBuffer, endBufferSize, remaining); + endBufferSize += remaining; + + buffer.flip(); + } + + @Override + public ByteBuffer getOutput() { + if (super.isEnded() && endBufferSize > 0) { + // Because audio processors may be drained in the middle of the stream we assume that the + // contents of the end buffer need to be output. For gapless transitions, configure will + // always be called, so the end buffer is cleared in onQueueEndOfStream. + replaceOutputBuffer(endBufferSize).put(endBuffer, 0, endBufferSize).flip(); + endBufferSize = 0; + } + return super.getOutput(); + } + + @Override + public boolean isEnded() { + return super.isEnded() && endBufferSize == 0; + } + + @Override + protected void onQueueEndOfStream() { + if (reconfigurationPending) { + // Trim audio in the end buffer. + if (endBufferSize > 0) { + trimmedFrameCount += endBufferSize / inputAudioFormat.bytesPerFrame; + } + endBufferSize = 0; + } + } + + @Override + protected void onFlush() { + if (reconfigurationPending) { + // This is the initial flush after reconfiguration. Prepare to trim bytes from the start/end. + reconfigurationPending = false; + endBuffer = new byte[trimEndFrames * inputAudioFormat.bytesPerFrame]; + pendingTrimStartBytes = trimStartFrames * inputAudioFormat.bytesPerFrame; + } else { + // This is a flush during playback (after the initial flush). We assume this was caused by a + // seek to a non-zero position and clear pending start bytes. This assumption may be wrong (we + // may be seeking to zero), but playing data that should have been trimmed shouldn't be + // noticeable after a seek. Ideally we would check the timestamp of the first input buffer + // queued after flushing to decide whether to trim (see also [Internal: b/77292509]). + pendingTrimStartBytes = 0; + } + endBufferSize = 0; + } + + @Override + protected void onReset() { + endBuffer = Util.EMPTY_BYTE_ARRAY; + } + +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/WavUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/WavUtil.java new file mode 100644 index 000000000000..d1245761aae1 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/WavUtil.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** Utilities for handling WAVE files. */ +public final class WavUtil { + + /** Four character code for "RIFF". */ + public static final int RIFF_FOURCC = 0x52494646; + /** Four character code for "WAVE". */ + public static final int WAVE_FOURCC = 0x57415645; + /** Four character code for "fmt ". */ + public static final int FMT_FOURCC = 0x666d7420; + /** Four character code for "data". */ + public static final int DATA_FOURCC = 0x64617461; + + /** WAVE type value for integer PCM audio data. */ + public static final int TYPE_PCM = 0x0001; + /** WAVE type value for float PCM audio data. */ + public static final int TYPE_FLOAT = 0x0003; + /** WAVE type value for 8-bit ITU-T G.711 A-law audio data. */ + public static final int TYPE_ALAW = 0x0006; + /** WAVE type value for 8-bit ITU-T G.711 mu-law audio data. */ + public static final int TYPE_MLAW = 0x0007; + /** WAVE type value for IMA ADPCM audio data. */ + public static final int TYPE_IMA_ADPCM = 0x0011; + /** WAVE type value for extended WAVE format. */ + public static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; + + /** + * Returns the WAVE format type value for the given {@link C.PcmEncoding}. + * + * @param pcmEncoding The {@link C.PcmEncoding} value. + * @return The corresponding WAVE format type. + * @throws IllegalArgumentException If {@code pcmEncoding} is not a {@link C.PcmEncoding}, or if + * it's {@link C#ENCODING_INVALID} or {@link Format#NO_VALUE}. + */ + public static int getTypeForPcmEncoding(@C.PcmEncoding int pcmEncoding) { + switch (pcmEncoding) { + case C.ENCODING_PCM_8BIT: + case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_24BIT: + case C.ENCODING_PCM_32BIT: + return TYPE_PCM; + case C.ENCODING_PCM_FLOAT: + return TYPE_FLOAT; + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: // Not TYPE_PCM, because TYPE_PCM is little endian. + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + throw new IllegalArgumentException(); + } + } + + /** + * Returns the {@link C.PcmEncoding} for the given WAVE format type value, or {@link + * C#ENCODING_INVALID} if the type is not a known PCM type. + */ + public static @C.PcmEncoding int getPcmEncodingForType(int type, int bitsPerSample) { + switch (type) { + case TYPE_PCM: + case TYPE_WAVE_FORMAT_EXTENSIBLE: + return Util.getPcmEncoding(bitsPerSample); + case TYPE_FLOAT: + return bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID; + default: + return C.ENCODING_INVALID; + } + } + + private WavUtil() { + // Prevent instantiation. + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/package-info.java new file mode 100644 index 000000000000..95c29d733363 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DatabaseIOException.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DatabaseIOException.java new file mode 100644 index 000000000000..4c03addf22cb --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DatabaseIOException.java @@ -0,0 +1,31 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.database; + +import android.database.SQLException; +import java.io.IOException; + +/** An {@link IOException} whose cause is an {@link SQLException}. */ +public final class DatabaseIOException extends IOException { + + public DatabaseIOException(SQLException cause) { + super(cause); + } + + public DatabaseIOException(SQLException cause, String message) { + super(message, cause); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DatabaseProvider.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DatabaseProvider.java new file mode 100644 index 000000000000..81deccaf9320 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DatabaseProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.database; + +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; + +/** + * Provides {@link SQLiteDatabase} instances to ExoPlayer components, which may read and write + * tables prefixed with {@link #TABLE_PREFIX}. + */ +public interface DatabaseProvider { + + /** Prefix for tables that can be read and written by ExoPlayer components. */ + String TABLE_PREFIX = "ExoPlayer"; + + /** + * Creates and/or opens a database that will be used for reading and writing. + * + *

Once opened successfully, the database is cached, so you can call this method every time you + * need to write to the database. Errors such as bad permissions or a full disk may cause this + * method to fail, but future attempts may succeed if the problem is fixed. + * + * @throws SQLiteException If the database cannot be opened for writing. + * @return A read/write database object. + */ + SQLiteDatabase getWritableDatabase(); + + /** + * Creates and/or opens a database. This will be the same object returned by {@link + * #getWritableDatabase()} unless some problem, such as a full disk, requires the database to be + * opened read-only. In that case, a read-only database object will be returned. If the problem is + * fixed, a future call to {@link #getWritableDatabase()} may succeed, in which case the read-only + * database object will be closed and the read/write object will be returned in the future. + * + *

Once opened successfully, the database is cached, so you can call this method every time you + * need to read from the database. + * + * @throws SQLiteException If the database cannot be opened. + * @return A database object valid until {@link #getWritableDatabase()} is called. + */ + SQLiteDatabase getReadableDatabase(); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DefaultDatabaseProvider.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DefaultDatabaseProvider.java new file mode 100644 index 000000000000..8da3de15c83c --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DefaultDatabaseProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.database; + +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +/** A {@link DatabaseProvider} that provides instances obtained from a {@link SQLiteOpenHelper}. */ +public final class DefaultDatabaseProvider implements DatabaseProvider { + + private final SQLiteOpenHelper sqliteOpenHelper; + + /** + * @param sqliteOpenHelper An {@link SQLiteOpenHelper} from which to obtain database instances. + */ + public DefaultDatabaseProvider(SQLiteOpenHelper sqliteOpenHelper) { + this.sqliteOpenHelper = sqliteOpenHelper; + } + + @Override + public SQLiteDatabase getWritableDatabase() { + return sqliteOpenHelper.getWritableDatabase(); + } + + @Override + public SQLiteDatabase getReadableDatabase() { + return sqliteOpenHelper.getReadableDatabase(); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/ExoDatabaseProvider.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/ExoDatabaseProvider.java new file mode 100644 index 000000000000..037442b10235 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/ExoDatabaseProvider.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.database; + +import android.content.Context; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; + +/** + * An {@link SQLiteOpenHelper} that provides instances of a standalone ExoPlayer database. + * + *

Suitable for use by applications that do not already have their own database, or that would + * prefer to keep ExoPlayer tables isolated in their own database. Other applications should prefer + * to use {@link DefaultDatabaseProvider} with their own {@link SQLiteOpenHelper}. + */ +public final class ExoDatabaseProvider extends SQLiteOpenHelper implements DatabaseProvider { + + /** The file name used for the standalone ExoPlayer database. */ + public static final String DATABASE_NAME = "exoplayer_internal.db"; + + private static final int VERSION = 1; + private static final String TAG = "ExoDatabaseProvider"; + + /** + * Provides instances of the database located by passing {@link #DATABASE_NAME} to {@link + * Context#getDatabasePath(String)}. + * + * @param context Any context. + */ + public ExoDatabaseProvider(Context context) { + super(context.getApplicationContext(), DATABASE_NAME, /* factory= */ null, VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + // Features create their own tables. + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // Features handle their own upgrades. + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + wipeDatabase(db); + } + + /** + * Makes a best effort to wipe the existing database. The wipe may be incomplete if the database + * contains foreign key constraints. + */ + private static void wipeDatabase(SQLiteDatabase db) { + String[] columns = {"type", "name"}; + try (Cursor cursor = + db.query( + "sqlite_master", + columns, + /* selection= */ null, + /* selectionArgs= */ null, + /* groupBy= */ null, + /* having= */ null, + /* orderBy= */ null)) { + while (cursor.moveToNext()) { + String type = cursor.getString(0); + String name = cursor.getString(1); + if (!"sqlite_sequence".equals(name)) { + // If it's not an SQL-controlled entity, drop it + String sql = "DROP " + type + " IF EXISTS " + name; + try { + db.execSQL(sql); + } catch (SQLException e) { + Log.e(TAG, "Error executing " + sql, e); + } + } + } + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/VersionTable.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/VersionTable.java new file mode 100644 index 000000000000..d3174e67b210 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/VersionTable.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.database; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import androidx.annotation.IntDef; +import androidx.annotation.VisibleForTesting; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Utility methods for accessing versions of ExoPlayer database components. This allows them to be + * versioned independently to the version of the containing database. + */ +public final class VersionTable { + + /** Returned by {@link #getVersion(SQLiteDatabase, int, String)} if the version is unset. */ + public static final int VERSION_UNSET = -1; + /** Version of tables used for offline functionality. */ + public static final int FEATURE_OFFLINE = 0; + /** Version of tables used for cache content metadata. */ + public static final int FEATURE_CACHE_CONTENT_METADATA = 1; + /** Version of tables used for cache file metadata. */ + public static final int FEATURE_CACHE_FILE_METADATA = 2; + + private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Versions"; + + private static final String COLUMN_FEATURE = "feature"; + private static final String COLUMN_INSTANCE_UID = "instance_uid"; + private static final String COLUMN_VERSION = "version"; + + private static final String WHERE_FEATURE_AND_INSTANCE_UID_EQUALS = + COLUMN_FEATURE + " = ? AND " + COLUMN_INSTANCE_UID + " = ?"; + + private static final String PRIMARY_KEY = + "PRIMARY KEY (" + COLUMN_FEATURE + ", " + COLUMN_INSTANCE_UID + ")"; + private static final String SQL_CREATE_TABLE_IF_NOT_EXISTS = + "CREATE TABLE IF NOT EXISTS " + + TABLE_NAME + + " (" + + COLUMN_FEATURE + + " INTEGER NOT NULL," + + COLUMN_INSTANCE_UID + + " TEXT NOT NULL," + + COLUMN_VERSION + + " INTEGER NOT NULL," + + PRIMARY_KEY + + ")"; + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({FEATURE_OFFLINE, FEATURE_CACHE_CONTENT_METADATA, FEATURE_CACHE_FILE_METADATA}) + private @interface Feature {} + + private VersionTable() {} + + /** + * Sets the version of a specified instance of a specified feature. + * + * @param writableDatabase The database to update. + * @param feature The feature. + * @param instanceUid The unique identifier of the instance of the feature. + * @param version The version. + * @throws DatabaseIOException If an error occurs executing the SQL. + */ + public static void setVersion( + SQLiteDatabase writableDatabase, @Feature int feature, String instanceUid, int version) + throws DatabaseIOException { + try { + writableDatabase.execSQL(SQL_CREATE_TABLE_IF_NOT_EXISTS); + ContentValues values = new ContentValues(); + values.put(COLUMN_FEATURE, feature); + values.put(COLUMN_INSTANCE_UID, instanceUid); + values.put(COLUMN_VERSION, version); + writableDatabase.replaceOrThrow(TABLE_NAME, /* nullColumnHack= */ null, values); + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + /** + * Removes the version of a specified instance of a feature. + * + * @param writableDatabase The database to update. + * @param feature The feature. + * @param instanceUid The unique identifier of the instance of the feature. + * @throws DatabaseIOException If an error occurs executing the SQL. + */ + public static void removeVersion( + SQLiteDatabase writableDatabase, @Feature int feature, String instanceUid) + throws DatabaseIOException { + try { + if (!tableExists(writableDatabase, TABLE_NAME)) { + return; + } + writableDatabase.delete( + TABLE_NAME, + WHERE_FEATURE_AND_INSTANCE_UID_EQUALS, + featureAndInstanceUidArguments(feature, instanceUid)); + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + /** + * Returns the version of a specified instance of a feature, or {@link #VERSION_UNSET} if no + * version is set. + * + * @param database The database to query. + * @param feature The feature. + * @param instanceUid The unique identifier of the instance of the feature. + * @return The version, or {@link #VERSION_UNSET} if no version is set. + * @throws DatabaseIOException If an error occurs executing the SQL. + */ + public static int getVersion(SQLiteDatabase database, @Feature int feature, String instanceUid) + throws DatabaseIOException { + try { + if (!tableExists(database, TABLE_NAME)) { + return VERSION_UNSET; + } + try (Cursor cursor = + database.query( + TABLE_NAME, + new String[] {COLUMN_VERSION}, + WHERE_FEATURE_AND_INSTANCE_UID_EQUALS, + featureAndInstanceUidArguments(feature, instanceUid), + /* groupBy= */ null, + /* having= */ null, + /* orderBy= */ null)) { + if (cursor.getCount() == 0) { + return VERSION_UNSET; + } + cursor.moveToNext(); + return cursor.getInt(/* COLUMN_VERSION index */ 0); + } + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + @VisibleForTesting + /* package */ static boolean tableExists(SQLiteDatabase readableDatabase, String tableName) { + long count = + DatabaseUtils.queryNumEntries( + readableDatabase, "sqlite_master", "tbl_name = ?", new String[] {tableName}); + return count > 0; + } + + private static String[] featureAndInstanceUidArguments(int feature, String instance) { + return new String[] {Integer.toString(feature), instance}; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/package-info.java new file mode 100644 index 000000000000..85e0dfa5e300 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.database; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/Buffer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/Buffer.java index 766e60520b55..ac254fae9638 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/Buffer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/Buffer.java @@ -53,6 +53,11 @@ public abstract class Buffer { return getFlag(C.BUFFER_FLAG_KEY_FRAME); } + /** Returns whether the {@link C#BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA} flag is set. */ + public final boolean hasSupplementalData() { + return getFlag(C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA); + } + /** * Replaces this buffer's flags with {@code flags}. * diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/CryptoInfo.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/CryptoInfo.java index c51659b20d9d..1bfb0fb06e14 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/CryptoInfo.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/CryptoInfo.java @@ -25,44 +25,58 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; public final class CryptoInfo { /** + * The 16 byte initialization vector. If the initialization vector of the content is shorter than + * 16 bytes, 0 byte padding is appended to extend the vector to the required 16 byte length. + * * @see android.media.MediaCodec.CryptoInfo#iv */ public byte[] iv; /** + * The 16 byte key id. + * * @see android.media.MediaCodec.CryptoInfo#key */ public byte[] key; /** + * The type of encryption that has been applied. Must be one of the {@link C.CryptoMode} values. + * * @see android.media.MediaCodec.CryptoInfo#mode */ - @C.CryptoMode - public int mode; + @C.CryptoMode public int mode; /** + * The number of leading unencrypted bytes in each sub-sample. If null, all bytes are treated as + * encrypted and {@link #numBytesOfEncryptedData} must be specified. + * * @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData */ public int[] numBytesOfClearData; /** + * The number of trailing encrypted bytes in each sub-sample. If null, all bytes are treated as + * clear and {@link #numBytesOfClearData} must be specified. + * * @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData */ public int[] numBytesOfEncryptedData; /** + * The number of subSamples that make up the buffer's contents. + * * @see android.media.MediaCodec.CryptoInfo#numSubSamples */ public int numSubSamples; /** * @see android.media.MediaCodec.CryptoInfo.Pattern */ - public int patternBlocksToEncrypt; + public int encryptedBlocks; /** * @see android.media.MediaCodec.CryptoInfo.Pattern */ - public int patternBlocksToSkip; + public int clearBlocks; private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo; private final PatternHolderV24 patternHolder; public CryptoInfo() { - frameworkCryptoInfo = Util.SDK_INT >= 16 ? newFrameworkCryptoInfoV16() : null; + frameworkCryptoInfo = new android.media.MediaCodec.CryptoInfo(); patternHolder = Util.SDK_INT >= 24 ? new PatternHolderV24(frameworkCryptoInfo) : null; } @@ -70,51 +84,17 @@ public final class CryptoInfo { * @see android.media.MediaCodec.CryptoInfo#set(int, int[], int[], byte[], byte[], int) */ public void set(int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData, - byte[] key, byte[] iv, @C.CryptoMode int mode) { + byte[] key, byte[] iv, @C.CryptoMode int mode, int encryptedBlocks, int clearBlocks) { this.numSubSamples = numSubSamples; this.numBytesOfClearData = numBytesOfClearData; this.numBytesOfEncryptedData = numBytesOfEncryptedData; this.key = key; this.iv = iv; this.mode = mode; - patternBlocksToEncrypt = 0; - patternBlocksToSkip = 0; - if (Util.SDK_INT >= 16) { - updateFrameworkCryptoInfoV16(); - } - } - - public void setPattern(int patternBlocksToEncrypt, int patternBlocksToSkip) { - this.patternBlocksToEncrypt = patternBlocksToEncrypt; - this.patternBlocksToSkip = patternBlocksToSkip; - if (Util.SDK_INT >= 24) { - patternHolder.set(patternBlocksToEncrypt, patternBlocksToSkip); - } - } - - /** - * Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance. - *

- * Successive calls to this method on a single {@link CryptoInfo} will return the same instance. - * Changes to the {@link CryptoInfo} will be reflected in the returned object. The return object - * should not be modified directly. - * - * @return The equivalent {@link android.media.MediaCodec.CryptoInfo} instance. - */ - @TargetApi(16) - public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() { - return frameworkCryptoInfo; - } - - @TargetApi(16) - private android.media.MediaCodec.CryptoInfo newFrameworkCryptoInfoV16() { - return new android.media.MediaCodec.CryptoInfo(); - } - - @TargetApi(16) - private void updateFrameworkCryptoInfoV16() { - // Update fields directly because the framework's CryptoInfo.set performs an unnecessary object - // allocation on Android N. + this.encryptedBlocks = encryptedBlocks; + this.clearBlocks = clearBlocks; + // Update frameworkCryptoInfo fields directly because CryptoInfo.set performs an unnecessary + // object allocation on Android N. frameworkCryptoInfo.numSubSamples = numSubSamples; frameworkCryptoInfo.numBytesOfClearData = numBytesOfClearData; frameworkCryptoInfo.numBytesOfEncryptedData = numBytesOfEncryptedData; @@ -122,26 +102,43 @@ public final class CryptoInfo { frameworkCryptoInfo.iv = iv; frameworkCryptoInfo.mode = mode; if (Util.SDK_INT >= 24) { - patternHolder.set(patternBlocksToEncrypt, patternBlocksToSkip); + patternHolder.set(encryptedBlocks, clearBlocks); } } + /** + * Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance. + * + *

Successive calls to this method on a single {@link CryptoInfo} will return the same + * instance. Changes to the {@link CryptoInfo} will be reflected in the returned object. The + * return object should not be modified directly. + * + * @return The equivalent {@link android.media.MediaCodec.CryptoInfo} instance. + */ + public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfo() { + return frameworkCryptoInfo; + } + + /** @deprecated Use {@link #getFrameworkCryptoInfo()}. */ + @Deprecated + public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() { + return getFrameworkCryptoInfo(); + } + @TargetApi(24) private static final class PatternHolderV24 { private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo; - - // Reference to the two tickets (Bug 1259098, Bug 1365543) - // private final android.media.MediaCodec.CryptoInfo.Pattern pattern; + private final android.media.MediaCodec.CryptoInfo.Pattern pattern; private PatternHolderV24(android.media.MediaCodec.CryptoInfo frameworkCryptoInfo) { this.frameworkCryptoInfo = frameworkCryptoInfo; - // pattern = new android.media.MediaCodec.CryptoInfo.Pattern(0, 0); + pattern = new android.media.MediaCodec.CryptoInfo.Pattern(0, 0); } - private void set(int blocksToEncrypt, int blocksToSkip) { - // pattern.set(blocksToEncrypt, blocksToSkip); - // frameworkCryptoInfo.setPattern(pattern); + private void set(int encryptedBlocks, int clearBlocks) { + pattern.set(encryptedBlocks, clearBlocks); + frameworkCryptoInfo.setPattern(pattern); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/Decoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/Decoder.java index 94b4b331343d..8040c04ebeb8 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/Decoder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/Decoder.java @@ -15,6 +15,8 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.decoder; +import androidx.annotation.Nullable; + /** * A media decoder. * @@ -37,6 +39,7 @@ public interface Decoder { * @return The input buffer, which will have been cleared, or null if a buffer isn't available. * @throws E If a decoder error has occurred. */ + @Nullable I dequeueInputBuffer() throws E; /** @@ -53,6 +56,7 @@ public interface Decoder { * @return The output buffer, or null if an output buffer isn't available. * @throws E If a decoder error has occurred. */ + @Nullable O dequeueOutputBuffer() throws E; /** diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/DecoderCounters.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/DecoderCounters.java index 7531b4fb41f6..f8bdb9b29ac9 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/DecoderCounters.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/DecoderCounters.java @@ -36,6 +36,12 @@ public final class DecoderCounters { * The number of queued input buffers. */ public int inputBufferCount; + /** + * The number of skipped input buffers. + *

+ * A skipped input buffer is an input buffer that was deliberately not sent to the decoder. + */ + public int skippedInputBufferCount; /** * The number of rendered output buffers. */ @@ -47,18 +53,26 @@ public final class DecoderCounters { */ public int skippedOutputBufferCount; /** - * The number of dropped output buffers. + * The number of dropped buffers. *

- * A dropped output buffer is an output buffer that was supposed to be rendered, but was instead + * A dropped buffer is an buffer that was supposed to be decoded/rendered, but was instead * dropped because it could not be rendered in time. */ - public int droppedOutputBufferCount; + public int droppedBufferCount; /** - * The maximum number of dropped output buffers without an interleaving rendered output buffer. + * The maximum number of dropped buffers without an interleaving rendered output buffer. *

* Skipped output buffers are ignored for the purposes of calculating this value. */ - public int maxConsecutiveDroppedOutputBufferCount; + public int maxConsecutiveDroppedBufferCount; + /** + * The number of times all buffers to a keyframe were dropped. + *

+ * Each time buffers to a keyframe are dropped, this counter is increased by one, and the dropped + * buffer counters are increased by one (for the current output buffer) plus the number of buffers + * dropped from the source to advance to the keyframe. + */ + public int droppedToKeyframeCount; /** * Should be called to ensure counter values are made visible across threads. The playback thread @@ -79,11 +93,13 @@ public final class DecoderCounters { decoderInitCount += other.decoderInitCount; decoderReleaseCount += other.decoderReleaseCount; inputBufferCount += other.inputBufferCount; + skippedInputBufferCount += other.skippedInputBufferCount; renderedOutputBufferCount += other.renderedOutputBufferCount; skippedOutputBufferCount += other.skippedOutputBufferCount; - droppedOutputBufferCount += other.droppedOutputBufferCount; - maxConsecutiveDroppedOutputBufferCount = Math.max(maxConsecutiveDroppedOutputBufferCount, - other.maxConsecutiveDroppedOutputBufferCount); + droppedBufferCount += other.droppedBufferCount; + maxConsecutiveDroppedBufferCount = Math.max(maxConsecutiveDroppedBufferCount, + other.maxConsecutiveDroppedBufferCount); + droppedToKeyframeCount += other.droppedToKeyframeCount; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java index 93a77e02593d..254ecfdec89f 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java @@ -15,11 +15,14 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.decoder; -import android.support.annotation.IntDef; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; /** * Holds input for a decoder. @@ -27,11 +30,17 @@ import java.nio.ByteBuffer; public class DecoderInputBuffer extends Buffer { /** - * The buffer replacement mode, which may disable replacement. + * The buffer replacement mode, which may disable replacement. One of {@link + * #BUFFER_REPLACEMENT_MODE_DISABLED}, {@link #BUFFER_REPLACEMENT_MODE_NORMAL} or {@link + * #BUFFER_REPLACEMENT_MODE_DIRECT}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({BUFFER_REPLACEMENT_MODE_DISABLED, BUFFER_REPLACEMENT_MODE_NORMAL, - BUFFER_REPLACEMENT_MODE_DIRECT}) + @IntDef({ + BUFFER_REPLACEMENT_MODE_DISABLED, + BUFFER_REPLACEMENT_MODE_NORMAL, + BUFFER_REPLACEMENT_MODE_DIRECT + }) public @interface BufferReplacementMode {} /** * Disallows buffer replacement. @@ -51,16 +60,28 @@ public class DecoderInputBuffer extends Buffer { */ public final CryptoInfo cryptoInfo; + /** The buffer's data, or {@code null} if no data has been set. */ + @Nullable public ByteBuffer data; + + // TODO: Remove this temporary signaling once end-of-stream propagation for clips using content + // protection is fixed. See [Internal: b/153326944] for details. /** - * The buffer's data, or {@code null} if no data has been set. + * Whether the last attempt to read a sample into this buffer failed due to not yet having the DRM + * keys associated with the next sample. */ - public ByteBuffer data; + public boolean waitingForKeys; /** * The time at which the sample should be presented. */ public long timeUs; + /** + * Supplemental data related to the buffer, if {@link #hasSupplementalData()} returns true. If + * present, the buffer is populated with supplemental data from position 0 to its limit. + */ + @Nullable public ByteBuffer supplementalData; + @BufferReplacementMode private final int bufferReplacementMode; /** @@ -82,11 +103,26 @@ public class DecoderInputBuffer extends Buffer { this.bufferReplacementMode = bufferReplacementMode; } + /** + * Clears {@link #supplementalData} and ensures that it's large enough to accommodate {@code + * length} bytes. + * + * @param length The length of the supplemental data that must be accommodated, in bytes. + */ + @EnsuresNonNull("supplementalData") + public void resetSupplementalData(int length) { + if (supplementalData == null || supplementalData.capacity() < length) { + supplementalData = ByteBuffer.allocate(length); + } else { + supplementalData.clear(); + } + } + /** * Ensures that {@link #data} is large enough to accommodate a write of a given length at its * current position. - *

- * If the capacity of {@link #data} is sufficient this method does nothing. If the capacity is + * + *

If the capacity of {@link #data} is sufficient this method does nothing. If the capacity is * insufficient then an attempt is made to replace {@link #data} with a new {@link ByteBuffer} * whose capacity is sufficient. Data up to the current position is copied to the new buffer. * @@ -94,7 +130,8 @@ public class DecoderInputBuffer extends Buffer { * @throws IllegalStateException If there is insufficient capacity to accommodate the write and * the buffer replacement mode of the holder is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}. */ - public void ensureSpaceForWrite(int length) throws IllegalStateException { + @EnsuresNonNull("data") + public void ensureSpaceForWrite(int length) { if (data == null) { data = createReplacementByteBuffer(length); return; @@ -108,10 +145,10 @@ public class DecoderInputBuffer extends Buffer { } // Instantiate a new buffer if possible. ByteBuffer newData = createReplacementByteBuffer(requiredCapacity); + newData.order(data.order()); // Copy data up to the current position from the old buffer to the new one. if (position > 0) { - data.position(0); - data.limit(position); + data.flip(); newData.put(data); } // Set the new buffer. @@ -134,12 +171,15 @@ public class DecoderInputBuffer extends Buffer { } /** - * Flips {@link #data} in preparation for being queued to a decoder. + * Flips {@link #data} and {@link #supplementalData} in preparation for being queued to a decoder. * * @see java.nio.Buffer#flip() */ public final void flip() { data.flip(); + if (supplementalData != null) { + supplementalData.flip(); + } } @Override @@ -148,6 +188,10 @@ public class DecoderInputBuffer extends Buffer { if (data != null) { data.clear(); } + if (supplementalData != null) { + supplementalData.clear(); + } + waitingForKeys = false; } private ByteBuffer createReplacementByteBuffer(int requiredCapacity) { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/SimpleDecoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/SimpleDecoder.java index d2f4ae70d657..a193ad3c8ef6 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/SimpleDecoder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/SimpleDecoder.java @@ -15,21 +15,23 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.decoder; +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; -import java.util.LinkedList; +import java.util.ArrayDeque; -/** - * Base class for {@link Decoder}s that use their own decode thread. - */ -public abstract class SimpleDecoder implements Decoder { +/** Base class for {@link Decoder}s that use their own decode thread. */ +@SuppressWarnings("UngroupedOverloads") +public abstract class SimpleDecoder< + I extends DecoderInputBuffer, O extends OutputBuffer, E extends Exception> + implements Decoder { private final Thread decodeThread; private final Object lock; - private final LinkedList queuedInputBuffers; - private final LinkedList queuedOutputBuffers; + private final ArrayDeque queuedInputBuffers; + private final ArrayDeque queuedOutputBuffers; private final I[] availableInputBuffers; private final O[] availableOutputBuffers; @@ -48,8 +50,8 @@ public abstract class SimpleDecoder(); - queuedOutputBuffers = new LinkedList<>(); + queuedInputBuffers = new ArrayDeque<>(); + queuedOutputBuffers = new ArrayDeque<>(); availableInputBuffers = inputBuffers; availableInputBufferCount = inputBuffers.length; for (int i = 0; i < availableInputBufferCount; i++) { @@ -85,6 +87,7 @@ public abstract class SimpleDecoder owner; - public ByteBuffer data; + @Nullable public ByteBuffer data; public SimpleOutputBuffer(SimpleDecoder owner) { this.owner = owner; @@ -40,7 +42,7 @@ public class SimpleOutputBuffer extends OutputBuffer { public ByteBuffer init(long timeUs, int size) { this.timeUs = timeUs; if (data == null || data.capacity() < size) { - data = ByteBuffer.allocateDirect(size); + data = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); } data.position(0); data.limit(size); diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/package-info.java new file mode 100644 index 000000000000..78a2c9f2e2ae --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.decoder; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ClearKeyUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ClearKeyUtil.java new file mode 100644 index 000000000000..770b8511d90d --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ClearKeyUtil.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.drm; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Utility methods for ClearKey. + */ +/* package */ final class ClearKeyUtil { + + private static final String TAG = "ClearKeyUtil"; + + private ClearKeyUtil() {} + + /** + * Adjusts ClearKey request data obtained from the Android ClearKey CDM to be spec compliant. + * + * @param request The request data. + * @return The adjusted request data. + */ + public static byte[] adjustRequestData(byte[] request) { + if (Util.SDK_INT >= 27) { + return request; + } + // Prior to O-MR1 the ClearKey CDM encoded the values in the "kids" array using Base64 encoding + // rather than Base64Url encoding. See [Internal: b/64388098]. We know the exact request format + // from the platform's InitDataParser.cpp. Since there aren't any "+" or "/" symbols elsewhere + // in the request, it's safe to fix the encoding by replacement through the whole request. + String requestString = Util.fromUtf8Bytes(request); + return Util.getUtf8Bytes(base64ToBase64Url(requestString)); + } + + /** + * Adjusts ClearKey response data to be suitable for providing to the Android ClearKey CDM. + * + * @param response The response data. + * @return The adjusted response data. + */ + public static byte[] adjustResponseData(byte[] response) { + if (Util.SDK_INT >= 27) { + return response; + } + // Prior to O-MR1 the ClearKey CDM expected Base64 encoding rather than Base64Url encoding for + // the "k" and "kid" strings. See [Internal: b/64388098]. We know that the ClearKey CDM only + // looks at the k, kid and kty parameters in each key, so can ignore the rest of the response. + try { + JSONObject responseJson = new JSONObject(Util.fromUtf8Bytes(response)); + StringBuilder adjustedResponseBuilder = new StringBuilder("{\"keys\":["); + JSONArray keysArray = responseJson.getJSONArray("keys"); + for (int i = 0; i < keysArray.length(); i++) { + if (i != 0) { + adjustedResponseBuilder.append(","); + } + JSONObject key = keysArray.getJSONObject(i); + adjustedResponseBuilder.append("{\"k\":\""); + adjustedResponseBuilder.append(base64UrlToBase64(key.getString("k"))); + adjustedResponseBuilder.append("\",\"kid\":\""); + adjustedResponseBuilder.append(base64UrlToBase64(key.getString("kid"))); + adjustedResponseBuilder.append("\",\"kty\":\""); + adjustedResponseBuilder.append(key.getString("kty")); + adjustedResponseBuilder.append("\"}"); + } + adjustedResponseBuilder.append("]}"); + return Util.getUtf8Bytes(adjustedResponseBuilder.toString()); + } catch (JSONException e) { + Log.e(TAG, "Failed to adjust response data: " + Util.fromUtf8Bytes(response), e); + return response; + } + } + + private static String base64ToBase64Url(String base64) { + return base64.replace('+', '-').replace('/', '_'); + } + + private static String base64UrlToBase64(String base64Url) { + return base64Url.replace('-', '+').replace('_', '/'); + } + +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DecryptionException.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DecryptionException.java index c55dd36e8ca0..989e68befd33 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DecryptionException.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DecryptionException.java @@ -1,20 +1,37 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.drm; /** - * An exception when doing drm decryption using the In-App Drm + * Thrown when a non-platform component fails to decrypt data. */ public class DecryptionException extends Exception { - private final int errorCode; + /** + * A component specific error code. + */ + public final int errorCode; + + /** + * @param errorCode A component specific error code. + * @param message The detail message. + */ public DecryptionException(int errorCode, String message) { super(message); this.errorCode = errorCode; } - /** - * Get error code - */ - public int getErrorCode() { - return errorCode; - } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSession.java new file mode 100644 index 000000000000..ad7ed80580e7 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -0,0 +1,607 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.drm; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.media.NotProvisionedException; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; +import android.util.Pair; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.EventDispatcher; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +/** A {@link DrmSession} that supports playbacks using {@link ExoMediaDrm}. */ +@TargetApi(18) +/* package */ class DefaultDrmSession implements DrmSession { + + /** Thrown when an unexpected exception or error is thrown during provisioning or key requests. */ + public static final class UnexpectedDrmSessionException extends IOException { + + public UnexpectedDrmSessionException(Throwable cause) { + super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause); + } + } + + /** Manages provisioning requests. */ + public interface ProvisioningManager { + + /** + * Called when a session requires provisioning. The manager may call {@link + * #provision()} to have this session perform the provisioning operation. The manager + * will call {@link DefaultDrmSession#onProvisionCompleted()} when provisioning has + * completed, or {@link DefaultDrmSession#onProvisionError} if provisioning fails. + * + * @param session The session. + */ + void provisionRequired(DefaultDrmSession session); + + /** + * Called by a session when it fails to perform a provisioning operation. + * + * @param error The error that occurred. + */ + void onProvisionError(Exception error); + + /** Called by a session when it successfully completes a provisioning operation. */ + void onProvisionCompleted(); + } + + /** Callback to be notified when the session is released. */ + public interface ReleaseCallback { + + /** + * Called immediately after releasing session resources. + * + * @param session The session. + */ + void onSessionReleased(DefaultDrmSession session); + } + + private static final String TAG = "DefaultDrmSession"; + + private static final int MSG_PROVISION = 0; + private static final int MSG_KEYS = 1; + private static final int MAX_LICENSE_DURATION_TO_RENEW_SECONDS = 60; + + /** The DRM scheme datas, or null if this session uses offline keys. */ + @Nullable public final List schemeDatas; + + private final ExoMediaDrm mediaDrm; + private final ProvisioningManager provisioningManager; + private final ReleaseCallback releaseCallback; + private final @DefaultDrmSessionManager.Mode int mode; + private final boolean playClearSamplesWithoutKeys; + private final boolean isPlaceholderSession; + private final HashMap keyRequestParameters; + private final EventDispatcher eventDispatcher; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; + + /* package */ final MediaDrmCallback callback; + /* package */ final UUID uuid; + /* package */ final ResponseHandler responseHandler; + + private @DrmSession.State int state; + private int referenceCount; + @Nullable private HandlerThread requestHandlerThread; + @Nullable private RequestHandler requestHandler; + @Nullable private T mediaCrypto; + @Nullable private DrmSessionException lastException; + @Nullable private byte[] sessionId; + @MonotonicNonNull private byte[] offlineLicenseKeySetId; + + @Nullable private KeyRequest currentKeyRequest; + @Nullable private ProvisionRequest currentProvisionRequest; + + /** + * Instantiates a new DRM session. + * + * @param uuid The UUID of the drm scheme. + * @param mediaDrm The media DRM. + * @param provisioningManager The manager for provisioning. + * @param releaseCallback The {@link ReleaseCallback}. + * @param schemeDatas DRM scheme datas for this session, or null if an {@code + * offlineLicenseKeySetId} is provided or if {@code isPlaceholderSession} is true. + * @param mode The DRM mode. Ignored if {@code isPlaceholderSession} is true. + * @param isPlaceholderSession Whether this session is not expected to acquire any keys. + * @param offlineLicenseKeySetId The offline license key set identifier, or null when not using + * offline keys. + * @param keyRequestParameters Key request parameters. + * @param callback The media DRM callback. + * @param playbackLooper The playback looper. + * @param eventDispatcher The dispatcher for DRM session manager events. + * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy} for key and provisioning + * requests. + */ + // the constructor does not initialize fields: sessionId + @SuppressWarnings("nullness:initialization.fields.uninitialized") + public DefaultDrmSession( + UUID uuid, + ExoMediaDrm mediaDrm, + ProvisioningManager provisioningManager, + ReleaseCallback releaseCallback, + @Nullable List schemeDatas, + @DefaultDrmSessionManager.Mode int mode, + boolean playClearSamplesWithoutKeys, + boolean isPlaceholderSession, + @Nullable byte[] offlineLicenseKeySetId, + HashMap keyRequestParameters, + MediaDrmCallback callback, + Looper playbackLooper, + EventDispatcher eventDispatcher, + LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + if (mode == DefaultDrmSessionManager.MODE_QUERY + || mode == DefaultDrmSessionManager.MODE_RELEASE) { + Assertions.checkNotNull(offlineLicenseKeySetId); + } + this.uuid = uuid; + this.provisioningManager = provisioningManager; + this.releaseCallback = releaseCallback; + this.mediaDrm = mediaDrm; + this.mode = mode; + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + this.isPlaceholderSession = isPlaceholderSession; + if (offlineLicenseKeySetId != null) { + this.offlineLicenseKeySetId = offlineLicenseKeySetId; + this.schemeDatas = null; + } else { + this.schemeDatas = Collections.unmodifiableList(Assertions.checkNotNull(schemeDatas)); + } + this.keyRequestParameters = keyRequestParameters; + this.callback = callback; + this.eventDispatcher = eventDispatcher; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + state = STATE_OPENING; + responseHandler = new ResponseHandler(playbackLooper); + } + + public boolean hasSessionId(byte[] sessionId) { + return Arrays.equals(this.sessionId, sessionId); + } + + public void onMediaDrmEvent(int what) { + switch (what) { + case ExoMediaDrm.EVENT_KEY_REQUIRED: + onKeysRequired(); + break; + default: + break; + } + } + + // Provisioning implementation. + + public void provision() { + currentProvisionRequest = mediaDrm.getProvisionRequest(); + Util.castNonNull(requestHandler) + .post( + MSG_PROVISION, + Assertions.checkNotNull(currentProvisionRequest), + /* allowRetry= */ true); + } + + public void onProvisionCompleted() { + if (openInternal(false)) { + doLicense(true); + } + } + + public void onProvisionError(Exception error) { + onError(error); + } + + // DrmSession implementation. + + @Override + @DrmSession.State + public final int getState() { + return state; + } + + @Override + public boolean playClearSamplesWithoutKeys() { + return playClearSamplesWithoutKeys; + } + + @Override + public final @Nullable DrmSessionException getError() { + return state == STATE_ERROR ? lastException : null; + } + + @Override + public final @Nullable T getMediaCrypto() { + return mediaCrypto; + } + + @Override + @Nullable + public Map queryKeyStatus() { + return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId); + } + + @Override + @Nullable + public byte[] getOfflineLicenseKeySetId() { + return offlineLicenseKeySetId; + } + + @Override + public void acquire() { + Assertions.checkState(referenceCount >= 0); + if (++referenceCount == 1) { + Assertions.checkState(state == STATE_OPENING); + requestHandlerThread = new HandlerThread("DrmRequestHandler"); + requestHandlerThread.start(); + requestHandler = new RequestHandler(requestHandlerThread.getLooper()); + if (openInternal(true)) { + doLicense(true); + } + } + } + + @Override + public void release() { + if (--referenceCount == 0) { + // Assigning null to various non-null variables for clean-up. + state = STATE_RELEASED; + Util.castNonNull(responseHandler).removeCallbacksAndMessages(null); + Util.castNonNull(requestHandler).removeCallbacksAndMessages(null); + requestHandler = null; + Util.castNonNull(requestHandlerThread).quit(); + requestHandlerThread = null; + mediaCrypto = null; + lastException = null; + currentKeyRequest = null; + currentProvisionRequest = null; + if (sessionId != null) { + mediaDrm.closeSession(sessionId); + sessionId = null; + eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionReleased); + } + releaseCallback.onSessionReleased(this); + } + } + + // Internal methods. + + /** + * Try to open a session, do provisioning if necessary. + * + * @param allowProvisioning if provisioning is allowed, set this to false when calling from + * processing provision response. + * @return true on success, false otherwise. + */ + @EnsuresNonNullIf(result = true, expression = "sessionId") + private boolean openInternal(boolean allowProvisioning) { + if (isOpen()) { + // Already opened + return true; + } + + try { + sessionId = mediaDrm.openSession(); + mediaCrypto = mediaDrm.createMediaCrypto(sessionId); + eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionAcquired); + state = STATE_OPENED; + Assertions.checkNotNull(sessionId); + return true; + } catch (NotProvisionedException e) { + if (allowProvisioning) { + provisioningManager.provisionRequired(this); + } else { + onError(e); + } + } catch (Exception e) { + onError(e); + } + + return false; + } + + private void onProvisionResponse(Object request, Object response) { + if (request != currentProvisionRequest || (state != STATE_OPENING && !isOpen())) { + // This event is stale. + return; + } + currentProvisionRequest = null; + + if (response instanceof Exception) { + provisioningManager.onProvisionError((Exception) response); + return; + } + + try { + mediaDrm.provideProvisionResponse((byte[]) response); + } catch (Exception e) { + provisioningManager.onProvisionError(e); + return; + } + + provisioningManager.onProvisionCompleted(); + } + + @RequiresNonNull("sessionId") + private void doLicense(boolean allowRetry) { + if (isPlaceholderSession) { + return; + } + byte[] sessionId = Util.castNonNull(this.sessionId); + switch (mode) { + case DefaultDrmSessionManager.MODE_PLAYBACK: + case DefaultDrmSessionManager.MODE_QUERY: + if (offlineLicenseKeySetId == null) { + postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_STREAMING, allowRetry); + } else if (state == STATE_OPENED_WITH_KEYS || restoreKeys()) { + long licenseDurationRemainingSec = getLicenseDurationRemainingSec(); + if (mode == DefaultDrmSessionManager.MODE_PLAYBACK + && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW_SECONDS) { + Log.d( + TAG, + "Offline license has expired or will expire soon. " + + "Remaining seconds: " + + licenseDurationRemainingSec); + postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry); + } else if (licenseDurationRemainingSec <= 0) { + onError(new KeysExpiredException()); + } else { + state = STATE_OPENED_WITH_KEYS; + eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysRestored); + } + } + break; + case DefaultDrmSessionManager.MODE_DOWNLOAD: + if (offlineLicenseKeySetId == null || restoreKeys()) { + postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry); + } + break; + case DefaultDrmSessionManager.MODE_RELEASE: + Assertions.checkNotNull(offlineLicenseKeySetId); + Assertions.checkNotNull(this.sessionId); + // It's not necessary to restore the key (and open a session to do that) before releasing it + // but this serves as a good sanity/fast-failure check. + if (restoreKeys()) { + postKeyRequest(offlineLicenseKeySetId, ExoMediaDrm.KEY_TYPE_RELEASE, allowRetry); + } + break; + default: + break; + } + } + + @RequiresNonNull({"sessionId", "offlineLicenseKeySetId"}) + private boolean restoreKeys() { + try { + mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId); + return true; + } catch (Exception e) { + Log.e(TAG, "Error trying to restore keys.", e); + onError(e); + } + return false; + } + + private long getLicenseDurationRemainingSec() { + if (!C.WIDEVINE_UUID.equals(uuid)) { + return Long.MAX_VALUE; + } + Pair pair = + Assertions.checkNotNull(WidevineUtil.getLicenseDurationRemainingSec(this)); + return Math.min(pair.first, pair.second); + } + + private void postKeyRequest(byte[] scope, int type, boolean allowRetry) { + try { + currentKeyRequest = mediaDrm.getKeyRequest(scope, schemeDatas, type, keyRequestParameters); + Util.castNonNull(requestHandler) + .post(MSG_KEYS, Assertions.checkNotNull(currentKeyRequest), allowRetry); + } catch (Exception e) { + onKeysError(e); + } + } + + private void onKeyResponse(Object request, Object response) { + if (request != currentKeyRequest || !isOpen()) { + // This event is stale. + return; + } + currentKeyRequest = null; + + if (response instanceof Exception) { + onKeysError((Exception) response); + return; + } + + try { + byte[] responseData = (byte[]) response; + if (mode == DefaultDrmSessionManager.MODE_RELEASE) { + mediaDrm.provideKeyResponse(Util.castNonNull(offlineLicenseKeySetId), responseData); + eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysRestored); + } else { + byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData); + if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD + || (mode == DefaultDrmSessionManager.MODE_PLAYBACK + && offlineLicenseKeySetId != null)) + && keySetId != null + && keySetId.length != 0) { + offlineLicenseKeySetId = keySetId; + } + state = STATE_OPENED_WITH_KEYS; + eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysLoaded); + } + } catch (Exception e) { + onKeysError(e); + } + } + + private void onKeysRequired() { + if (mode == DefaultDrmSessionManager.MODE_PLAYBACK && state == STATE_OPENED_WITH_KEYS) { + Util.castNonNull(sessionId); + doLicense(/* allowRetry= */ false); + } + } + + private void onKeysError(Exception e) { + if (e instanceof NotProvisionedException) { + provisioningManager.provisionRequired(this); + } else { + onError(e); + } + } + + private void onError(final Exception e) { + lastException = new DrmSessionException(e); + eventDispatcher.dispatch(listener -> listener.onDrmSessionManagerError(e)); + if (state != STATE_OPENED_WITH_KEYS) { + state = STATE_ERROR; + } + } + + @EnsuresNonNullIf(result = true, expression = "sessionId") + @SuppressWarnings("contracts.conditional.postcondition.not.satisfied") + private boolean isOpen() { + return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS; + } + + // Internal classes. + + @SuppressLint("HandlerLeak") + private class ResponseHandler extends Handler { + + public ResponseHandler(Looper looper) { + super(looper); + } + + @Override + @SuppressWarnings("unchecked") + public void handleMessage(Message msg) { + Pair requestAndResponse = (Pair) msg.obj; + Object request = requestAndResponse.first; + Object response = requestAndResponse.second; + switch (msg.what) { + case MSG_PROVISION: + onProvisionResponse(request, response); + break; + case MSG_KEYS: + onKeyResponse(request, response); + break; + default: + break; + } + } + } + + @SuppressLint("HandlerLeak") + private class RequestHandler extends Handler { + + public RequestHandler(Looper backgroundLooper) { + super(backgroundLooper); + } + + void post(int what, Object request, boolean allowRetry) { + RequestTask requestTask = + new RequestTask(allowRetry, /* startTimeMs= */ SystemClock.elapsedRealtime(), request); + obtainMessage(what, requestTask).sendToTarget(); + } + + @Override + public void handleMessage(Message msg) { + RequestTask requestTask = (RequestTask) msg.obj; + Object response; + try { + switch (msg.what) { + case MSG_PROVISION: + response = + callback.executeProvisionRequest(uuid, (ProvisionRequest) requestTask.request); + break; + case MSG_KEYS: + response = callback.executeKeyRequest(uuid, (KeyRequest) requestTask.request); + break; + default: + throw new RuntimeException(); + } + } catch (Exception e) { + if (maybeRetryRequest(msg, e)) { + return; + } + response = e; + } + responseHandler + .obtainMessage(msg.what, Pair.create(requestTask.request, response)) + .sendToTarget(); + } + + private boolean maybeRetryRequest(Message originalMsg, Exception e) { + RequestTask requestTask = (RequestTask) originalMsg.obj; + if (!requestTask.allowRetry) { + return false; + } + requestTask.errorCount++; + if (requestTask.errorCount + > loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_DRM)) { + return false; + } + IOException ioException = + e instanceof IOException ? (IOException) e : new UnexpectedDrmSessionException(e); + long retryDelayMs = + loadErrorHandlingPolicy.getRetryDelayMsFor( + C.DATA_TYPE_DRM, + /* loadDurationMs= */ SystemClock.elapsedRealtime() - requestTask.startTimeMs, + ioException, + requestTask.errorCount); + if (retryDelayMs == C.TIME_UNSET) { + // The error is fatal. + return false; + } + sendMessageDelayed(Message.obtain(originalMsg), retryDelayMs); + return true; + } + } + + private static final class RequestTask { + + public final boolean allowRetry; + public final long startTimeMs; + public final Object request; + public int errorCount; + + public RequestTask(boolean allowRetry, long startTimeMs, Object request) { + this.allowRetry = allowRetry; + this.startTimeMs = startTimeMs; + this.request = request; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java new file mode 100644 index 000000000000..35bc7faf2843 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.drm; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player; + +/** Listener of {@link DefaultDrmSessionManager} events. */ +public interface DefaultDrmSessionEventListener { + + /** Called each time a drm session is acquired. */ + default void onDrmSessionAcquired() {} + + /** Called each time keys are loaded. */ + default void onDrmKeysLoaded() {} + + /** + * Called when a drm error occurs. + * + *

This method being called does not indicate that playback has failed, or that it will fail. + * The player may be able to recover from the error and continue. Hence applications should + * not implement this method to display a user visible error or initiate an application + * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement + * such behavior). This method is called to provide the application with an opportunity to log the + * error if it wishes to do so. + * + * @param error The corresponding exception. + */ + default void onDrmSessionManagerError(Exception error) {} + + /** Called each time offline keys are restored. */ + default void onDrmKeysRestored() {} + + /** Called each time offline keys are removed. */ + default void onDrmKeysRemoved() {} + + /** Called each time a drm session is released. */ + default void onDrmSessionReleased() {} +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 95bf052a250f..683862b99af7 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -17,74 +17,204 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.drm; import android.annotation.SuppressLint; import android.annotation.TargetApi; -import android.media.DeniedByServerException; -import android.media.MediaDrm; -import android.media.NotProvisionedException; import android.os.Handler; -import android.os.HandlerThread; import android.os.Looper; import android.os.Message; -import android.support.annotation.IntDef; -import android.text.TextUtils; -import android.util.Log; -import android.util.Pair; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData; -import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; -import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.EventDispatcher; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; -/** - * A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}. - */ +/** A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */ @TargetApi(18) -public class DefaultDrmSessionManager implements DrmSessionManager, - DrmSession { +public class DefaultDrmSessionManager implements DrmSessionManager { /** - * Listener of {@link DefaultDrmSessionManager} events. + * Builder for {@link DefaultDrmSessionManager} instances. + * + *

See {@link #Builder} for the list of default values. */ - public interface EventListener { + public static final class Builder { + + private final HashMap keyRequestParameters; + private UUID uuid; + private ExoMediaDrm.Provider exoMediaDrmProvider; + private boolean multiSession; + private int[] useDrmSessionsForClearContentTrackTypes; + private boolean playClearSamplesWithoutKeys; + private LoadErrorHandlingPolicy loadErrorHandlingPolicy; /** - * Called each time keys are loaded. - */ - void onDrmKeysLoaded(); - - /** - * Called when a drm error occurs. + * Creates a builder with default values. The default values are: * - * @param e The corresponding exception. + *

    + *
  • {@link #setKeyRequestParameters keyRequestParameters}: An empty map. + *
  • {@link #setUuidAndExoMediaDrmProvider UUID}: {@link C#WIDEVINE_UUID}. + *
  • {@link #setUuidAndExoMediaDrmProvider ExoMediaDrm.Provider}: {@link + * FrameworkMediaDrm#DEFAULT_PROVIDER}. + *
  • {@link #setMultiSession multiSession}: {@code false}. + *
  • {@link #setUseDrmSessionsForClearContent useDrmSessionsForClearContent}: No tracks. + *
  • {@link #setPlayClearSamplesWithoutKeys playClearSamplesWithoutKeys}: {@code false}. + *
  • {@link #setLoadErrorHandlingPolicy LoadErrorHandlingPolicy}: {@link + * DefaultLoadErrorHandlingPolicy}. + *
*/ - void onDrmSessionManagerError(Exception e); + @SuppressWarnings("unchecked") + public Builder() { + keyRequestParameters = new HashMap<>(); + uuid = C.WIDEVINE_UUID; + exoMediaDrmProvider = (ExoMediaDrm.Provider) FrameworkMediaDrm.DEFAULT_PROVIDER; + loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); + useDrmSessionsForClearContentTrackTypes = new int[0]; + } /** - * Called each time offline keys are restored. + * Sets the key request parameters to pass as the last argument to {@link + * ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. + * + *

Custom data for PlayReady should be set under {@link #PLAYREADY_CUSTOM_DATA_KEY}. + * + * @param keyRequestParameters A map with parameters. + * @return This builder. */ - void onDrmKeysRestored(); + public Builder setKeyRequestParameters(Map keyRequestParameters) { + this.keyRequestParameters.clear(); + this.keyRequestParameters.putAll(Assertions.checkNotNull(keyRequestParameters)); + return this; + } /** - * Called each time offline keys are removed. + * Sets the UUID of the DRM scheme and the {@link ExoMediaDrm.Provider} to use. + * + * @param uuid The UUID of the DRM scheme. + * @param exoMediaDrmProvider The {@link ExoMediaDrm.Provider}. + * @return This builder. */ - void onDrmKeysRemoved(); + @SuppressWarnings({"rawtypes", "unchecked"}) + public Builder setUuidAndExoMediaDrmProvider( + UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider) { + this.uuid = Assertions.checkNotNull(uuid); + this.exoMediaDrmProvider = Assertions.checkNotNull(exoMediaDrmProvider); + return this; + } + /** + * Sets whether this session manager is allowed to acquire multiple simultaneous sessions. + * + *

Users should pass false when a single key request will obtain all keys required to decrypt + * the associated content. {@code multiSession} is required when content uses key rotation. + * + * @param multiSession Whether this session manager is allowed to acquire multiple simultaneous + * sessions. + * @return This builder. + */ + public Builder setMultiSession(boolean multiSession) { + this.multiSession = multiSession; + return this; + } + + /** + * Sets whether this session manager should attach {@link DrmSession DrmSessions} to the clear + * sections of the media content. + * + *

Using {@link DrmSession DrmSessions} for clear content avoids the recreation of decoders + * when transitioning between clear and encrypted sections of content. + * + * @param useDrmSessionsForClearContentTrackTypes The track types ({@link C#TRACK_TYPE_AUDIO} + * and/or {@link C#TRACK_TYPE_VIDEO}) for which to use a {@link DrmSession} regardless of + * whether the content is clear or encrypted. + * @return This builder. + * @throws IllegalArgumentException If {@code useDrmSessionsForClearContentTrackTypes} contains + * track types other than {@link C#TRACK_TYPE_AUDIO} and {@link C#TRACK_TYPE_VIDEO}. + */ + public Builder setUseDrmSessionsForClearContent( + int... useDrmSessionsForClearContentTrackTypes) { + for (int trackType : useDrmSessionsForClearContentTrackTypes) { + Assertions.checkArgument( + trackType == C.TRACK_TYPE_VIDEO || trackType == C.TRACK_TYPE_AUDIO); + } + this.useDrmSessionsForClearContentTrackTypes = + useDrmSessionsForClearContentTrackTypes.clone(); + return this; + } + + /** + * Sets whether clear samples within protected content should be played when keys for the + * encrypted part of the content have yet to be loaded. + * + * @param playClearSamplesWithoutKeys Whether clear samples within protected content should be + * played when keys for the encrypted part of the content have yet to be loaded. + * @return This builder. + */ + public Builder setPlayClearSamplesWithoutKeys(boolean playClearSamplesWithoutKeys) { + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + return this; + } + + /** + * Sets the {@link LoadErrorHandlingPolicy} for key and provisioning requests. + * + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @return This builder. + */ + public Builder setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + this.loadErrorHandlingPolicy = Assertions.checkNotNull(loadErrorHandlingPolicy); + return this; + } + + /** Builds a {@link DefaultDrmSessionManager} instance. */ + public DefaultDrmSessionManager build(MediaDrmCallback mediaDrmCallback) { + return new DefaultDrmSessionManager<>( + uuid, + exoMediaDrmProvider, + mediaDrmCallback, + keyRequestParameters, + multiSession, + useDrmSessionsForClearContentTrackTypes, + playClearSamplesWithoutKeys, + loadErrorHandlingPolicy); + } } /** - * The key to use when passing CustomData to a PlayReady instance in an optional parameter map. + * Signals that the {@link DrmInitData} passed to {@link #acquireSession} does not contain does + * not contain scheme data for the required UUID. + */ + public static final class MissingSchemeDataException extends Exception { + + private MissingSchemeDataException(UUID uuid) { + super("Media does not support uuid: " + uuid); + } + } + + /** + * A key for specifying PlayReady custom data in the key request parameters passed to {@link + * Builder#setKeyRequestParameters(Map)}. */ public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData"; - /** Determines the action to be done after a session acquired. */ + /** + * Determines the action to be done after a session acquired. One of {@link #MODE_PLAYBACK}, + * {@link #MODE_QUERY}, {@link #MODE_DOWNLOAD} or {@link #MODE_RELEASE}. + */ + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({MODE_PLAYBACK, MODE_QUERY, MODE_DOWNLOAD, MODE_RELEASE}) public @interface Mode {} @@ -93,211 +223,195 @@ public class DefaultDrmSessionManager implements DrmSe * licenses. */ public static final int MODE_PLAYBACK = 0; - /** - * Restores an offline license to allow its status to be queried. If the offline license is - * expired sets state to {@link #STATE_ERROR}. - */ + /** Restores an offline license to allow its status to be queried. */ public static final int MODE_QUERY = 1; /** Downloads an offline license or renews an existing one. */ public static final int MODE_DOWNLOAD = 2; /** Releases an existing offline license. */ public static final int MODE_RELEASE = 3; + /** Number of times to retry for initial provisioning and key request for reporting error. */ + public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3; - private static final String TAG = "OfflineDrmSessionMngr"; - private static final String CENC_SCHEME_MIME_TYPE = "cenc"; + private static final String TAG = "DefaultDrmSessionMgr"; - private static final int MSG_PROVISION = 0; - private static final int MSG_KEYS = 1; + private final UUID uuid; + private final ExoMediaDrm.Provider exoMediaDrmProvider; + private final MediaDrmCallback callback; + private final HashMap keyRequestParameters; + private final EventDispatcher eventDispatcher; + private final boolean multiSession; + private final int[] useDrmSessionsForClearContentTrackTypes; + private final boolean playClearSamplesWithoutKeys; + private final ProvisioningManagerImpl provisioningManagerImpl; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; - private static final int MAX_LICENSE_DURATION_TO_RENEW = 60; - - private final Handler eventHandler; - private final EventListener eventListener; - private final ExoMediaDrm mediaDrm; - private final HashMap optionalKeyRequestParameters; - - /* package */ final MediaDrmCallback callback; - /* package */ final UUID uuid; - - /* package */ MediaDrmHandler mediaDrmHandler; - /* package */ PostResponseHandler postResponseHandler; - - private Looper playbackLooper; - private HandlerThread requestHandlerThread; - private Handler postRequestHandler; + private final List> sessions; + private final List> provisioningSessions; + private int prepareCallsCount; + @Nullable private ExoMediaDrm exoMediaDrm; + @Nullable private DefaultDrmSession placeholderDrmSession; + @Nullable private DefaultDrmSession noMultiSessionDrmSession; + @Nullable private Looper playbackLooper; private int mode; - private int openCount; - private boolean provisioningInProgress; - @DrmSession.State - private int state; - private T mediaCrypto; - private DrmSessionException lastException; - private byte[] schemeInitData; - private String schemeMimeType; - private byte[] sessionId; - private byte[] offlineLicenseKeySetId; + @Nullable private byte[] offlineLicenseKeySetId; + + /* package */ volatile @Nullable MediaDrmHandler mediaDrmHandler; /** - * Instantiates a new instance using the Widevine scheme. - * - * @param callback Performs key and provisioning requests. - * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @throws UnsupportedDrmException If the specified DRM scheme is not supported. - */ - public static DefaultDrmSessionManager newWidevineInstance( - MediaDrmCallback callback, HashMap optionalKeyRequestParameters, - Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { - return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters, - eventHandler, eventListener); - } - - /** - * Instantiates a new instance using the PlayReady scheme. - *

- * Note that PlayReady is unsupported by most Android devices, with the exception of Android TV - * devices, which do provide support. - * - * @param callback Performs key and provisioning requests. - * @param customData Optional custom data to include in requests generated by the instance. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @throws UnsupportedDrmException If the specified DRM scheme is not supported. - */ - public static DefaultDrmSessionManager newPlayReadyInstance( - MediaDrmCallback callback, String customData, Handler eventHandler, - EventListener eventListener) throws UnsupportedDrmException { - HashMap optionalKeyRequestParameters; - if (!TextUtils.isEmpty(customData)) { - optionalKeyRequestParameters = new HashMap<>(); - optionalKeyRequestParameters.put(PLAYREADY_CUSTOM_DATA_KEY, customData); - } else { - optionalKeyRequestParameters = null; - } - return newFrameworkInstance(C.PLAYREADY_UUID, callback, optionalKeyRequestParameters, - eventHandler, eventListener); - } - - /** - * Instantiates a new instance. - * * @param uuid The UUID of the drm scheme. + * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. - * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @throws UnsupportedDrmException If the specified DRM scheme is not supported. + * @param keyRequestParameters An optional map of parameters to pass as the last argument to + * {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. + * @deprecated Use {@link Builder} instead. */ - public static DefaultDrmSessionManager newFrameworkInstance( - UUID uuid, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, - Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { - return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback, - optionalKeyRequestParameters, eventHandler, eventListener); + @SuppressWarnings("deprecation") + @Deprecated + public DefaultDrmSessionManager( + UUID uuid, + ExoMediaDrm exoMediaDrm, + MediaDrmCallback callback, + @Nullable HashMap keyRequestParameters) { + this( + uuid, + exoMediaDrm, + callback, + keyRequestParameters == null ? new HashMap<>() : keyRequestParameters, + /* multiSession= */ false, + INITIAL_DRM_REQUEST_RETRY_COUNT); } /** * @param uuid The UUID of the drm scheme. - * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. + * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. - * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param keyRequestParameters An optional map of parameters to pass as the last argument to + * {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. + * @param multiSession A boolean that specify whether multiple key session support is enabled. + * Default is false. + * @deprecated Use {@link Builder} instead. */ - public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, - HashMap optionalKeyRequestParameters, Handler eventHandler, - EventListener eventListener) { + @Deprecated + public DefaultDrmSessionManager( + UUID uuid, + ExoMediaDrm exoMediaDrm, + MediaDrmCallback callback, + @Nullable HashMap keyRequestParameters, + boolean multiSession) { + this( + uuid, + exoMediaDrm, + callback, + keyRequestParameters == null ? new HashMap<>() : keyRequestParameters, + multiSession, + INITIAL_DRM_REQUEST_RETRY_COUNT); + } + + /** + * @param uuid The UUID of the drm scheme. + * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager. + * @param callback Performs key and provisioning requests. + * @param keyRequestParameters An optional map of parameters to pass as the last argument to + * {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. + * @param multiSession A boolean that specify whether multiple key session support is enabled. + * Default is false. + * @param initialDrmRequestRetryCount The number of times to retry for initial provisioning and + * key request before reporting error. + * @deprecated Use {@link Builder} instead. + */ + @Deprecated + public DefaultDrmSessionManager( + UUID uuid, + ExoMediaDrm exoMediaDrm, + MediaDrmCallback callback, + @Nullable HashMap keyRequestParameters, + boolean multiSession, + int initialDrmRequestRetryCount) { + this( + uuid, + new ExoMediaDrm.AppManagedProvider<>(exoMediaDrm), + callback, + keyRequestParameters == null ? new HashMap<>() : keyRequestParameters, + multiSession, + /* useDrmSessionsForClearContentTrackTypes= */ new int[0], + /* playClearSamplesWithoutKeys= */ false, + new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount)); + } + + // the constructor does not initialize fields: offlineLicenseKeySetId + @SuppressWarnings("nullness:initialization.fields.uninitialized") + private DefaultDrmSessionManager( + UUID uuid, + ExoMediaDrm.Provider exoMediaDrmProvider, + MediaDrmCallback callback, + HashMap keyRequestParameters, + boolean multiSession, + int[] useDrmSessionsForClearContentTrackTypes, + boolean playClearSamplesWithoutKeys, + LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + Assertions.checkNotNull(uuid); + Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); this.uuid = uuid; - this.mediaDrm = mediaDrm; + this.exoMediaDrmProvider = exoMediaDrmProvider; this.callback = callback; - this.optionalKeyRequestParameters = optionalKeyRequestParameters; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - mediaDrm.setOnEventListener(new MediaDrmEventListener()); - state = STATE_CLOSED; + this.keyRequestParameters = keyRequestParameters; + this.eventDispatcher = new EventDispatcher<>(); + this.multiSession = multiSession; + this.useDrmSessionsForClearContentTrackTypes = useDrmSessionsForClearContentTrackTypes; + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + provisioningManagerImpl = new ProvisioningManagerImpl(); mode = MODE_PLAYBACK; + sessions = new ArrayList<>(); + provisioningSessions = new ArrayList<>(); } /** - * Provides access to {@link MediaDrm#getPropertyString(String)}. - *

- * This method may be called when the manager is in any state. + * Adds a {@link DefaultDrmSessionEventListener} to listen to drm session events. * - * @param key The key to request. - * @return The retrieved property. + * @param handler A handler to use when delivering events to {@code eventListener}. + * @param eventListener A listener of events. */ - public final String getPropertyString(String key) { - return mediaDrm.getPropertyString(key); + public final void addListener(Handler handler, DefaultDrmSessionEventListener eventListener) { + eventDispatcher.addListener(handler, eventListener); } /** - * Provides access to {@link MediaDrm#setPropertyString(String, String)}. - *

- * This method may be called when the manager is in any state. + * Removes a {@link DefaultDrmSessionEventListener} from the list of drm session event listeners. * - * @param key The property to write. - * @param value The value to write. + * @param eventListener The listener to remove. */ - public final void setPropertyString(String key, String value) { - mediaDrm.setPropertyString(key, value); - } - - /** - * Provides access to {@link MediaDrm#getPropertyByteArray(String)}. - *

- * This method may be called when the manager is in any state. - * - * @param key The key to request. - * @return The retrieved property. - */ - public final byte[] getPropertyByteArray(String key) { - return mediaDrm.getPropertyByteArray(key); - } - - /** - * Provides access to {@link MediaDrm#setPropertyByteArray(String, byte[])}. - *

- * This method may be called when the manager is in any state. - * - * @param key The property to write. - * @param value The value to write. - */ - public final void setPropertyByteArray(String key, byte[] value) { - mediaDrm.setPropertyByteArray(key, value); + public final void removeListener(DefaultDrmSessionEventListener eventListener) { + eventDispatcher.removeListener(eventListener); } /** * Sets the mode, which determines the role of sessions acquired from the instance. This must be - * called before {@link #acquireSession(Looper, DrmInitData)} is called. + * called before {@link #acquireSession(Looper, DrmInitData)} or {@link + * #acquirePlaceholderSession} is called. * *

By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when * required. * *

{@code mode} must be one of these: + * *

    - *
  • {@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is - * requested otherwise the offline license is restored. - *
  • {@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license - * is restored. - *
  • {@link #MODE_DOWNLOAD}: If {@code offlineLicenseKeySetId} is null, an offline license is - * requested otherwise the offline license is renewed. - *
  • {@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline license - * is released. + *
  • {@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is + * requested otherwise the offline license is restored. + *
  • {@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license + * is restored. + *
  • {@link #MODE_DOWNLOAD}: If {@code offlineLicenseKeySetId} is null, an offline license is + * requested otherwise the offline license is renewed. + *
  • {@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline + * license is released. *
* * @param mode The mode to be set. * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode. */ - public void setMode(@Mode int mode, byte[] offlineLicenseKeySetId) { - Assertions.checkState(openCount == 0); + public void setMode(@Mode int mode, @Nullable byte[] offlineLicenseKeySetId) { + Assertions.checkState(sessions.isEmpty()); if (mode == MODE_QUERY || mode == MODE_RELEASE) { Assertions.checkNotNull(offlineLicenseKeySetId); } @@ -308,311 +422,204 @@ public class DefaultDrmSessionManager implements DrmSe // DrmSessionManager implementation. @Override - public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData) { - Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper); - if (++openCount != 1) { - return this; + public final void prepare() { + if (prepareCallsCount++ == 0) { + Assertions.checkState(exoMediaDrm == null); + exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid); + exoMediaDrm.setOnEventListener(new MediaDrmEventListener()); } + } - if (this.playbackLooper == null) { - this.playbackLooper = playbackLooper; - mediaDrmHandler = new MediaDrmHandler(playbackLooper); - postResponseHandler = new PostResponseHandler(playbackLooper); + @Override + public final void release() { + if (--prepareCallsCount == 0) { + Assertions.checkNotNull(exoMediaDrm).release(); + exoMediaDrm = null; } + } - requestHandlerThread = new HandlerThread("DrmRequestHandler"); - requestHandlerThread.start(); - postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); - - if (offlineLicenseKeySetId == null) { - SchemeData schemeData = drmInitData.get(uuid); - if (schemeData == null) { - onError(new IllegalStateException("Media does not support uuid: " + uuid)); - return this; + @Override + public boolean canAcquireSession(DrmInitData drmInitData) { + if (offlineLicenseKeySetId != null) { + // An offline license can be restored so a session can always be acquired. + return true; + } + List schemeDatas = getSchemeDatas(drmInitData, uuid, true); + if (schemeDatas.isEmpty()) { + if (drmInitData.schemeDataCount == 1 && drmInitData.get(0).matches(C.COMMON_PSSH_UUID)) { + // Assume scheme specific data will be added before the session is opened. + Log.w( + TAG, "DrmInitData only contains common PSSH SchemeData. Assuming support for: " + uuid); + } else { + // No data for this manager's scheme. + return false; } - schemeInitData = schemeData.data; - schemeMimeType = schemeData.mimeType; - if (Util.SDK_INT < 21) { - // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. - byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, C.WIDEVINE_UUID); - if (psshData == null) { - // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. - } else { - schemeInitData = psshData; + } + String schemeType = drmInitData.schemeType; + if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) { + // If there is no scheme information, assume patternless AES-CTR. + return true; + } else if (C.CENC_TYPE_cbc1.equals(schemeType) + || C.CENC_TYPE_cbcs.equals(schemeType) + || C.CENC_TYPE_cens.equals(schemeType)) { + // API support for AES-CBC and pattern encryption was added in API 24. However, the + // implementation was not stable until API 25. + return Util.SDK_INT >= 25; + } + // Unknown schemes, assume one of them is supported. + return true; + } + + @Override + @Nullable + public DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) { + assertExpectedPlaybackLooper(playbackLooper); + ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm); + boolean avoidPlaceholderDrmSessions = + FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType()) + && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC; + // Avoid attaching a session to sparse formats. + if (avoidPlaceholderDrmSessions + || Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) == C.INDEX_UNSET + || exoMediaDrm.getExoMediaCryptoType() == null) { + return null; + } + maybeCreateMediaDrmHandler(playbackLooper); + if (placeholderDrmSession == null) { + DefaultDrmSession placeholderDrmSession = + createNewDefaultSession( + /* schemeDatas= */ Collections.emptyList(), /* isPlaceholderSession= */ true); + sessions.add(placeholderDrmSession); + this.placeholderDrmSession = placeholderDrmSession; + } + placeholderDrmSession.acquire(); + return placeholderDrmSession; + } + + @Override + public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData) { + assertExpectedPlaybackLooper(playbackLooper); + maybeCreateMediaDrmHandler(playbackLooper); + + @Nullable List schemeDatas = null; + if (offlineLicenseKeySetId == null) { + schemeDatas = getSchemeDatas(drmInitData, uuid, false); + if (schemeDatas.isEmpty()) { + final MissingSchemeDataException error = new MissingSchemeDataException(uuid); + eventDispatcher.dispatch(listener -> listener.onDrmSessionManagerError(error)); + return new ErrorStateDrmSession<>(new DrmSessionException(error)); + } + } + + @Nullable DefaultDrmSession session; + if (!multiSession) { + session = noMultiSessionDrmSession; + } else { + // Only use an existing session if it has matching init data. + session = null; + for (DefaultDrmSession existingSession : sessions) { + if (Util.areEqual(existingSession.schemeDatas, schemeDatas)) { + session = existingSession; + break; } } - if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid) - && (MimeTypes.VIDEO_MP4.equals(schemeMimeType) - || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) { - // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4. - schemeMimeType = CENC_SCHEME_MIME_TYPE; + } + + if (session == null) { + // Create a new session. + session = createNewDefaultSession(schemeDatas, /* isPlaceholderSession= */ false); + if (!multiSession) { + noMultiSessionDrmSession = session; } + sessions.add(session); } - state = STATE_OPENING; - openInternal(true); - return this; + session.acquire(); + return session; } @Override - public void releaseSession(DrmSession session) { - if (--openCount != 0) { - return; - } - state = STATE_CLOSED; - provisioningInProgress = false; - mediaDrmHandler.removeCallbacksAndMessages(null); - postResponseHandler.removeCallbacksAndMessages(null); - postRequestHandler.removeCallbacksAndMessages(null); - postRequestHandler = null; - requestHandlerThread.quit(); - requestHandlerThread = null; - schemeInitData = null; - schemeMimeType = null; - mediaCrypto = null; - lastException = null; - if (sessionId != null) { - mediaDrm.closeSession(sessionId); - sessionId = null; - } - } - - // DrmSession implementation. - - @Override - @DrmSession.State - public final int getState() { - return state; - } - - @Override - public final T getMediaCrypto() { - if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) { - throw new IllegalStateException(); - } - return mediaCrypto; - } - - @Override - public boolean requiresSecureDecoderComponent(String mimeType) { - if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) { - throw new IllegalStateException(); - } - return mediaCrypto.requiresSecureDecoderComponent(mimeType); - } - - @Override - public final DrmSessionException getError() { - return state == STATE_ERROR ? lastException : null; - } - - @Override - public Map queryKeyStatus() { - // User may call this method rightfully even if state == STATE_ERROR. So only check if there is - // a sessionId - if (sessionId == null) { - throw new IllegalStateException(); - } - return mediaDrm.queryKeyStatus(sessionId); - } - - @Override - public byte[] getOfflineLicenseKeySetId() { - return offlineLicenseKeySetId; + @Nullable + public Class getExoMediaCryptoType(DrmInitData drmInitData) { + return canAcquireSession(drmInitData) + ? Assertions.checkNotNull(exoMediaDrm).getExoMediaCryptoType() + : null; } // Internal methods. - private void openInternal(boolean allowProvisioning) { - try { - sessionId = mediaDrm.openSession(); - mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId); - state = STATE_OPENED; - doLicense(); - } catch (NotProvisionedException e) { - if (allowProvisioning) { - postProvisionRequest(); - } else { - onError(e); + private void assertExpectedPlaybackLooper(Looper playbackLooper) { + Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper); + this.playbackLooper = playbackLooper; + } + + private void maybeCreateMediaDrmHandler(Looper playbackLooper) { + if (mediaDrmHandler == null) { + mediaDrmHandler = new MediaDrmHandler(playbackLooper); + } + } + + private DefaultDrmSession createNewDefaultSession( + @Nullable List schemeDatas, boolean isPlaceholderSession) { + Assertions.checkNotNull(exoMediaDrm); + // Placeholder sessions should always play clear samples without keys. + boolean playClearSamplesWithoutKeys = this.playClearSamplesWithoutKeys | isPlaceholderSession; + return new DefaultDrmSession<>( + uuid, + exoMediaDrm, + /* provisioningManager= */ provisioningManagerImpl, + /* releaseCallback= */ this::onSessionReleased, + schemeDatas, + mode, + playClearSamplesWithoutKeys, + isPlaceholderSession, + offlineLicenseKeySetId, + keyRequestParameters, + callback, + Assertions.checkNotNull(playbackLooper), + eventDispatcher, + loadErrorHandlingPolicy); + } + + private void onSessionReleased(DefaultDrmSession drmSession) { + sessions.remove(drmSession); + if (placeholderDrmSession == drmSession) { + placeholderDrmSession = null; + } + if (noMultiSessionDrmSession == drmSession) { + noMultiSessionDrmSession = null; + } + if (provisioningSessions.size() > 1 && provisioningSessions.get(0) == drmSession) { + // Other sessions were waiting for the released session to complete a provision operation. + // We need to have one of those sessions perform the provision operation instead. + provisioningSessions.get(1).provision(); + } + provisioningSessions.remove(drmSession); + } + + /** + * Extracts {@link SchemeData} instances suitable for the given DRM scheme {@link UUID}. + * + * @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}. + * @param uuid The UUID. + * @param allowMissingData Whether a {@link SchemeData} with null {@link SchemeData#data} may be + * returned. + * @return The extracted {@link SchemeData} instances, or an empty list if no suitable data is + * present. + */ + private static List getSchemeDatas( + DrmInitData drmInitData, UUID uuid, boolean allowMissingData) { + // Look for matching scheme data (matching the Common PSSH box for ClearKey). + List matchingSchemeDatas = new ArrayList<>(drmInitData.schemeDataCount); + for (int i = 0; i < drmInitData.schemeDataCount; i++) { + SchemeData schemeData = drmInitData.get(i); + boolean uuidMatches = + schemeData.matches(uuid) + || (C.CLEARKEY_UUID.equals(uuid) && schemeData.matches(C.COMMON_PSSH_UUID)); + if (uuidMatches && (schemeData.data != null || allowMissingData)) { + matchingSchemeDatas.add(schemeData); } - } catch (Exception e) { - onError(e); - } - } - - private void postProvisionRequest() { - if (provisioningInProgress) { - return; - } - provisioningInProgress = true; - ProvisionRequest request = mediaDrm.getProvisionRequest(); - postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget(); - } - - private void onProvisionResponse(Object response) { - provisioningInProgress = false; - if (state != STATE_OPENING && state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) { - // This event is stale. - return; - } - - if (response instanceof Exception) { - onError((Exception) response); - return; - } - - try { - mediaDrm.provideProvisionResponse((byte[]) response); - if (state == STATE_OPENING) { - openInternal(false); - } else { - doLicense(); - } - } catch (DeniedByServerException e) { - onError(e); - } - } - - private void doLicense() { - switch (mode) { - case MODE_PLAYBACK: - case MODE_QUERY: - if (offlineLicenseKeySetId == null) { - postKeyRequest(sessionId, MediaDrm.KEY_TYPE_STREAMING); - } else { - if (restoreKeys()) { - long licenseDurationRemainingSec = getLicenseDurationRemainingSec(); - if (mode == MODE_PLAYBACK - && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) { - Log.d(TAG, "Offline license has expired or will expire soon. " - + "Remaining seconds: " + licenseDurationRemainingSec); - postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE); - } else if (licenseDurationRemainingSec <= 0) { - onError(new KeysExpiredException()); - } else { - state = STATE_OPENED_WITH_KEYS; - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrmKeysRestored(); - } - }); - } - } - } - } - break; - case MODE_DOWNLOAD: - if (offlineLicenseKeySetId == null) { - postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE); - } else { - // Renew - if (restoreKeys()) { - postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE); - } - } - break; - case MODE_RELEASE: - if (restoreKeys()) { - postKeyRequest(offlineLicenseKeySetId, MediaDrm.KEY_TYPE_RELEASE); - } - break; - } - } - - private boolean restoreKeys() { - try { - mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId); - return true; - } catch (Exception e) { - Log.e(TAG, "Error trying to restore Widevine keys.", e); - onError(e); - } - return false; - } - - private long getLicenseDurationRemainingSec() { - if (!C.WIDEVINE_UUID.equals(uuid)) { - return Long.MAX_VALUE; - } - Pair pair = WidevineUtil.getLicenseDurationRemainingSec(this); - return Math.min(pair.first, pair.second); - } - - private void postKeyRequest(byte[] scope, int keyType) { - try { - KeyRequest keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType, - optionalKeyRequestParameters); - postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget(); - } catch (Exception e) { - onKeysError(e); - } - } - - private void onKeyResponse(Object response) { - if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) { - // This event is stale. - return; - } - - if (response instanceof Exception) { - onKeysError((Exception) response); - return; - } - - try { - if (mode == MODE_RELEASE) { - mediaDrm.provideKeyResponse(offlineLicenseKeySetId, (byte[]) response); - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrmKeysRemoved(); - } - }); - } - } else { - byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response); - if ((mode == MODE_DOWNLOAD || (mode == MODE_PLAYBACK && offlineLicenseKeySetId != null)) - && keySetId != null && keySetId.length != 0) { - offlineLicenseKeySetId = keySetId; - } - state = STATE_OPENED_WITH_KEYS; - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrmKeysLoaded(); - } - }); - } - } - } catch (Exception e) { - onKeysError(e); - } - } - - private void onKeysError(Exception e) { - if (e instanceof NotProvisionedException) { - postProvisionRequest(); - } else { - onError(e); - } - } - - private void onError(final Exception e) { - lastException = new DrmSessionException(e); - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrmSessionManagerError(e); - } - }); - } - if (state != STATE_OPENED_WITH_KEYS) { - state = STATE_ERROR; } + return matchingSchemeDatas; } @SuppressLint("HandlerLeak") @@ -622,94 +629,63 @@ public class DefaultDrmSessionManager implements DrmSe super(looper); } - @SuppressWarnings("deprecation") @Override public void handleMessage(Message msg) { - if (openCount == 0 || (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS)) { + byte[] sessionId = (byte[]) msg.obj; + if (sessionId == null) { + // The event is not associated with any particular session. return; } - switch (msg.what) { - case MediaDrm.EVENT_KEY_REQUIRED: - doLicense(); - break; - case MediaDrm.EVENT_KEY_EXPIRED: - // When an already expired key is loaded MediaDrm sends this event immediately. Ignore - // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still - // waiting for key response. - if (state == STATE_OPENED_WITH_KEYS) { - state = STATE_OPENED; - onError(new KeysExpiredException()); - } - break; - case MediaDrm.EVENT_PROVISION_REQUIRED: - state = STATE_OPENED; - postProvisionRequest(); - break; + for (DefaultDrmSession session : sessions) { + if (session.hasSessionId(sessionId)) { + session.onMediaDrmEvent(msg.what); + return; + } + } + } + } + + private class ProvisioningManagerImpl implements DefaultDrmSession.ProvisioningManager { + @Override + public void provisionRequired(DefaultDrmSession session) { + if (provisioningSessions.contains(session)) { + // The session has already requested provisioning. + return; + } + provisioningSessions.add(session); + if (provisioningSessions.size() == 1) { + // This is the first session requesting provisioning, so have it perform the operation. + session.provision(); } } + @Override + public void onProvisionCompleted() { + for (DefaultDrmSession session : provisioningSessions) { + session.onProvisionCompleted(); + } + provisioningSessions.clear(); + } + + @Override + public void onProvisionError(Exception error) { + for (DefaultDrmSession session : provisioningSessions) { + session.onProvisionError(error); + } + provisioningSessions.clear(); + } } private class MediaDrmEventListener implements OnEventListener { @Override - public void onEvent(ExoMediaDrm md, byte[] sessionId, int event, int extra, - byte[] data) { - if (mode == MODE_PLAYBACK) { - mediaDrmHandler.sendEmptyMessage(event); - } + public void onEvent( + ExoMediaDrm md, + @Nullable byte[] sessionId, + int event, + int extra, + @Nullable byte[] data) { + Assertions.checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget(); } - } - - @SuppressLint("HandlerLeak") - private class PostResponseHandler extends Handler { - - public PostResponseHandler(Looper looper) { - super(looper); - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_PROVISION: - onProvisionResponse(msg.obj); - break; - case MSG_KEYS: - onKeyResponse(msg.obj); - break; - } - } - - } - - @SuppressLint("HandlerLeak") - private class PostRequestHandler extends Handler { - - public PostRequestHandler(Looper backgroundLooper) { - super(backgroundLooper); - } - - @Override - public void handleMessage(Message msg) { - Object response; - try { - switch (msg.what) { - case MSG_PROVISION: - response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj); - break; - case MSG_KEYS: - response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj); - break; - default: - throw new RuntimeException(); - } - } catch (Exception e) { - response = e; - } - postResponseHandler.obtainMessage(msg.what, response).sendToTarget(); - } - - } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmInitData.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmInitData.java index 5e0b7056e1c5..2a25d1deb4b5 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmInitData.java @@ -17,10 +17,13 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.drm; import android.os.Parcel; import android.os.Parcelable; +import android.text.TextUtils; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -31,11 +34,61 @@ import java.util.UUID; */ public final class DrmInitData implements Comparator, Parcelable { + /** + * Merges {@link DrmInitData} obtained from a media manifest and a media stream. + * + *

The result is generated as follows. + * + *

    + *
  1. Include all {@link SchemeData}s from {@code manifestData} where {@link + * SchemeData#hasData()} is true. + *
  2. Include all {@link SchemeData}s in {@code mediaData} where {@link SchemeData#hasData()} + * is true and for which we did not include an entry from the manifest targeting the same + * UUID. + *
  3. If available, the scheme type from the manifest is used. If not, the scheme type from the + * media is used. + *
+ * + * @param manifestData DRM session acquisition data obtained from the manifest. + * @param mediaData DRM session acquisition data obtained from the media. + * @return A {@link DrmInitData} obtained from merging a media manifest and a media stream. + */ + public static @Nullable DrmInitData createSessionCreationData( + @Nullable DrmInitData manifestData, @Nullable DrmInitData mediaData) { + ArrayList result = new ArrayList<>(); + String schemeType = null; + if (manifestData != null) { + schemeType = manifestData.schemeType; + for (SchemeData data : manifestData.schemeDatas) { + if (data.hasData()) { + result.add(data); + } + } + } + + if (mediaData != null) { + if (schemeType == null) { + schemeType = mediaData.schemeType; + } + int manifestDatasCount = result.size(); + for (SchemeData data : mediaData.schemeDatas) { + if (data.hasData() && !containsSchemeDataWithUuid(result, manifestDatasCount, data.uuid)) { + result.add(data); + } + } + } + + return result.isEmpty() ? null : new DrmInitData(schemeType, result); + } + private final SchemeData[] schemeDatas; // Lazily initialized hashcode. private int hashCode; + /** The protection scheme type, or null if not applicable or unknown. */ + @Nullable public final String schemeType; + /** * Number of {@link SchemeData}s. */ @@ -45,44 +98,61 @@ public final class DrmInitData implements Comparator, Parcelable { * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. */ public DrmInitData(List schemeDatas) { - this(false, schemeDatas.toArray(new SchemeData[schemeDatas.size()])); + this(null, false, schemeDatas.toArray(new SchemeData[0])); + } + + /** + * @param schemeType See {@link #schemeType}. + * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. + */ + public DrmInitData(@Nullable String schemeType, List schemeDatas) { + this(schemeType, false, schemeDatas.toArray(new SchemeData[0])); } /** * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. */ public DrmInitData(SchemeData... schemeDatas) { - this(true, schemeDatas); + this(null, schemeDatas); } - private DrmInitData(boolean cloneSchemeDatas, SchemeData... schemeDatas) { + /** + * @param schemeType See {@link #schemeType}. + * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. + */ + public DrmInitData(@Nullable String schemeType, SchemeData... schemeDatas) { + this(schemeType, true, schemeDatas); + } + + private DrmInitData(@Nullable String schemeType, boolean cloneSchemeDatas, + SchemeData... schemeDatas) { + this.schemeType = schemeType; if (cloneSchemeDatas) { schemeDatas = schemeDatas.clone(); } - // Sorting ensures that universal scheme data(i.e. data that applies to all schemes) is matched - // last. It's also required by the equals and hashcode implementations. - Arrays.sort(schemeDatas, this); - // Check for no duplicates. - for (int i = 1; i < schemeDatas.length; i++) { - if (schemeDatas[i - 1].uuid.equals(schemeDatas[i].uuid)) { - throw new IllegalArgumentException("Duplicate data for uuid: " + schemeDatas[i].uuid); - } - } this.schemeDatas = schemeDatas; schemeDataCount = schemeDatas.length; + // Sorting ensures that universal scheme data (i.e. data that applies to all schemes) is matched + // last. It's also required by the equals and hashcode implementations. + Arrays.sort(this.schemeDatas, this); } - /* package */ DrmInitData(Parcel in) { - schemeDatas = in.createTypedArray(SchemeData.CREATOR); + /* package */ + DrmInitData(Parcel in) { + schemeType = in.readString(); + schemeDatas = Util.castNonNull(in.createTypedArray(SchemeData.CREATOR)); schemeDataCount = schemeDatas.length; } /** * Retrieves data for a given DRM scheme, specified by its UUID. * + * @deprecated Use {@link #get(int)} and {@link SchemeData#matches(UUID)} instead. * @param uuid The DRM scheme's UUID. * @return The initialization data for the scheme, or null if the scheme is not supported. */ + @Deprecated + @Nullable public SchemeData get(UUID uuid) { for (SchemeData schemeData : schemeDatas) { if (schemeData.matches(uuid)) { @@ -95,30 +165,66 @@ public final class DrmInitData implements Comparator, Parcelable { /** * Retrieves the {@link SchemeData} at a given index. * - * @param index index of the scheme to return. - * @return The {@link SchemeData} at the index. + * @param index The index of the scheme to return. Must not exceed {@link #schemeDataCount}. + * @return The {@link SchemeData} at the specified index. */ public SchemeData get(int index) { return schemeDatas[index]; } + /** + * Returns a copy with the specified protection scheme type. + * + * @param schemeType A protection scheme type. May be null. + * @return A copy with the specified protection scheme type. + */ + public DrmInitData copyWithSchemeType(@Nullable String schemeType) { + if (Util.areEqual(this.schemeType, schemeType)) { + return this; + } + return new DrmInitData(schemeType, false, schemeDatas); + } + + /** + * Returns an instance containing the {@link #schemeDatas} from both this and {@code other}. The + * {@link #schemeType} of the instances being merged must either match, or at least one scheme + * type must be {@code null}. + * + * @param drmInitData The instance to merge. + * @return The merged result. + */ + public DrmInitData merge(DrmInitData drmInitData) { + Assertions.checkState( + schemeType == null + || drmInitData.schemeType == null + || TextUtils.equals(schemeType, drmInitData.schemeType)); + String mergedSchemeType = schemeType != null ? this.schemeType : drmInitData.schemeType; + SchemeData[] mergedSchemeDatas = + Util.nullSafeArrayConcatenation(schemeDatas, drmInitData.schemeDatas); + return new DrmInitData(mergedSchemeType, mergedSchemeDatas); + } + @Override public int hashCode() { if (hashCode == 0) { - hashCode = Arrays.hashCode(schemeDatas); + int result = (schemeType == null ? 0 : schemeType.hashCode()); + result = 31 * result + Arrays.hashCode(schemeDatas); + hashCode = result; } return hashCode; } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } - return Arrays.equals(schemeDatas, ((DrmInitData) obj).schemeDatas); + DrmInitData other = (DrmInitData) obj; + return Util.areEqual(schemeType, other.schemeType) + && Arrays.equals(schemeDatas, other.schemeDatas); } @Override @@ -136,6 +242,7 @@ public final class DrmInitData implements Comparator, Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeString(schemeType); dest.writeTypedArray(schemeDatas, 0); } @@ -154,6 +261,18 @@ public final class DrmInitData implements Comparator, Parcelable { }; + // Internal methods. + + private static boolean containsSchemeDataWithUuid( + ArrayList datas, int limit, UUID uuid) { + for (int i = 0; i < limit; i++) { + if (datas.get(i).uuid.equals(uuid)) { + return true; + } + } + return false; + } + /** * Scheme initialization data. */ @@ -167,48 +286,43 @@ public final class DrmInitData implements Comparator, Parcelable { * applies to all schemes). */ private final UUID uuid; - /** - * The mimeType of {@link #data}. - */ + /** The URL of the server to which license requests should be made. May be null if unknown. */ + @Nullable public final String licenseServerUrl; + /** The mimeType of {@link #data}. */ public final String mimeType; - /** - * The initialization data. - */ - public final byte[] data; - /** - * Whether secure decryption is required. - */ - public final boolean requiresSecureDecryption; + /** The initialization data. May be null for scheme support checks only. */ + @Nullable public final byte[] data; /** * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is * universal (i.e. applies to all schemes). - * @param mimeType The mimeType of the initialization data. - * @param data The initialization data. + * @param mimeType See {@link #mimeType}. + * @param data See {@link #data}. */ - public SchemeData(UUID uuid, String mimeType, byte[] data) { - this(uuid, mimeType, data, false); + public SchemeData(UUID uuid, String mimeType, @Nullable byte[] data) { + this(uuid, /* licenseServerUrl= */ null, mimeType, data); } /** * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is * universal (i.e. applies to all schemes). - * @param mimeType The mimeType of the initialization data. - * @param data The initialization data. - * @param requiresSecureDecryption Whether secure decryption is required. + * @param licenseServerUrl See {@link #licenseServerUrl}. + * @param mimeType See {@link #mimeType}. + * @param data See {@link #data}. */ - public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) { + public SchemeData( + UUID uuid, @Nullable String licenseServerUrl, String mimeType, @Nullable byte[] data) { this.uuid = Assertions.checkNotNull(uuid); + this.licenseServerUrl = licenseServerUrl; this.mimeType = Assertions.checkNotNull(mimeType); - this.data = Assertions.checkNotNull(data); - this.requiresSecureDecryption = requiresSecureDecryption; + this.data = data; } /* package */ SchemeData(Parcel in) { uuid = new UUID(in.readLong(), in.readLong()); - mimeType = in.readString(); + licenseServerUrl = in.readString(); + mimeType = Util.castNonNull(in.readString()); data = in.createByteArray(); - requiresSecureDecryption = in.readByte() != 0; } /** @@ -221,8 +335,35 @@ public final class DrmInitData implements Comparator, Parcelable { return C.UUID_NIL.equals(uuid) || schemeUuid.equals(uuid); } + /** + * Returns whether this {@link SchemeData} can be used to replace {@code other}. + * + * @param other A {@link SchemeData}. + * @return Whether this {@link SchemeData} can be used to replace {@code other}. + */ + public boolean canReplace(SchemeData other) { + return hasData() && !other.hasData() && matches(other.uuid); + } + + /** + * Returns whether {@link #data} is non-null. + */ + public boolean hasData() { + return data != null; + } + + /** + * Returns a copy of this instance with the specified data. + * + * @param data The data to include in the copy. + * @return The new instance. + */ + public SchemeData copyWithData(@Nullable byte[] data) { + return new SchemeData(uuid, licenseServerUrl, mimeType, data); + } + @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (!(obj instanceof SchemeData)) { return false; } @@ -230,7 +371,9 @@ public final class DrmInitData implements Comparator, Parcelable { return true; } SchemeData other = (SchemeData) obj; - return mimeType.equals(other.mimeType) && Util.areEqual(uuid, other.uuid) + return Util.areEqual(licenseServerUrl, other.licenseServerUrl) + && Util.areEqual(mimeType, other.mimeType) + && Util.areEqual(uuid, other.uuid) && Arrays.equals(data, other.data); } @@ -238,6 +381,7 @@ public final class DrmInitData implements Comparator, Parcelable { public int hashCode() { if (hashCode == 0) { int result = uuid.hashCode(); + result = 31 * result + (licenseServerUrl == null ? 0 : licenseServerUrl.hashCode()); result = 31 * result + mimeType.hashCode(); result = 31 * result + Arrays.hashCode(data); hashCode = result; @@ -256,12 +400,11 @@ public final class DrmInitData implements Comparator, Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeLong(uuid.getMostSignificantBits()); dest.writeLong(uuid.getLeastSignificantBits()); + dest.writeString(licenseServerUrl); dest.writeString(mimeType); dest.writeByteArray(data); - dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0)); } - @SuppressWarnings("hiding") public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSession.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSession.java index a1e932ce18ac..7a9af2684f1c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSession.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSession.java @@ -15,9 +15,11 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.drm; -import android.annotation.TargetApi; import android.media.MediaDrm; -import android.support.annotation.IntDef; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import java.io.IOException; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Map; @@ -25,107 +27,118 @@ import java.util.Map; /** * A DRM session. */ -@TargetApi(16) public interface DrmSession { - /** Wraps the exception which is the cause of the error state. */ - class DrmSessionException extends Exception { + /** + * Invokes {@code newSession's} {@link #acquire()} and {@code previousSession's} {@link + * #release()} in that order. Null arguments are ignored. Does nothing if {@code previousSession} + * and {@code newSession} are the same session. + */ + static void replaceSession( + @Nullable DrmSession previousSession, @Nullable DrmSession newSession) { + if (previousSession == newSession) { + // Do nothing. + return; + } + if (newSession != null) { + newSession.acquire(); + } + if (previousSession != null) { + previousSession.release(); + } + } - public DrmSessionException(Exception e) { - super(e); + /** Wraps the throwable which is the cause of the error state. */ + class DrmSessionException extends IOException { + + public DrmSessionException(Throwable cause) { + super(cause); } } /** - * The state of the DRM session. + * The state of the DRM session. One of {@link #STATE_RELEASED}, {@link #STATE_ERROR}, {@link + * #STATE_OPENING}, {@link #STATE_OPENED} or {@link #STATE_OPENED_WITH_KEYS}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({STATE_ERROR, STATE_CLOSED, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS}) + @IntDef({STATE_RELEASED, STATE_ERROR, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS}) @interface State {} + /** + * The session has been released. + */ + int STATE_RELEASED = 0; /** * The session has encountered an error. {@link #getError()} can be used to retrieve the cause. */ - int STATE_ERROR = 0; - /** - * The session is closed. - */ - int STATE_CLOSED = 1; + int STATE_ERROR = 1; /** * The session is being opened. */ int STATE_OPENING = 2; - /** - * The session is open, but does not yet have the keys required for decryption. - */ + /** The session is open, but does not have keys required for decryption. */ int STATE_OPENED = 3; - /** - * The session is open and has the keys required for decryption. - */ + /** The session is open and has keys required for decryption. */ int STATE_OPENED_WITH_KEYS = 4; /** - * Returns the current state of the session. - * - * @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING}, - * {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}. + * Returns the current state of the session, which is one of {@link #STATE_ERROR}, + * {@link #STATE_RELEASED}, {@link #STATE_OPENING}, {@link #STATE_OPENED} and + * {@link #STATE_OPENED_WITH_KEYS}. */ @State int getState(); - /** - * Returns a {@link ExoMediaCrypto} for the open session. - *

- * This method may be called when the session is in the following states: - * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS} - * - * @return A {@link ExoMediaCrypto} for the open session. - * @throws IllegalStateException If called when a session isn't opened. - */ - T getMediaCrypto(); + /** Returns whether this session allows playback of clear samples prior to keys being loaded. */ + default boolean playClearSamplesWithoutKeys() { + return false; + } /** - * Whether the session requires a secure decoder for the specified mime type. - *

- * Normally this method should return - * {@link ExoMediaCrypto#requiresSecureDecoderComponent(String)}, however in some cases - * implementations may wish to modify the return value (i.e. to force a secure decoder even when - * one is not required). - *

- * This method may be called when the session is in the following states: - * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS} - * - * @return Whether the open session requires a secure decoder for the specified mime type. - * @throws IllegalStateException If called when a session isn't opened. - */ - boolean requiresSecureDecoderComponent(String mimeType); - - /** - * Returns the cause of the error state. - *

- * This method may be called when the session is in any state. - * - * @return An exception if the state is {@link #STATE_ERROR}. Null otherwise. + * Returns the cause of the error state, or null if {@link #getState()} is not {@link + * #STATE_ERROR}. */ + @Nullable DrmSessionException getError(); /** - * Returns an informative description of the key status for the session. The status is in the form - * of {name, value} pairs. + * Returns a {@link ExoMediaCrypto} for the open session, or null if called before the session has + * been opened or after it's been released. + */ + @Nullable + T getMediaCrypto(); + + /** + * Returns a map describing the key status for the session, or null if called before the session + * has been opened or after it's been released. * *

Since DRM license policies vary by vendor, the specific status field names are determined by * each DRM vendor. Refer to your DRM provider documentation for definitions of the field names * for a particular DRM engine plugin. * - * @return A map of key status. - * @throws IllegalStateException If called when the session isn't opened. + * @return A map describing the key status for the session, or null if called before the session + * has been opened or after it's been released. * @see MediaDrm#queryKeyStatus(byte[]) */ + @Nullable Map queryKeyStatus(); /** - * Returns the key set id of the offline license loaded into this session, if there is one. Null - * otherwise. + * Returns the key set id of the offline license loaded into this session, or null if there isn't + * one. */ + @Nullable byte[] getOfflineLicenseKeySetId(); + /** + * Increments the reference count. When the caller no longer needs to use the instance, it must + * call {@link #release()} to decrement the reference count. + */ + void acquire(); + + /** + * Decrements the reference count. If the reference count drops to 0 underlying resources are + * released, and the instance cannot be re-used. + */ + void release(); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSessionManager.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSessionManager.java index 0409cdfd8b6c..bf98a0a6584b 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSessionManager.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSessionManager.java @@ -15,28 +15,107 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.drm; -import android.annotation.TargetApi; import android.os.Looper; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData; /** * Manages a DRM session. */ -@TargetApi(16) public interface DrmSessionManager { + /** Returns {@link #DUMMY}. */ + @SuppressWarnings("unchecked") + static DrmSessionManager getDummyDrmSessionManager() { + return (DrmSessionManager) DUMMY; + } + + /** {@link DrmSessionManager} that supports no DRM schemes. */ + DrmSessionManager DUMMY = + new DrmSessionManager() { + + @Override + public boolean canAcquireSession(DrmInitData drmInitData) { + return false; + } + + @Override + public DrmSession acquireSession( + Looper playbackLooper, DrmInitData drmInitData) { + return new ErrorStateDrmSession<>( + new DrmSession.DrmSessionException( + new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME))); + } + + @Override + @Nullable + public Class getExoMediaCryptoType(DrmInitData drmInitData) { + return null; + } + }; + /** - * Acquires a {@link DrmSession} for the specified {@link DrmInitData}. The {@link DrmSession} - * must be returned to {@link #releaseSession(DrmSession)} when it is no longer required. + * Acquires any required resources. + * + *

{@link #release()} must be called to ensure the acquired resources are released. After + * releasing, an instance may be re-prepared. + */ + default void prepare() { + // Do nothing. + } + + /** Releases any acquired resources. */ + default void release() { + // Do nothing. + } + + /** + * Returns whether the manager is capable of acquiring a session for the given + * {@link DrmInitData}. + * + * @param drmInitData DRM initialization data. + * @return Whether the manager is capable of acquiring a session for the given + * {@link DrmInitData}. + */ + boolean canAcquireSession(DrmInitData drmInitData); + + /** + * Returns a {@link DrmSession} that does not execute key requests, with an incremented reference + * count. When the caller no longer needs to use the instance, it must call {@link + * DrmSession#release()} to decrement the reference count. + * + *

Placeholder {@link DrmSession DrmSessions} may be used to configure secure decoders for + * playback of clear content periods. This can reduce the cost of transitioning between clear and + * encrypted content periods. * * @param playbackLooper The looper associated with the media playback thread. - * @param drmInitData DRM initialization data. + * @param trackType The type of the track to acquire a placeholder session for. Must be one of the + * {@link C}{@code .TRACK_TYPE_*} constants. + * @return The placeholder DRM session, or null if this DRM session manager does not support + * placeholder sessions. + */ + @Nullable + default DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) { + return null; + } + + /** + * Returns a {@link DrmSession} for the specified {@link DrmInitData}, with an incremented + * reference count. When the caller no longer needs to use the instance, it must call {@link + * DrmSession#release()} to decrement the reference count. + * + * @param playbackLooper The looper associated with the media playback thread. + * @param drmInitData DRM initialization data. All contained {@link SchemeData}s must contain + * non-null {@link SchemeData#data}. * @return The DRM session. */ DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData); /** - * Releases a {@link DrmSession}. + * Returns the {@link ExoMediaCrypto} type returned by sessions acquired using the given {@link + * DrmInitData}, or null if a session cannot be acquired with the given {@link DrmInitData}. */ - void releaseSession(DrmSession drmSession); - + @Nullable + Class getExoMediaCryptoType(DrmInitData drmInitData); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java new file mode 100644 index 000000000000..b6a66ceac09f --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java @@ -0,0 +1,146 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.drm; + +import android.media.MediaDrmException; +import android.os.PersistableBundle; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** An {@link ExoMediaDrm} that does not support any protection schemes. */ +@RequiresApi(18) +public final class DummyExoMediaDrm implements ExoMediaDrm { + + /** Returns a new instance. */ + @SuppressWarnings("unchecked") + public static DummyExoMediaDrm getInstance() { + return (DummyExoMediaDrm) new DummyExoMediaDrm<>(); + } + + @Override + public void setOnEventListener(OnEventListener listener) { + // Do nothing. + } + + @Override + public void setOnKeyStatusChangeListener(OnKeyStatusChangeListener listener) { + // Do nothing. + } + + @Override + public byte[] openSession() throws MediaDrmException { + throw new MediaDrmException("Attempting to open a session using a dummy ExoMediaDrm."); + } + + @Override + public void closeSession(byte[] sessionId) { + // Do nothing. + } + + @Override + public KeyRequest getKeyRequest( + byte[] scope, + @Nullable List schemeDatas, + int keyType, + @Nullable HashMap optionalParameters) { + // Should not be invoked. No session should exist. + throw new IllegalStateException(); + } + + @Nullable + @Override + public byte[] provideKeyResponse(byte[] scope, byte[] response) { + // Should not be invoked. No session should exist. + throw new IllegalStateException(); + } + + @Override + public ProvisionRequest getProvisionRequest() { + // Should not be invoked. No provision should be required. + throw new IllegalStateException(); + } + + @Override + public void provideProvisionResponse(byte[] response) { + // Should not be invoked. No provision should be required. + throw new IllegalStateException(); + } + + @Override + public Map queryKeyStatus(byte[] sessionId) { + // Should not be invoked. No session should exist. + throw new IllegalStateException(); + } + + @Override + public void acquire() { + // Do nothing. + } + + @Override + public void release() { + // Do nothing. + } + + @Override + public void restoreKeys(byte[] sessionId, byte[] keySetId) { + // Should not be invoked. No session should exist. + throw new IllegalStateException(); + } + + @Override + @Nullable + public PersistableBundle getMetrics() { + return null; + } + + @Override + public String getPropertyString(String propertyName) { + return ""; + } + + @Override + public byte[] getPropertyByteArray(String propertyName) { + return Util.EMPTY_BYTE_ARRAY; + } + + @Override + public void setPropertyString(String propertyName, String value) { + // Do nothing. + } + + @Override + public void setPropertyByteArray(String propertyName, byte[] value) { + // Do nothing. + } + + @Override + public T createMediaCrypto(byte[] sessionId) { + // Should not be invoked. No session should exist. + throw new IllegalStateException(); + } + + @Override + @Nullable + public Class getExoMediaCryptoType() { + // No ExoMediaCrypto type is supported. + return null; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java new file mode 100644 index 000000000000..97d0ecaaa437 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.drm; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.util.Map; + +/** A {@link DrmSession} that's in a terminal error state. */ +public final class ErrorStateDrmSession implements DrmSession { + + private final DrmSessionException error; + + public ErrorStateDrmSession(DrmSessionException error) { + this.error = Assertions.checkNotNull(error); + } + + @Override + public int getState() { + return STATE_ERROR; + } + + @Override + public boolean playClearSamplesWithoutKeys() { + return false; + } + + @Override + @Nullable + public DrmSessionException getError() { + return error; + } + + @Override + @Nullable + public T getMediaCrypto() { + return null; + } + + @Override + @Nullable + public Map queryKeyStatus() { + return null; + } + + @Override + @Nullable + public byte[] getOfflineLicenseKeySetId() { + return null; + } + + @Override + public void acquire() { + // Do nothing. + } + + @Override + public void release() { + // Do nothing. + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaCrypto.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaCrypto.java index e676753efe64..a12b212799d5 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaCrypto.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaCrypto.java @@ -15,14 +15,5 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.drm; -/** - * An opaque {@link android.media.MediaCrypto} equivalent. - */ -public interface ExoMediaCrypto { - - /** - * @see android.media.MediaCrypto#requiresSecureDecoderComponent(String) - */ - boolean requiresSecureDecoderComponent(String mimeType); - -} +/** An opaque {@link android.media.MediaCrypto} equivalent. */ +public interface ExoMediaCrypto {} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaDrm.java index eb2932b9450b..1e851a7c0b55 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaDrm.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaDrm.java @@ -18,17 +18,96 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.drm; import android.media.DeniedByServerException; import android.media.MediaCryptoException; import android.media.MediaDrm; +import android.media.MediaDrmException; import android.media.NotProvisionedException; -import android.media.ResourceBusyException; +import android.os.Handler; +import android.os.PersistableBundle; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; /** * Used to obtain keys for decrypting protected media streams. See {@link android.media.MediaDrm}. + * + *

Reference counting

+ * + *

Access to an instance is managed by reference counting, where {@link #acquire()} increments + * the reference count and {@link #release()} decrements it. When the reference count drops to 0 + * underlying resources are released, and the instance cannot be re-used. + * + *

Each new instance has an initial reference count of 1. Hence application code that creates a + * new instance does not normally need to call {@link #acquire()}, and must call {@link #release()} + * when the instance is no longer required. */ public interface ExoMediaDrm { + /** {@link ExoMediaDrm} instances provider. */ + interface Provider { + + /** + * Returns an {@link ExoMediaDrm} instance with an incremented reference count. When the caller + * no longer needs to use the instance, it must call {@link ExoMediaDrm#release()} to decrement + * the reference count. + */ + ExoMediaDrm acquireExoMediaDrm(UUID uuid); + } + + /** + * Provides an {@link ExoMediaDrm} instance owned by the app. + * + *

Note that when using this provider the app will have instantiated the {@link ExoMediaDrm} + * instance, and remains responsible for calling {@link ExoMediaDrm#release()} on the instance + * when it's no longer being used. + */ + final class AppManagedProvider implements Provider { + + private final ExoMediaDrm exoMediaDrm; + + /** Creates an instance that provides the given {@link ExoMediaDrm}. */ + public AppManagedProvider(ExoMediaDrm exoMediaDrm) { + this.exoMediaDrm = exoMediaDrm; + } + + @Override + public ExoMediaDrm acquireExoMediaDrm(UUID uuid) { + exoMediaDrm.acquire(); + return exoMediaDrm; + } + } + + /** @see MediaDrm#EVENT_KEY_REQUIRED */ + @SuppressWarnings("InlinedApi") + int EVENT_KEY_REQUIRED = MediaDrm.EVENT_KEY_REQUIRED; + /** + * @see MediaDrm#EVENT_KEY_EXPIRED + */ + @SuppressWarnings("InlinedApi") + int EVENT_KEY_EXPIRED = MediaDrm.EVENT_KEY_EXPIRED; + /** + * @see MediaDrm#EVENT_PROVISION_REQUIRED + */ + @SuppressWarnings("InlinedApi") + int EVENT_PROVISION_REQUIRED = MediaDrm.EVENT_PROVISION_REQUIRED; + + /** + * @see MediaDrm#KEY_TYPE_STREAMING + */ + @SuppressWarnings("InlinedApi") + int KEY_TYPE_STREAMING = MediaDrm.KEY_TYPE_STREAMING; + /** + * @see MediaDrm#KEY_TYPE_OFFLINE + */ + @SuppressWarnings("InlinedApi") + int KEY_TYPE_OFFLINE = MediaDrm.KEY_TYPE_OFFLINE; + /** + * @see MediaDrm#KEY_TYPE_RELEASE + */ + @SuppressWarnings("InlinedApi") + int KEY_TYPE_RELEASE = MediaDrm.KEY_TYPE_RELEASE; + /** * @see android.media.MediaDrm.OnEventListener */ @@ -36,30 +115,101 @@ public interface ExoMediaDrm { /** * Called when an event occurs that requires the app to be notified * - * @param mediaDrm the {@link ExoMediaDrm} object on which the event occurred. - * @param sessionId the DRM session ID on which the event occurred - * @param event indicates the event type - * @param extra an secondary error code - * @param data optional byte array of data that may be associated with the event + * @param mediaDrm The {@link ExoMediaDrm} object on which the event occurred. + * @param sessionId The DRM session ID on which the event occurred. + * @param event Indicates the event type. + * @param extra A secondary error code. + * @param data Optional byte array of data that may be associated with the event. */ - void onEvent(ExoMediaDrm mediaDrm, byte[] sessionId, int event, int extra, - byte[] data); + void onEvent( + ExoMediaDrm mediaDrm, + @Nullable byte[] sessionId, + int event, + int extra, + @Nullable byte[] data); } /** - * @see android.media.MediaDrm.KeyRequest + * @see android.media.MediaDrm.OnKeyStatusChangeListener */ - interface KeyRequest { - byte[] getData(); - String getDefaultUrl(); + interface OnKeyStatusChangeListener { + /** + * Called when the keys in a session change status, such as when the license is renewed or + * expires. + * + * @param mediaDrm The {@link ExoMediaDrm} object on which the event occurred. + * @param sessionId The DRM session ID on which the event occurred. + * @param exoKeyInformation A list of {@link KeyStatus} that contains key ID and status. + * @param hasNewUsableKey Whether a new key became usable. + */ + void onKeyStatusChange( + ExoMediaDrm mediaDrm, + byte[] sessionId, + List exoKeyInformation, + boolean hasNewUsableKey); } - /** - * @see android.media.MediaDrm.ProvisionRequest - */ - interface ProvisionRequest { - byte[] getData(); - String getDefaultUrl(); + /** @see android.media.MediaDrm.KeyStatus */ + final class KeyStatus { + + private final int statusCode; + private final byte[] keyId; + + public KeyStatus(int statusCode, byte[] keyId) { + this.statusCode = statusCode; + this.keyId = keyId; + } + + public int getStatusCode() { + return statusCode; + } + + public byte[] getKeyId() { + return keyId; + } + + } + + /** @see android.media.MediaDrm.KeyRequest */ + final class KeyRequest { + + private final byte[] data; + private final String licenseServerUrl; + + public KeyRequest(byte[] data, String licenseServerUrl) { + this.data = data; + this.licenseServerUrl = licenseServerUrl; + } + + public byte[] getData() { + return data; + } + + public String getLicenseServerUrl() { + return licenseServerUrl; + } + + } + + /** @see android.media.MediaDrm.ProvisionRequest */ + final class ProvisionRequest { + + private final byte[] data; + private final String defaultUrl; + + public ProvisionRequest(byte[] data, String defaultUrl) { + this.data = data; + this.defaultUrl = defaultUrl; + } + + public byte[] getData() { + return data; + } + + public String getDefaultUrl() { + return defaultUrl; + } + } /** @@ -67,10 +217,15 @@ public interface ExoMediaDrm { */ void setOnEventListener(OnEventListener listener); + /** + * @see MediaDrm#setOnKeyStatusChangeListener(MediaDrm.OnKeyStatusChangeListener, Handler) + */ + void setOnKeyStatusChangeListener(OnKeyStatusChangeListener listener); + /** * @see MediaDrm#openSession() */ - byte[] openSession() throws NotProvisionedException, ResourceBusyException; + byte[] openSession() throws MediaDrmException; /** * @see MediaDrm#closeSession(byte[]) @@ -78,14 +233,32 @@ public interface ExoMediaDrm { void closeSession(byte[] sessionId); /** + * Generates a key request. + * + * @param scope If {@code keyType} is {@link #KEY_TYPE_STREAMING} or {@link #KEY_TYPE_OFFLINE}, + * the session id that the keys will be provided to. If {@code keyType} is {@link + * #KEY_TYPE_RELEASE}, the keySetId of the keys to release. + * @param schemeDatas If key type is {@link #KEY_TYPE_STREAMING} or {@link #KEY_TYPE_OFFLINE}, a + * list of {@link SchemeData} instances extracted from the media. Null otherwise. + * @param keyType The type of the request. Either {@link #KEY_TYPE_STREAMING} to acquire keys for + * streaming, {@link #KEY_TYPE_OFFLINE} to acquire keys for offline usage, or {@link + * #KEY_TYPE_RELEASE} to release acquired keys. Releasing keys invalidates them for all + * sessions. + * @param optionalParameters Are included in the key request message to allow a client application + * to provide additional message parameters to the server. This may be {@code null} if no + * additional parameters are to be sent. + * @return The generated key request. * @see MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap) */ - KeyRequest getKeyRequest(byte[] scope, byte[] init, String mimeType, int keyType, - HashMap optionalParameters) throws NotProvisionedException; + KeyRequest getKeyRequest( + byte[] scope, + @Nullable List schemeDatas, + int keyType, + @Nullable HashMap optionalParameters) + throws NotProvisionedException; - /** - * @see MediaDrm#provideKeyResponse(byte[], byte[]) - */ + /** @see MediaDrm#provideKeyResponse(byte[], byte[]) */ + @Nullable byte[] provideKeyResponse(byte[] scope, byte[] response) throws NotProvisionedException, DeniedByServerException; @@ -105,7 +278,17 @@ public interface ExoMediaDrm { Map queryKeyStatus(byte[] sessionId); /** - * @see MediaDrm#release() + * Increments the reference count. When the caller no longer needs to use the instance, it must + * call {@link #release()} to decrement the reference count. + * + *

A new instance will have an initial reference count of 1, and therefore it is not normally + * necessary for application code to call this method. + */ + void acquire(); + + /** + * Decrements the reference count. If the reference count drops to 0 underlying resources are + * released, and the instance cannot be re-used. */ void release(); @@ -114,6 +297,14 @@ public interface ExoMediaDrm { */ void restoreKeys(byte[] sessionId, byte[] keySetId); + /** + * Returns drm metrics. May be null if unavailable. + * + * @see MediaDrm#getMetrics() + */ + @Nullable + PersistableBundle getMetrics(); + /** * @see MediaDrm#getPropertyString(String) */ @@ -136,12 +327,16 @@ public interface ExoMediaDrm { /** * @see android.media.MediaCrypto#MediaCrypto(UUID, byte[]) - * - * @param uuid The UUID of the crypto scheme. - * @param initData Opaque initialization data specific to the crypto scheme. + * @param sessionId The DRM session ID. * @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data. - * @throws MediaCryptoException + * @throws MediaCryptoException If the instance can't be created. */ - T createMediaCrypto(UUID uuid, byte[] initData) throws MediaCryptoException; + T createMediaCrypto(byte[] sessionId) throws MediaCryptoException; + /** + * Returns the {@link ExoMediaCrypto} type created by {@link #createMediaCrypto(byte[])}, or null + * if this instance cannot create any {@link ExoMediaCrypto} instances. + */ + @Nullable + Class getExoMediaCryptoType(); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java index ae17975a358d..bb3a9b272b10 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java @@ -15,29 +15,45 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.drm; -import android.annotation.TargetApi; import android.media.MediaCrypto; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.UUID; /** - * An {@link ExoMediaCrypto} implementation that wraps the framework {@link MediaCrypto}. + * An {@link ExoMediaCrypto} implementation that contains the necessary information to build or + * update a framework {@link MediaCrypto}. */ -@TargetApi(16) public final class FrameworkMediaCrypto implements ExoMediaCrypto { - private final MediaCrypto mediaCrypto; + /** + * Whether the device needs keys to have been loaded into the {@link DrmSession} before codec + * configuration. + */ + public static final boolean WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC = + "Amazon".equals(Util.MANUFACTURER) + && ("AFTM".equals(Util.MODEL) // Fire TV Stick Gen 1 + || "AFTB".equals(Util.MODEL)); // Fire TV Gen 1 - /* package */ FrameworkMediaCrypto(MediaCrypto mediaCrypto) { - this.mediaCrypto = Assertions.checkNotNull(mediaCrypto); + /** The DRM scheme UUID. */ + public final UUID uuid; + /** The DRM session id. */ + public final byte[] sessionId; + /** + * Whether to allow use of insecure decoder components even if the underlying platform says + * otherwise. + */ + public final boolean forceAllowInsecureDecoderComponents; + + /** + * @param uuid The DRM scheme UUID. + * @param sessionId The DRM session id. + * @param forceAllowInsecureDecoderComponents Whether to allow use of insecure decoder components + * even if the underlying platform says otherwise. + */ + public FrameworkMediaCrypto( + UUID uuid, byte[] sessionId, boolean forceAllowInsecureDecoderComponents) { + this.uuid = uuid; + this.sessionId = sessionId; + this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents; } - - public MediaCrypto getWrappedMediaCrypto() { - return mediaCrypto; - } - - @Override - public boolean requiresSecureDecoderComponent(String mimeType) { - return mediaCrypto.requiresSecureDecoderComponent(mimeType); - } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index cce2e0f57513..10ca857448d0 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -15,30 +15,69 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.drm; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.media.DeniedByServerException; -import android.media.MediaCrypto; import android.media.MediaCryptoException; import android.media.MediaDrm; +import android.media.MediaDrmException; import android.media.NotProvisionedException; -import android.media.ResourceBusyException; import android.media.UnsupportedSchemeException; -import android.support.annotation.NonNull; +import android.os.PersistableBundle; +import android.text.TextUtils; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; -/** - * An {@link ExoMediaDrm} implementation that wraps the framework {@link MediaDrm}. - */ -@TargetApi(18) +/** An {@link ExoMediaDrm} implementation that wraps the framework {@link MediaDrm}. */ +@TargetApi(23) +@RequiresApi(18) public final class FrameworkMediaDrm implements ExoMediaDrm { - private final MediaDrm mediaDrm; + private static final String TAG = "FrameworkMediaDrm"; /** - * Creates an instance for the specified scheme UUID. + * {@link ExoMediaDrm.Provider} that returns a new {@link FrameworkMediaDrm} for the requested + * UUID. Returns a {@link DummyExoMediaDrm} if the protection scheme identified by the given UUID + * is not supported by the device. + */ + public static final Provider DEFAULT_PROVIDER = + uuid -> { + try { + return newInstance(uuid); + } catch (UnsupportedDrmException e) { + Log.e(TAG, "Failed to instantiate a FrameworkMediaDrm for uuid: " + uuid + "."); + return new DummyExoMediaDrm<>(); + } + }; + + private static final String CENC_SCHEME_MIME_TYPE = "cenc"; + private static final String MOCK_LA_URL_VALUE = "https://x"; + private static final String MOCK_LA_URL = "" + MOCK_LA_URL_VALUE + ""; + private static final int UTF_16_BYTES_PER_CHARACTER = 2; + + private final UUID uuid; + private final MediaDrm mediaDrm; + private int referenceCount; + + /** + * Creates an instance with an initial reference count of 1. {@link #release()} must be called on + * the instance when it's no longer required. * * @param uuid The scheme uuid. * @return The created instance. @@ -55,23 +94,50 @@ public final class FrameworkMediaDrm implements ExoMediaDrm listener) { - mediaDrm.setOnEventListener(listener == null ? null : new MediaDrm.OnEventListener() { - @Override - public void onEvent(@NonNull MediaDrm md, byte[] sessionId, int event, int extra, - byte[] data) { - listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data); - } - }); + mediaDrm.setOnEventListener( + listener == null + ? null + : (mediaDrm, sessionId, event, extra, data) -> + listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data)); } @Override - public byte[] openSession() throws NotProvisionedException, ResourceBusyException { + public void setOnKeyStatusChangeListener( + final ExoMediaDrm.OnKeyStatusChangeListener listener) { + if (Util.SDK_INT < 23) { + throw new UnsupportedOperationException(); + } + + mediaDrm.setOnKeyStatusChangeListener( + listener == null + ? null + : (mediaDrm, sessionId, keyInfo, hasNewUsableKey) -> { + List exoKeyInfo = new ArrayList<>(); + for (MediaDrm.KeyStatus keyStatus : keyInfo) { + exoKeyInfo.add(new KeyStatus(keyStatus.getStatusCode(), keyStatus.getKeyId())); + } + listener.onKeyStatusChange( + FrameworkMediaDrm.this, sessionId, exoKeyInfo, hasNewUsableKey); + }, + null); + } + + @Override + public byte[] openSession() throws MediaDrmException { return mediaDrm.openSession(); } @@ -81,43 +147,53 @@ public final class FrameworkMediaDrm implements ExoMediaDrm optionalParameters) throws NotProvisionedException { - final MediaDrm.KeyRequest request = mediaDrm.getKeyRequest(scope, init, mimeType, keyType, - optionalParameters); - return new KeyRequest() { - @Override - public byte[] getData() { - return request.getData(); - } + public KeyRequest getKeyRequest( + byte[] scope, + @Nullable List schemeDatas, + int keyType, + @Nullable HashMap optionalParameters) + throws NotProvisionedException { + SchemeData schemeData = null; + byte[] initData = null; + String mimeType = null; + if (schemeDatas != null) { + schemeData = getSchemeData(uuid, schemeDatas); + initData = adjustRequestInitData(uuid, Assertions.checkNotNull(schemeData.data)); + mimeType = adjustRequestMimeType(uuid, schemeData.mimeType); + } + MediaDrm.KeyRequest request = + mediaDrm.getKeyRequest(scope, initData, mimeType, keyType, optionalParameters); - @Override - public String getDefaultUrl() { - return request.getDefaultUrl(); - } - }; + byte[] requestData = adjustRequestData(uuid, request.getData()); + + String licenseServerUrl = request.getDefaultUrl(); + if (MOCK_LA_URL_VALUE.equals(licenseServerUrl)) { + licenseServerUrl = ""; + } + if (TextUtils.isEmpty(licenseServerUrl) + && schemeData != null + && !TextUtils.isEmpty(schemeData.licenseServerUrl)) { + licenseServerUrl = schemeData.licenseServerUrl; + } + + return new KeyRequest(requestData, licenseServerUrl); } + @Nullable @Override public byte[] provideKeyResponse(byte[] scope, byte[] response) throws NotProvisionedException, DeniedByServerException { + if (C.CLEARKEY_UUID.equals(uuid)) { + response = ClearKeyUtil.adjustResponseData(response); + } + return mediaDrm.provideKeyResponse(scope, response); } @Override public ProvisionRequest getProvisionRequest() { - final MediaDrm.ProvisionRequest provisionRequest = mediaDrm.getProvisionRequest(); - return new ProvisionRequest() { - @Override - public byte[] getData() { - return provisionRequest.getData(); - } - - @Override - public String getDefaultUrl() { - return provisionRequest.getDefaultUrl(); - } - }; + final MediaDrm.ProvisionRequest request = mediaDrm.getProvisionRequest(); + return new ProvisionRequest(request.getData(), request.getDefaultUrl()); } @Override @@ -131,8 +207,16 @@ public final class FrameworkMediaDrm implements ExoMediaDrm 0); + referenceCount++; + } + + @Override + public synchronized void release() { + if (--referenceCount == 0) { + mediaDrm.release(); + } } @Override @@ -140,6 +224,16 @@ public final class FrameworkMediaDrm implements ExoMediaDrm getExoMediaCryptoType() { + return FrameworkMediaCrypto.class; + } + + private static SchemeData getSchemeData(UUID uuid, List schemeDatas) { + if (!C.WIDEVINE_UUID.equals(uuid)) { + // For non-Widevine CDMs always use the first scheme data. + return schemeDatas.get(0); + } + + if (Util.SDK_INT >= 28 && schemeDatas.size() > 1) { + // For API level 28 and above, concatenate multiple PSSH scheme datas if possible. + SchemeData firstSchemeData = schemeDatas.get(0); + int concatenatedDataLength = 0; + boolean canConcatenateData = true; + for (int i = 0; i < schemeDatas.size(); i++) { + SchemeData schemeData = schemeDatas.get(i); + byte[] schemeDataData = Util.castNonNull(schemeData.data); + if (Util.areEqual(schemeData.mimeType, firstSchemeData.mimeType) + && Util.areEqual(schemeData.licenseServerUrl, firstSchemeData.licenseServerUrl) + && PsshAtomUtil.isPsshAtom(schemeDataData)) { + concatenatedDataLength += schemeDataData.length; + } else { + canConcatenateData = false; + break; + } + } + if (canConcatenateData) { + byte[] concatenatedData = new byte[concatenatedDataLength]; + int concatenatedDataPosition = 0; + for (int i = 0; i < schemeDatas.size(); i++) { + SchemeData schemeData = schemeDatas.get(i); + byte[] schemeDataData = Util.castNonNull(schemeData.data); + int schemeDataLength = schemeDataData.length; + System.arraycopy( + schemeDataData, 0, concatenatedData, concatenatedDataPosition, schemeDataLength); + concatenatedDataPosition += schemeDataLength; + } + return firstSchemeData.copyWithData(concatenatedData); + } + } + + // For API levels 23 - 27, prefer the first V1 PSSH box. For API levels 22 and earlier, prefer + // the first V0 box. + for (int i = 0; i < schemeDatas.size(); i++) { + SchemeData schemeData = schemeDatas.get(i); + int version = PsshAtomUtil.parseVersion(Util.castNonNull(schemeData.data)); + if (Util.SDK_INT < 23 && version == 0) { + return schemeData; + } else if (Util.SDK_INT >= 23 && version == 1) { + return schemeData; + } + } + + // If all else fails, use the first scheme data. + return schemeDatas.get(0); + } + + private static UUID adjustUuid(UUID uuid) { + // ClearKey had to be accessed using the Common PSSH UUID prior to API level 27. + return Util.SDK_INT < 27 && C.CLEARKEY_UUID.equals(uuid) ? C.COMMON_PSSH_UUID : uuid; + } + + private static byte[] adjustRequestInitData(UUID uuid, byte[] initData) { + // TODO: Add API level check once [Internal ref: b/112142048] is fixed. + if (C.PLAYREADY_UUID.equals(uuid)) { + byte[] schemeSpecificData = PsshAtomUtil.parseSchemeSpecificData(initData, uuid); + if (schemeSpecificData == null) { + // The init data is not contained in a pssh box. + schemeSpecificData = initData; + } + initData = + PsshAtomUtil.buildPsshAtom( + C.PLAYREADY_UUID, addLaUrlAttributeIfMissing(schemeSpecificData)); + } + + // Prior to API level 21, the Widevine CDM required scheme specific data to be extracted from + // the PSSH atom. We also extract the data on API levels 21 and 22 because these API levels + // don't handle V1 PSSH atoms, but do handle scheme specific data regardless of whether it's + // extracted from a V0 or a V1 PSSH atom. Hence extracting the data allows us to support content + // that only provides V1 PSSH atoms. API levels 23 and above understand V0 and V1 PSSH atoms, + // and so we do not extract the data. + // Some Amazon devices also require data to be extracted from the PSSH atom for PlayReady. + if ((Util.SDK_INT < 23 && C.WIDEVINE_UUID.equals(uuid)) + || (C.PLAYREADY_UUID.equals(uuid) + && "Amazon".equals(Util.MANUFACTURER) + && ("AFTB".equals(Util.MODEL) // Fire TV Gen 1 + || "AFTS".equals(Util.MODEL) // Fire TV Gen 2 + || "AFTM".equals(Util.MODEL) // Fire TV Stick Gen 1 + || "AFTT".equals(Util.MODEL)))) { // Fire TV Stick Gen 2 + byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(initData, uuid); + if (psshData != null) { + // Extraction succeeded, so return the extracted data. + return psshData; + } + } + return initData; + } + + private static String adjustRequestMimeType(UUID uuid, String mimeType) { + // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4. + if (Util.SDK_INT < 26 + && C.CLEARKEY_UUID.equals(uuid) + && (MimeTypes.VIDEO_MP4.equals(mimeType) || MimeTypes.AUDIO_MP4.equals(mimeType))) { + return CENC_SCHEME_MIME_TYPE; + } + return mimeType; + } + + private static byte[] adjustRequestData(UUID uuid, byte[] requestData) { + if (C.CLEARKEY_UUID.equals(uuid)) { + return ClearKeyUtil.adjustRequestData(requestData); + } + return requestData; + } + + @SuppressLint("WrongConstant") // Suppress spurious lint error [Internal ref: b/32137960] + private static void forceWidevineL3(MediaDrm mediaDrm) { + mediaDrm.setPropertyString("securityLevel", "L3"); + } + + /** + * Returns whether the device codec is known to fail if security level L1 is used. + * + *

See GitHub issue #4413. + */ + private static boolean needsForceWidevineL3Workaround() { + return "ASUS_Z00AD".equals(Util.MODEL); + } + + /** + * If the LA_URL tag is missing, injects a mock LA_URL value to avoid causing the CDM to throw + * when creating the key request. The LA_URL attribute is optional but some Android PlayReady + * implementations are known to require it. Does nothing it the provided {@code data} already + * contains an LA_URL value. + */ + private static byte[] addLaUrlAttributeIfMissing(byte[] data) { + ParsableByteArray byteArray = new ParsableByteArray(data); + // See https://docs.microsoft.com/en-us/playready/specifications/specifications for more + // information about the init data format. + int length = byteArray.readLittleEndianInt(); + int objectRecordCount = byteArray.readLittleEndianShort(); + int recordType = byteArray.readLittleEndianShort(); + if (objectRecordCount != 1 || recordType != 1) { + Log.i(TAG, "Unexpected record count or type. Skipping LA_URL workaround."); + return data; + } + int recordLength = byteArray.readLittleEndianShort(); + String xml = byteArray.readString(recordLength, Charset.forName(C.UTF16LE_NAME)); + if (xml.contains("")) { + // LA_URL already present. Do nothing. + return data; + } + // This PlayReady object record does not include an LA_URL. We add a mock value for it. + int endOfDataTagIndex = xml.indexOf(""); + if (endOfDataTagIndex == -1) { + Log.w(TAG, "Could not find the tag. Skipping LA_URL workaround."); + } + String xmlWithMockLaUrl = + xml.substring(/* beginIndex= */ 0, /* endIndex= */ endOfDataTagIndex) + + MOCK_LA_URL + + xml.substring(/* beginIndex= */ endOfDataTagIndex); + int extraBytes = MOCK_LA_URL.length() * UTF_16_BYTES_PER_CHARACTER; + ByteBuffer newData = ByteBuffer.allocate(length + extraBytes); + newData.order(ByteOrder.LITTLE_ENDIAN); + newData.putInt(length + extraBytes); + newData.putShort((short) objectRecordCount); + newData.putShort((short) recordType); + newData.putShort((short) (xmlWithMockLaUrl.length() * UTF_16_BYTES_PER_CHARACTER)); + newData.put(xmlWithMockLaUrl.getBytes(Charset.forName(C.UTF16LE_NAME))); + return newData.array(); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index 4b83c3d7a60f..baa5bf0916b2 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -18,17 +18,19 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.drm; import android.annotation.TargetApi; import android.net.Uri; import android.text.TextUtils; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSourceInputStream; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource.Factory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -38,42 +40,36 @@ import java.util.UUID; @TargetApi(18) public final class HttpMediaDrmCallback implements MediaDrmCallback { - private static final Map PLAYREADY_KEY_REQUEST_PROPERTIES; - static { - PLAYREADY_KEY_REQUEST_PROPERTIES = new HashMap<>(); - PLAYREADY_KEY_REQUEST_PROPERTIES.put("Content-Type", "text/xml"); - PLAYREADY_KEY_REQUEST_PROPERTIES.put("SOAPAction", - "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"); - } + private static final int MAX_MANUAL_REDIRECTS = 5; private final HttpDataSource.Factory dataSourceFactory; - private final String defaultUrl; + private final String defaultLicenseUrl; + private final boolean forceDefaultLicenseUrl; private final Map keyRequestProperties; /** - * @param defaultUrl The default license URL. + * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. */ - public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory) { - this(defaultUrl, dataSourceFactory, null); + public HttpMediaDrmCallback(String defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) { + this(defaultLicenseUrl, false, dataSourceFactory); } /** - * @deprecated Use {@link HttpMediaDrmCallback#HttpMediaDrmCallback(String, Factory)}. Request - * properties can be set by calling {@link #setKeyRequestProperty(String, String)}. - * @param defaultUrl The default license URL. + * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL, or for all key requests if {@code forceDefaultLicenseUrl} is + * set to true. + * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that + * include their own license URL. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. - * @param keyRequestProperties Request properties to set when making key requests, or null. */ - @Deprecated - public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory, - Map keyRequestProperties) { + public HttpMediaDrmCallback(String defaultLicenseUrl, boolean forceDefaultLicenseUrl, + HttpDataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; - this.defaultUrl = defaultUrl; + this.defaultLicenseUrl = defaultLicenseUrl; + this.forceDefaultLicenseUrl = forceDefaultLicenseUrl; this.keyRequestProperties = new HashMap<>(); - if (keyRequestProperties != null) { - this.keyRequestProperties.putAll(keyRequestProperties); - } } /** @@ -113,43 +109,87 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { @Override public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { - String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); - return executePost(dataSourceFactory, url, new byte[0], null); + String url = + request.getDefaultUrl() + "&signedRequest=" + Util.fromUtf8Bytes(request.getData()); + return executePost(dataSourceFactory, url, /* httpBody= */ null, /* requestProperties= */ null); } @Override public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { - String url = request.getDefaultUrl(); - if (TextUtils.isEmpty(url)) { - url = defaultUrl; + String url = request.getLicenseServerUrl(); + if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) { + url = defaultLicenseUrl; } Map requestProperties = new HashMap<>(); - requestProperties.put("Content-Type", "application/octet-stream"); + // Add standard request properties for supported schemes. + String contentType = C.PLAYREADY_UUID.equals(uuid) ? "text/xml" + : (C.CLEARKEY_UUID.equals(uuid) ? "application/json" : "application/octet-stream"); + requestProperties.put("Content-Type", contentType); if (C.PLAYREADY_UUID.equals(uuid)) { - requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES); + requestProperties.put("SOAPAction", + "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"); } + // Add additional request properties. synchronized (keyRequestProperties) { requestProperties.putAll(keyRequestProperties); } return executePost(dataSourceFactory, url, request.getData(), requestProperties); } - private static byte[] executePost(HttpDataSource.Factory dataSourceFactory, String url, - byte[] data, Map requestProperties) throws IOException { + private static byte[] executePost( + HttpDataSource.Factory dataSourceFactory, + String url, + @Nullable byte[] httpBody, + @Nullable Map requestProperties) + throws IOException { HttpDataSource dataSource = dataSourceFactory.createDataSource(); if (requestProperties != null) { for (Map.Entry requestProperty : requestProperties.entrySet()) { dataSource.setRequestProperty(requestProperty.getKey(), requestProperty.getValue()); } } - DataSpec dataSpec = new DataSpec(Uri.parse(url), data, 0, 0, C.LENGTH_UNSET, null, - DataSpec.FLAG_ALLOW_GZIP); - DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); - try { - return Util.toByteArray(inputStream); - } finally { - Util.closeQuietly(inputStream); + + int manualRedirectCount = 0; + while (true) { + DataSpec dataSpec = + new DataSpec( + Uri.parse(url), + DataSpec.HTTP_METHOD_POST, + httpBody, + /* absoluteStreamPosition= */ 0, + /* position= */ 0, + /* length= */ C.LENGTH_UNSET, + /* key= */ null, + DataSpec.FLAG_ALLOW_GZIP); + DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); + try { + return Util.toByteArray(inputStream); + } catch (InvalidResponseCodeException e) { + // For POST requests, the underlying network stack will not normally follow 307 or 308 + // redirects automatically. Do so manually here. + boolean manuallyRedirect = + (e.responseCode == 307 || e.responseCode == 308) + && manualRedirectCount++ < MAX_MANUAL_REDIRECTS; + String redirectUrl = manuallyRedirect ? getRedirectUrl(e) : null; + if (redirectUrl == null) { + throw e; + } + url = redirectUrl; + } finally { + Util.closeQuietly(inputStream); + } } } + private static @Nullable String getRedirectUrl(InvalidResponseCodeException exception) { + Map> headerFields = exception.headerFields; + if (headerFields != null) { + List locationHeaders = headerFields.get("Location"); + if (locationHeaders != null && !locationHeaders.isEmpty()) { + return locationHeaders.get(0); + } + } + return null; + } + } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java new file mode 100644 index 000000000000..23e1859ca87c --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.drm; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.UUID; + +/** + * A {@link MediaDrmCallback} that provides a fixed response to key requests. Provisioning is not + * supported. This implementation is primarily useful for providing locally stored keys to decrypt + * ClearKey protected content. It is not suitable for use with Widevine or PlayReady protected + * content. + */ +public final class LocalMediaDrmCallback implements MediaDrmCallback { + + private final byte[] keyResponse; + + /** + * @param keyResponse The fixed response for all key requests. + */ + public LocalMediaDrmCallback(byte[] keyResponse) { + this.keyResponse = Assertions.checkNotNull(keyResponse); + } + + @Override + public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { + return keyResponse; + } + +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/MediaDrmCallback.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/MediaDrmCallback.java index a2c3c4fe60f0..2bc41f6becef 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/MediaDrmCallback.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/MediaDrmCallback.java @@ -43,5 +43,4 @@ public interface MediaDrmCallback { * @throws Exception If an error occurred executing the request. */ byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception; - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 42b0820caed1..3ce3879a76a3 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -13,29 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.mozilla.thirdparty.com.google.android.exoplayer2.drm; +import android.annotation.TargetApi; import android.media.MediaDrm; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.util.Pair; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; -import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DefaultDrmSessionManager.EventListener; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; -import java.io.IOException; -import java.util.HashMap; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; -/** - * Helper class to download, renew and release offline licenses. - */ +/** Helper class to download, renew and release offline licenses. */ +@TargetApi(18) +@RequiresApi(18) public final class OfflineLicenseHelper { + private static final DrmInitData DUMMY_DRM_INIT_DATA = new DrmInitData(); + private final ConditionVariable conditionVariable; private final DefaultDrmSessionManager drmSessionManager; private final HandlerThread handlerThread; @@ -44,81 +48,118 @@ public final class OfflineLicenseHelper { * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance * is no longer required. * - * @param licenseUrl The default license URL. + * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL. * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. * @return A new instance which uses Widevine CDM. * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be * instantiated. */ public static OfflineLicenseHelper newWidevineInstance( - String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException { - return newWidevineInstance( - new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null); + String defaultLicenseUrl, Factory httpDataSourceFactory) + throws UnsupportedDrmException { + return newWidevineInstance(defaultLicenseUrl, false, httpDataSourceFactory, null); } /** * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance * is no longer required. * - * @param callback Performs key and provisioning requests. - * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL. + * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that + * include their own license URL. + * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. * @return A new instance which uses Widevine CDM. * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be * instantiated. - * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm, - * MediaDrmCallback, HashMap, Handler, EventListener) */ public static OfflineLicenseHelper newWidevineInstance( - MediaDrmCallback callback, HashMap optionalKeyRequestParameters) + String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException { - return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), callback, + return newWidevineInstance(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory, + null); + } + + /** + * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance + * is no longer required. + * + * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL. + * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that + * include their own license URL. + * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument + * to {@link MediaDrm#getKeyRequest}. May be null. + * @return A new instance which uses Widevine CDM. + * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be + * instantiated. + * @see DefaultDrmSessionManager.Builder + */ + public static OfflineLicenseHelper newWidevineInstance( + String defaultLicenseUrl, + boolean forceDefaultLicenseUrl, + Factory httpDataSourceFactory, + @Nullable Map optionalKeyRequestParameters) + throws UnsupportedDrmException { + return new OfflineLicenseHelper<>( + C.WIDEVINE_UUID, + FrameworkMediaDrm.DEFAULT_PROVIDER, + new HttpMediaDrmCallback(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory), optionalKeyRequestParameters); } /** * Constructs an instance. Call {@link #release()} when the instance is no longer required. * - * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. + * @param uuid The UUID of the drm scheme. + * @param mediaDrmProvider A {@link ExoMediaDrm.Provider}. * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. - * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm, - * MediaDrmCallback, HashMap, Handler, EventListener) + * to {@link MediaDrm#getKeyRequest}. May be null. + * @see DefaultDrmSessionManager.Builder */ - public OfflineLicenseHelper(ExoMediaDrm mediaDrm, MediaDrmCallback callback, - HashMap optionalKeyRequestParameters) { + @SuppressWarnings("unchecked") + public OfflineLicenseHelper( + UUID uuid, + ExoMediaDrm.Provider mediaDrmProvider, + MediaDrmCallback callback, + @Nullable Map optionalKeyRequestParameters) { handlerThread = new HandlerThread("OfflineLicenseHelper"); handlerThread.start(); conditionVariable = new ConditionVariable(); - EventListener eventListener = new EventListener() { - @Override - public void onDrmKeysLoaded() { - conditionVariable.open(); - } + DefaultDrmSessionEventListener eventListener = + new DefaultDrmSessionEventListener() { + @Override + public void onDrmKeysLoaded() { + conditionVariable.open(); + } - @Override - public void onDrmSessionManagerError(Exception e) { - conditionVariable.open(); - } + @Override + public void onDrmSessionManagerError(Exception e) { + conditionVariable.open(); + } - @Override - public void onDrmKeysRestored() { - conditionVariable.open(); - } + @Override + public void onDrmKeysRestored() { + conditionVariable.open(); + } - @Override - public void onDrmKeysRemoved() { - conditionVariable.open(); - } - }; - drmSessionManager = new DefaultDrmSessionManager<>(C.WIDEVINE_UUID, mediaDrm, callback, - optionalKeyRequestParameters, new Handler(handlerThread.getLooper()), eventListener); - } - - /** Releases the helper. Should be called when the helper is no longer required. */ - public void release() { - handlerThread.quit(); + @Override + public void onDrmKeysRemoved() { + conditionVariable.open(); + } + }; + if (optionalKeyRequestParameters == null) { + optionalKeyRequestParameters = Collections.emptyMap(); + } + drmSessionManager = + (DefaultDrmSessionManager) + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(uuid, mediaDrmProvider) + .setKeyRequestParameters(optionalKeyRequestParameters) + .build(callback); + drmSessionManager.addListener(new Handler(handlerThread.getLooper()), eventListener); } /** @@ -126,12 +167,9 @@ public final class OfflineLicenseHelper { * * @param drmInitData The {@link DrmInitData} for the content whose license is to be downloaded. * @return The key set id for the downloaded license. - * @throws IOException If an error occurs reading data from the stream. - * @throws InterruptedException If the thread has been interrupted. * @throws DrmSessionException Thrown when a DRM session error occurs. */ - public synchronized byte[] downloadLicense(DrmInitData drmInitData) throws IOException, - InterruptedException, DrmSessionException { + public synchronized byte[] downloadLicense(DrmInitData drmInitData) throws DrmSessionException { Assertions.checkArgument(drmInitData != null); return blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, null, drmInitData); } @@ -146,7 +184,8 @@ public final class OfflineLicenseHelper { public synchronized byte[] renewLicense(byte[] offlineLicenseKeySetId) throws DrmSessionException { Assertions.checkNotNull(offlineLicenseKeySetId); - return blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, offlineLicenseKeySetId, null); + return blockingKeyRequest( + DefaultDrmSessionManager.MODE_DOWNLOAD, offlineLicenseKeySetId, DUMMY_DRM_INIT_DATA); } /** @@ -158,7 +197,8 @@ public final class OfflineLicenseHelper { public synchronized void releaseLicense(byte[] offlineLicenseKeySetId) throws DrmSessionException { Assertions.checkNotNull(offlineLicenseKeySetId); - blockingKeyRequest(DefaultDrmSessionManager.MODE_RELEASE, offlineLicenseKeySetId, null); + blockingKeyRequest( + DefaultDrmSessionManager.MODE_RELEASE, offlineLicenseKeySetId, DUMMY_DRM_INIT_DATA); } /** @@ -171,36 +211,49 @@ public final class OfflineLicenseHelper { public synchronized Pair getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId) throws DrmSessionException { Assertions.checkNotNull(offlineLicenseKeySetId); - DrmSession drmSession = openBlockingKeyRequest(DefaultDrmSessionManager.MODE_QUERY, - offlineLicenseKeySetId, null); + drmSessionManager.prepare(); + DrmSession drmSession = + openBlockingKeyRequest( + DefaultDrmSessionManager.MODE_QUERY, offlineLicenseKeySetId, DUMMY_DRM_INIT_DATA); DrmSessionException error = drmSession.getError(); Pair licenseDurationRemainingSec = WidevineUtil.getLicenseDurationRemainingSec(drmSession); - drmSessionManager.releaseSession(drmSession); + drmSession.release(); + drmSessionManager.release(); if (error != null) { if (error.getCause() instanceof KeysExpiredException) { return Pair.create(0L, 0L); } throw error; } - return licenseDurationRemainingSec; + return Assertions.checkNotNull(licenseDurationRemainingSec); } - private byte[] blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId, - DrmInitData drmInitData) throws DrmSessionException { + /** + * Releases the helper. Should be called when the helper is no longer required. + */ + public void release() { + handlerThread.quit(); + } + + private byte[] blockingKeyRequest( + @Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, DrmInitData drmInitData) + throws DrmSessionException { + drmSessionManager.prepare(); DrmSession drmSession = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId, drmInitData); DrmSessionException error = drmSession.getError(); byte[] keySetId = drmSession.getOfflineLicenseKeySetId(); - drmSessionManager.releaseSession(drmSession); + drmSession.release(); + drmSessionManager.release(); if (error != null) { throw error; } - return keySetId; + return Assertions.checkNotNull(keySetId); } - private DrmSession openBlockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId, - DrmInitData drmInitData) { + private DrmSession openBlockingKeyRequest( + @Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, DrmInitData drmInitData) { drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId); conditionVariable.close(); DrmSession drmSession = drmSessionManager.acquireSession(handlerThread.getLooper(), diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/UnsupportedDrmException.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/UnsupportedDrmException.java index fe9f94a25a85..4dc9f2b0b297 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/UnsupportedDrmException.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/UnsupportedDrmException.java @@ -15,7 +15,8 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.drm; -import android.support.annotation.IntDef; +import androidx.annotation.IntDef; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -25,8 +26,10 @@ import java.lang.annotation.RetentionPolicy; public final class UnsupportedDrmException extends Exception { /** - * The reason for the exception. + * The reason for the exception. One of {@link #REASON_UNSUPPORTED_SCHEME} or {@link + * #REASON_INSTANTIATION_ERROR}. */ + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({REASON_UNSUPPORTED_SCHEME, REASON_INSTANTIATION_ERROR}) public @interface Reason {} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/WidevineUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/WidevineUtil.java index 920453782b87..67539bef39d6 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/WidevineUtil.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/WidevineUtil.java @@ -16,6 +16,7 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.drm; import android.util.Pair; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import java.util.Map; @@ -34,14 +35,17 @@ public final class WidevineUtil { /** * Returns license and playback durations remaining in seconds. * - * @return A {@link Pair} consisting of the remaining license and playback durations in seconds. - * @throws IllegalStateException If called when a session isn't opened. - * @param drmSession + * @param drmSession The drm session to query. + * @return A {@link Pair} consisting of the remaining license and playback durations in seconds, + * or null if called before the session has been opened or after it's been released. */ - public static Pair getLicenseDurationRemainingSec(DrmSession drmSession) { + public static @Nullable Pair getLicenseDurationRemainingSec( + DrmSession drmSession) { Map keyStatus = drmSession.queryKeyStatus(); - return new Pair<>( - getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING), + if (keyStatus == null) { + return null; + } + return new Pair<>(getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING), getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING)); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/package-info.java new file mode 100644 index 000000000000..ec885e2ad753 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.drm; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java new file mode 100644 index 000000000000..b0b7c7da13ee --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java @@ -0,0 +1,538 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A seeker that supports seeking within a stream by searching for the target frame using binary + * search. + * + *

This seeker operates on a stream that contains multiple frames (or samples). Each frame is + * associated with some kind of timestamps, such as stream time, or frame indices. Given a target + * seek time, the seeker will find the corresponding target timestamp, and perform a search + * operation within the stream to identify the target frame and return the byte position in the + * stream of the target frame. + */ +public abstract class BinarySearchSeeker { + + /** A seeker that looks for a given timestamp from an input. */ + protected interface TimestampSeeker { + + /** + * Searches a limited window of the provided input for a target timestamp. The size of the + * window is implementation specific, but should be small enough such that it's reasonable for + * multiple such reads to occur during a seek operation. + * + * @param input The {@link ExtractorInput} from which data should be peeked. + * @param targetTimestamp The target timestamp. + * @return A {@link TimestampSearchResult} that describes the result of the search. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp) + throws IOException, InterruptedException; + + /** Called when a seek operation finishes. */ + default void onSeekFinished() {} + } + + /** + * A {@link SeekTimestampConverter} implementation that returns the seek time itself as the + * timestamp for a seek time position. + */ + public static final class DefaultSeekTimestampConverter implements SeekTimestampConverter { + + @Override + public long timeUsToTargetTime(long timeUs) { + return timeUs; + } + } + + /** + * A converter that converts seek time in stream time into target timestamp for the {@link + * BinarySearchSeeker}. + */ + protected interface SeekTimestampConverter { + /** + * Converts a seek time in microseconds into target timestamp for the {@link + * BinarySearchSeeker}. + */ + long timeUsToTargetTime(long timeUs); + } + + /** + * When seeking within the source, if the offset is smaller than or equal to this value, the seek + * operation will be performed using a skip operation. Otherwise, the source will be reloaded at + * the new seek position. + */ + private static final long MAX_SKIP_BYTES = 256 * 1024; + + protected final BinarySearchSeekMap seekMap; + protected final TimestampSeeker timestampSeeker; + protected @Nullable SeekOperationParams seekOperationParams; + + private final int minimumSearchRange; + + /** + * Constructs an instance. + * + * @param seekTimestampConverter The {@link SeekTimestampConverter} that converts seek time in + * stream time into target timestamp. + * @param timestampSeeker A {@link TimestampSeeker} that will be used to search for timestamps + * within the stream. + * @param durationUs The duration of the stream in microseconds. + * @param floorTimePosition The minimum timestamp value (inclusive) in the stream. + * @param ceilingTimePosition The minimum timestamp value (exclusive) in the stream. + * @param floorBytePosition The starting position of the frame with minimum timestamp value + * (inclusive) in the stream. + * @param ceilingBytePosition The position after the frame with maximum timestamp value in the + * stream. + * @param approxBytesPerFrame Approximated bytes per frame. + * @param minimumSearchRange The minimum byte range that this binary seeker will operate on. If + * the remaining search range is smaller than this value, the search will stop, and the seeker + * will return the position at the floor of the range as the result. + */ + @SuppressWarnings("initialization") + protected BinarySearchSeeker( + SeekTimestampConverter seekTimestampConverter, + TimestampSeeker timestampSeeker, + long durationUs, + long floorTimePosition, + long ceilingTimePosition, + long floorBytePosition, + long ceilingBytePosition, + long approxBytesPerFrame, + int minimumSearchRange) { + this.timestampSeeker = timestampSeeker; + this.minimumSearchRange = minimumSearchRange; + this.seekMap = + new BinarySearchSeekMap( + seekTimestampConverter, + durationUs, + floorTimePosition, + ceilingTimePosition, + floorBytePosition, + ceilingBytePosition, + approxBytesPerFrame); + } + + /** Returns the seek map for the stream. */ + public final SeekMap getSeekMap() { + return seekMap; + } + + /** + * Sets the target time in microseconds within the stream to seek to. + * + * @param timeUs The target time in microseconds within the stream. + */ + public final void setSeekTargetUs(long timeUs) { + if (seekOperationParams != null && seekOperationParams.getSeekTimeUs() == timeUs) { + return; + } + seekOperationParams = createSeekParamsForTargetTimeUs(timeUs); + } + + /** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */ + public final boolean isSeeking() { + return seekOperationParams != null; + } + + /** + * Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from + * {@link Extractor}. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated + * to hold the position of the required seek. + * @return One of the {@code RESULT_} values defined in {@link Extractor}. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + public int handlePendingSeek(ExtractorInput input, PositionHolder seekPositionHolder) + throws InterruptedException, IOException { + TimestampSeeker timestampSeeker = Assertions.checkNotNull(this.timestampSeeker); + while (true) { + SeekOperationParams seekOperationParams = Assertions.checkNotNull(this.seekOperationParams); + long floorPosition = seekOperationParams.getFloorBytePosition(); + long ceilingPosition = seekOperationParams.getCeilingBytePosition(); + long searchPosition = seekOperationParams.getNextSearchBytePosition(); + + if (ceilingPosition - floorPosition <= minimumSearchRange) { + // The seeking range is too small, so we can just continue from the floor position. + markSeekOperationFinished(/* foundTargetFrame= */ false, floorPosition); + return seekToPosition(input, floorPosition, seekPositionHolder); + } + if (!skipInputUntilPosition(input, searchPosition)) { + return seekToPosition(input, searchPosition, seekPositionHolder); + } + + input.resetPeekPosition(); + TimestampSearchResult timestampSearchResult = + timestampSeeker.searchForTimestamp(input, seekOperationParams.getTargetTimePosition()); + + switch (timestampSearchResult.type) { + case TimestampSearchResult.TYPE_POSITION_OVERESTIMATED: + seekOperationParams.updateSeekCeiling( + timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate); + break; + case TimestampSearchResult.TYPE_POSITION_UNDERESTIMATED: + seekOperationParams.updateSeekFloor( + timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate); + break; + case TimestampSearchResult.TYPE_TARGET_TIMESTAMP_FOUND: + markSeekOperationFinished( + /* foundTargetFrame= */ true, timestampSearchResult.bytePositionToUpdate); + skipInputUntilPosition(input, timestampSearchResult.bytePositionToUpdate); + return seekToPosition( + input, timestampSearchResult.bytePositionToUpdate, seekPositionHolder); + case TimestampSearchResult.TYPE_NO_TIMESTAMP: + // We can't find any timestamp in the search range from the search position. + // Give up, and just continue reading from the last search position in this case. + markSeekOperationFinished(/* foundTargetFrame= */ false, searchPosition); + return seekToPosition(input, searchPosition, seekPositionHolder); + default: + throw new IllegalStateException("Invalid case"); + } + } + } + + protected SeekOperationParams createSeekParamsForTargetTimeUs(long timeUs) { + return new SeekOperationParams( + timeUs, + seekMap.timeUsToTargetTime(timeUs), + seekMap.floorTimePosition, + seekMap.ceilingTimePosition, + seekMap.floorBytePosition, + seekMap.ceilingBytePosition, + seekMap.approxBytesPerFrame); + } + + protected final void markSeekOperationFinished(boolean foundTargetFrame, long resultPosition) { + seekOperationParams = null; + timestampSeeker.onSeekFinished(); + onSeekOperationFinished(foundTargetFrame, resultPosition); + } + + protected void onSeekOperationFinished(boolean foundTargetFrame, long resultPosition) { + // Do nothing. + } + + protected final boolean skipInputUntilPosition(ExtractorInput input, long position) + throws IOException, InterruptedException { + long bytesToSkip = position - input.getPosition(); + if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) { + input.skipFully((int) bytesToSkip); + return true; + } + return false; + } + + protected final int seekToPosition( + ExtractorInput input, long position, PositionHolder seekPositionHolder) { + if (position == input.getPosition()) { + return Extractor.RESULT_CONTINUE; + } else { + seekPositionHolder.position = position; + return Extractor.RESULT_SEEK; + } + } + + /** + * Contains parameters for a pending seek operation by {@link BinarySearchSeeker}. + * + *

This class holds parameters for a binary-search for the {@code targetTimePosition} in the + * range [floorPosition, ceilingPosition). + */ + protected static class SeekOperationParams { + private final long seekTimeUs; + private final long targetTimePosition; + private final long approxBytesPerFrame; + + private long floorTimePosition; + private long ceilingTimePosition; + private long floorBytePosition; + private long ceilingBytePosition; + private long nextSearchBytePosition; + + /** + * Returns the next position in the stream to search for target frame, given [floorBytePosition, + * ceilingBytePosition), with corresponding [floorTimePosition, ceilingTimePosition). + */ + protected static long calculateNextSearchBytePosition( + long targetTimePosition, + long floorTimePosition, + long ceilingTimePosition, + long floorBytePosition, + long ceilingBytePosition, + long approxBytesPerFrame) { + if (floorBytePosition + 1 >= ceilingBytePosition + || floorTimePosition + 1 >= ceilingTimePosition) { + return floorBytePosition; + } + long seekTimeDuration = targetTimePosition - floorTimePosition; + float estimatedBytesPerTimeUnit = + (float) (ceilingBytePosition - floorBytePosition) + / (ceilingTimePosition - floorTimePosition); + // It's better to under-estimate rather than over-estimate, because the extractor + // input can skip forward easily, but cannot rewind easily (it may require a new connection + // to be made). + // Therefore, we should reduce the estimated position by some amount, so it will converge to + // the correct frame earlier. + long bytesToSkip = (long) (seekTimeDuration * estimatedBytesPerTimeUnit); + long confidenceInterval = bytesToSkip / 20; + long estimatedFramePosition = floorBytePosition + bytesToSkip - approxBytesPerFrame; + long estimatedPosition = estimatedFramePosition - confidenceInterval; + return Util.constrainValue(estimatedPosition, floorBytePosition, ceilingBytePosition - 1); + } + + protected SeekOperationParams( + long seekTimeUs, + long targetTimePosition, + long floorTimePosition, + long ceilingTimePosition, + long floorBytePosition, + long ceilingBytePosition, + long approxBytesPerFrame) { + this.seekTimeUs = seekTimeUs; + this.targetTimePosition = targetTimePosition; + this.floorTimePosition = floorTimePosition; + this.ceilingTimePosition = ceilingTimePosition; + this.floorBytePosition = floorBytePosition; + this.ceilingBytePosition = ceilingBytePosition; + this.approxBytesPerFrame = approxBytesPerFrame; + this.nextSearchBytePosition = + calculateNextSearchBytePosition( + targetTimePosition, + floorTimePosition, + ceilingTimePosition, + floorBytePosition, + ceilingBytePosition, + approxBytesPerFrame); + } + + /** + * Returns the floor byte position of the range [floorPosition, ceilingPosition) for this seek + * operation. + */ + private long getFloorBytePosition() { + return floorBytePosition; + } + + /** + * Returns the ceiling byte position of the range [floorPosition, ceilingPosition) for this seek + * operation. + */ + private long getCeilingBytePosition() { + return ceilingBytePosition; + } + + /** Returns the target timestamp as translated from the seek time. */ + private long getTargetTimePosition() { + return targetTimePosition; + } + + /** Returns the target seek time in microseconds. */ + private long getSeekTimeUs() { + return seekTimeUs; + } + + /** Updates the floor constraints (inclusive) of the seek operation. */ + private void updateSeekFloor(long floorTimePosition, long floorBytePosition) { + this.floorTimePosition = floorTimePosition; + this.floorBytePosition = floorBytePosition; + updateNextSearchBytePosition(); + } + + /** Updates the ceiling constraints (exclusive) of the seek operation. */ + private void updateSeekCeiling(long ceilingTimePosition, long ceilingBytePosition) { + this.ceilingTimePosition = ceilingTimePosition; + this.ceilingBytePosition = ceilingBytePosition; + updateNextSearchBytePosition(); + } + + /** Returns the next position in the stream to search. */ + private long getNextSearchBytePosition() { + return nextSearchBytePosition; + } + + private void updateNextSearchBytePosition() { + this.nextSearchBytePosition = + calculateNextSearchBytePosition( + targetTimePosition, + floorTimePosition, + ceilingTimePosition, + floorBytePosition, + ceilingBytePosition, + approxBytesPerFrame); + } + } + + /** + * Represents possible search results for {@link + * TimestampSeeker#searchForTimestamp(ExtractorInput, long)}. + */ + public static final class TimestampSearchResult { + + /** The search found a timestamp that it deems close enough to the given target. */ + public static final int TYPE_TARGET_TIMESTAMP_FOUND = 0; + /** The search found only timestamps larger than the target timestamp. */ + public static final int TYPE_POSITION_OVERESTIMATED = -1; + /** The search found only timestamps smaller than the target timestamp. */ + public static final int TYPE_POSITION_UNDERESTIMATED = -2; + /** The search didn't find any timestamps. */ + public static final int TYPE_NO_TIMESTAMP = -3; + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + TYPE_TARGET_TIMESTAMP_FOUND, + TYPE_POSITION_OVERESTIMATED, + TYPE_POSITION_UNDERESTIMATED, + TYPE_NO_TIMESTAMP + }) + @interface Type {} + + public static final TimestampSearchResult NO_TIMESTAMP_IN_RANGE_RESULT = + new TimestampSearchResult(TYPE_NO_TIMESTAMP, C.TIME_UNSET, C.POSITION_UNSET); + + /** The type of the result. */ + @Type private final int type; + + /** + * When {@link #type} is {@link #TYPE_POSITION_OVERESTIMATED}, the {@link + * SeekOperationParams#ceilingTimePosition} should be updated with this value. When {@link + * #type} is {@link #TYPE_POSITION_UNDERESTIMATED}, the {@link + * SeekOperationParams#floorTimePosition} should be updated with this value. + */ + private final long timestampToUpdate; + /** + * When {@link #type} is {@link #TYPE_POSITION_OVERESTIMATED}, the {@link + * SeekOperationParams#ceilingBytePosition} should be updated with this value. When {@link + * #type} is {@link #TYPE_POSITION_UNDERESTIMATED}, the {@link + * SeekOperationParams#floorBytePosition} should be updated with this value. + */ + private final long bytePositionToUpdate; + + private TimestampSearchResult( + @Type int type, long timestampToUpdate, long bytePositionToUpdate) { + this.type = type; + this.timestampToUpdate = timestampToUpdate; + this.bytePositionToUpdate = bytePositionToUpdate; + } + + /** + * Returns a result to signal that the current position in the input stream overestimates the + * true position of the target frame, and the {@link BinarySearchSeeker} should modify its + * {@link SeekOperationParams}'s ceiling timestamp and byte position using the given values. + */ + public static TimestampSearchResult overestimatedResult( + long newCeilingTimestamp, long newCeilingBytePosition) { + return new TimestampSearchResult( + TYPE_POSITION_OVERESTIMATED, newCeilingTimestamp, newCeilingBytePosition); + } + + /** + * Returns a result to signal that the current position in the input stream underestimates the + * true position of the target frame, and the {@link BinarySearchSeeker} should modify its + * {@link SeekOperationParams}'s floor timestamp and byte position using the given values. + */ + public static TimestampSearchResult underestimatedResult( + long newFloorTimestamp, long newCeilingBytePosition) { + return new TimestampSearchResult( + TYPE_POSITION_UNDERESTIMATED, newFloorTimestamp, newCeilingBytePosition); + } + + /** + * Returns a result to signal that the target timestamp has been found at {@code + * resultBytePosition}, and the seek operation can stop. + */ + public static TimestampSearchResult targetFoundResult(long resultBytePosition) { + return new TimestampSearchResult( + TYPE_TARGET_TIMESTAMP_FOUND, C.TIME_UNSET, resultBytePosition); + } + } + + /** + * A {@link SeekMap} implementation that returns the estimated byte location from {@link + * SeekOperationParams#calculateNextSearchBytePosition(long, long, long, long, long, long)} for + * each {@link #getSeekPoints(long)} query. + */ + public static class BinarySearchSeekMap implements SeekMap { + private final SeekTimestampConverter seekTimestampConverter; + private final long durationUs; + private final long floorTimePosition; + private final long ceilingTimePosition; + private final long floorBytePosition; + private final long ceilingBytePosition; + private final long approxBytesPerFrame; + + /** Constructs a new instance of this seek map. */ + public BinarySearchSeekMap( + SeekTimestampConverter seekTimestampConverter, + long durationUs, + long floorTimePosition, + long ceilingTimePosition, + long floorBytePosition, + long ceilingBytePosition, + long approxBytesPerFrame) { + this.seekTimestampConverter = seekTimestampConverter; + this.durationUs = durationUs; + this.floorTimePosition = floorTimePosition; + this.ceilingTimePosition = ceilingTimePosition; + this.floorBytePosition = floorBytePosition; + this.ceilingBytePosition = ceilingBytePosition; + this.approxBytesPerFrame = approxBytesPerFrame; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + long nextSearchPosition = + SeekOperationParams.calculateNextSearchBytePosition( + /* targetTimePosition= */ seekTimestampConverter.timeUsToTargetTime(timeUs), + /* floorTimePosition= */ floorTimePosition, + /* ceilingTimePosition= */ ceilingTimePosition, + /* floorBytePosition= */ floorBytePosition, + /* ceilingBytePosition= */ ceilingBytePosition, + /* approxBytesPerFrame= */ approxBytesPerFrame); + return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition)); + } + + @Override + public long getDurationUs() { + return durationUs; + } + + /** @see SeekTimestampConverter#timeUsToTargetTime(long) */ + public long timeUsToTargetTime(long timeUs) { + return seekTimestampConverter.timeUsToTargetTime(timeUs); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.java index df1de60734d9..4fdf9f3c5561 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.java @@ -16,6 +16,7 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.Arrays; /** * Defines chunks of samples within a media stream. @@ -91,8 +92,30 @@ public final class ChunkIndex implements SeekMap { } @Override - public long getPosition(long timeUs) { - return offsets[getChunkIndex(timeUs)]; + public SeekPoints getSeekPoints(long timeUs) { + int chunkIndex = getChunkIndex(timeUs); + SeekPoint seekPoint = new SeekPoint(timesUs[chunkIndex], offsets[chunkIndex]); + if (seekPoint.timeUs >= timeUs || chunkIndex == length - 1) { + return new SeekPoints(seekPoint); + } else { + SeekPoint nextSeekPoint = new SeekPoint(timesUs[chunkIndex + 1], offsets[chunkIndex + 1]); + return new SeekPoints(seekPoint, nextSeekPoint); + } } + @Override + public String toString() { + return "ChunkIndex(" + + "length=" + + length + + ", sizes=" + + Arrays.toString(sizes) + + ", offsets=" + + Arrays.toString(offsets) + + ", timeUs=" + + Arrays.toString(timesUs) + + ", durationsUs=" + + Arrays.toString(durationsUs) + + ")"; + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java new file mode 100644 index 000000000000..215aac0e6dd3 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * A {@link SeekMap} implementation that assumes the stream has a constant bitrate and consists of + * multiple independent frames of the same size. Seek points are calculated to be at frame + * boundaries. + */ +public class ConstantBitrateSeekMap implements SeekMap { + + private final long inputLength; + private final long firstFrameBytePosition; + private final int frameSize; + private final long dataSize; + private final int bitrate; + private final long durationUs; + + /** + * Constructs a new instance from a stream. + * + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param firstFrameBytePosition The byte-position of the first frame in the stream. + * @param bitrate The bitrate (which is assumed to be constant in the stream). + * @param frameSize The size of each frame in the stream in bytes. May be {@link C#LENGTH_UNSET} + * if unknown. + */ + public ConstantBitrateSeekMap( + long inputLength, long firstFrameBytePosition, int bitrate, int frameSize) { + this.inputLength = inputLength; + this.firstFrameBytePosition = firstFrameBytePosition; + this.frameSize = frameSize == C.LENGTH_UNSET ? 1 : frameSize; + this.bitrate = bitrate; + + if (inputLength == C.LENGTH_UNSET) { + dataSize = C.LENGTH_UNSET; + durationUs = C.TIME_UNSET; + } else { + dataSize = inputLength - firstFrameBytePosition; + durationUs = getTimeUsAtPosition(inputLength, firstFrameBytePosition, bitrate); + } + } + + @Override + public boolean isSeekable() { + return dataSize != C.LENGTH_UNSET; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + if (dataSize == C.LENGTH_UNSET) { + return new SeekPoints(new SeekPoint(0, firstFrameBytePosition)); + } + long seekFramePosition = getFramePositionForTimeUs(timeUs); + long seekTimeUs = getTimeUsAtPosition(seekFramePosition); + SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekFramePosition); + if (seekTimeUs >= timeUs || seekFramePosition + frameSize >= inputLength) { + return new SeekPoints(seekPoint); + } else { + long secondSeekPosition = seekFramePosition + frameSize; + long secondSeekTimeUs = getTimeUsAtPosition(secondSeekPosition); + SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); + return new SeekPoints(seekPoint, secondSeekPoint); + } + } + + @Override + public long getDurationUs() { + return durationUs; + } + + /** + * Returns the stream time in microseconds for a given position. + * + * @param position The stream byte-position. + * @return The stream time in microseconds for the given position. + */ + public long getTimeUsAtPosition(long position) { + return getTimeUsAtPosition(position, firstFrameBytePosition, bitrate); + } + + // Internal methods + + /** + * Returns the stream time in microseconds for a given stream position. + * + * @param position The stream byte-position. + * @param firstFrameBytePosition The position of the first frame in the stream. + * @param bitrate The bitrate (which is assumed to be constant in the stream). + * @return The stream time in microseconds for the given stream position. + */ + private static long getTimeUsAtPosition(long position, long firstFrameBytePosition, int bitrate) { + return Math.max(0, position - firstFrameBytePosition) + * C.BITS_PER_BYTE + * C.MICROS_PER_SECOND + / bitrate; + } + + private long getFramePositionForTimeUs(long timeUs) { + long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * C.BITS_PER_BYTE); + // Constrain to nearest preceding frame offset. + positionOffset = (positionOffset / frameSize) * frameSize; + positionOffset = + Util.constrainValue(positionOffset, /* min= */ 0, /* max= */ dataSize - frameSize); + return firstFrameBytePosition + positionOffset; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java index f3d8a3292cd7..93009f2d5c8a 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java @@ -30,8 +30,9 @@ public final class DefaultExtractorInput implements ExtractorInput { private static final int PEEK_MIN_FREE_SPACE_AFTER_RESIZE = 64 * 1024; private static final int PEEK_MAX_FREE_SPACE = 512 * 1024; - private static final byte[] SCRATCH_SPACE = new byte[4096]; + private static final int SCRATCH_SPACE_SIZE = 4096; + private final byte[] scratchSpace; private final DataSource dataSource; private final long streamLength; @@ -50,13 +51,16 @@ public final class DefaultExtractorInput implements ExtractorInput { this.position = position; this.streamLength = length; peekBuffer = new byte[PEEK_MIN_FREE_SPACE_AFTER_RESIZE]; + scratchSpace = new byte[SCRATCH_SPACE_SIZE]; } @Override public int read(byte[] target, int offset, int length) throws IOException, InterruptedException { int bytesRead = readFromPeekBuffer(target, offset, length); if (bytesRead == 0) { - bytesRead = readFromDataSource(target, offset, length, 0, true); + bytesRead = + readFromDataSource( + target, offset, length, /* bytesAlreadyRead= */ 0, /* allowEndOfInput= */ true); } commitBytesRead(bytesRead); return bytesRead; @@ -84,7 +88,7 @@ public final class DefaultExtractorInput implements ExtractorInput { int bytesSkipped = skipFromPeekBuffer(length); if (bytesSkipped == 0) { bytesSkipped = - readFromDataSource(SCRATCH_SPACE, 0, Math.min(length, SCRATCH_SPACE.length), 0, true); + readFromDataSource(scratchSpace, 0, Math.min(length, scratchSpace.length), 0, true); } commitBytesRead(bytesSkipped); return bytesSkipped; @@ -95,8 +99,9 @@ public final class DefaultExtractorInput implements ExtractorInput { throws IOException, InterruptedException { int bytesSkipped = skipFromPeekBuffer(length); while (bytesSkipped < length && bytesSkipped != C.RESULT_END_OF_INPUT) { - bytesSkipped = readFromDataSource(SCRATCH_SPACE, -bytesSkipped, - Math.min(length, bytesSkipped + SCRATCH_SPACE.length), bytesSkipped, allowEndOfInput); + int minLength = Math.min(length, bytesSkipped + scratchSpace.length); + bytesSkipped = + readFromDataSource(scratchSpace, -bytesSkipped, minLength, bytesSkipped, allowEndOfInput); } commitBytesRead(bytesSkipped); return bytesSkipped != C.RESULT_END_OF_INPUT; @@ -107,6 +112,31 @@ public final class DefaultExtractorInput implements ExtractorInput { skipFully(length, false); } + @Override + public int peek(byte[] target, int offset, int length) throws IOException, InterruptedException { + ensureSpaceForPeek(length); + int peekBufferRemainingBytes = peekBufferLength - peekBufferPosition; + int bytesPeeked; + if (peekBufferRemainingBytes == 0) { + bytesPeeked = + readFromDataSource( + peekBuffer, + peekBufferPosition, + length, + /* bytesAlreadyRead= */ 0, + /* allowEndOfInput= */ true); + if (bytesPeeked == C.RESULT_END_OF_INPUT) { + return C.RESULT_END_OF_INPUT; + } + peekBufferLength += bytesPeeked; + } else { + bytesPeeked = Math.min(length, peekBufferRemainingBytes); + } + System.arraycopy(peekBuffer, peekBufferPosition, target, offset, bytesPeeked); + peekBufferPosition += bytesPeeked; + return bytesPeeked; + } + @Override public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) throws IOException, InterruptedException { @@ -127,16 +157,16 @@ public final class DefaultExtractorInput implements ExtractorInput { public boolean advancePeekPosition(int length, boolean allowEndOfInput) throws IOException, InterruptedException { ensureSpaceForPeek(length); - int bytesPeeked = Math.min(peekBufferLength - peekBufferPosition, length); + int bytesPeeked = peekBufferLength - peekBufferPosition; while (bytesPeeked < length) { bytesPeeked = readFromDataSource(peekBuffer, peekBufferPosition, length, bytesPeeked, allowEndOfInput); if (bytesPeeked == C.RESULT_END_OF_INPUT) { return false; } + peekBufferLength = peekBufferPosition + bytesPeeked; } peekBufferPosition += length; - peekBufferLength = Math.max(peekBufferLength, peekBufferPosition); return true; } @@ -198,7 +228,7 @@ public final class DefaultExtractorInput implements ExtractorInput { } /** - * Reads from the peek buffer + * Reads from the peek buffer. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index e3a2af00b0e3..8425f8986027 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -15,6 +15,8 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.amr.AmrExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flac.FlacExtractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flv.FlvExtractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; @@ -22,53 +24,128 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.Fragme import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg.OggExtractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.Ac3Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.Ac4Extractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.AdtsExtractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.PsExtractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.wav.WavExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; import java.lang.reflect.Constructor; /** * An {@link ExtractorsFactory} that provides an array of extractors for the following formats: * *

    - *
  • MP4, including M4A ({@link Mp4Extractor})
  • - *
  • fMP4 ({@link FragmentedMp4Extractor})
  • - *
  • Matroska and WebM ({@link MatroskaExtractor})
  • - *
  • Ogg Vorbis/FLAC ({@link OggExtractor}
  • - *
  • MP3 ({@link Mp3Extractor})
  • - *
  • AAC ({@link AdtsExtractor})
  • - *
  • MPEG TS ({@link TsExtractor})
  • - *
  • MPEG PS ({@link PsExtractor})
  • - *
  • FLV ({@link FlvExtractor})
  • - *
  • WAV ({@link WavExtractor})
  • - *
  • AC3 ({@link Ac3Extractor})
  • - *
  • FLAC (only available if the FLAC extension is built and included)
  • + *
  • MP4, including M4A ({@link Mp4Extractor}) + *
  • fMP4 ({@link FragmentedMp4Extractor}) + *
  • Matroska and WebM ({@link MatroskaExtractor}) + *
  • Ogg Vorbis/FLAC ({@link OggExtractor} + *
  • MP3 ({@link Mp3Extractor}) + *
  • AAC ({@link AdtsExtractor}) + *
  • MPEG TS ({@link TsExtractor}) + *
  • MPEG PS ({@link PsExtractor}) + *
  • FLV ({@link FlvExtractor}) + *
  • WAV ({@link WavExtractor}) + *
  • AC3 ({@link Ac3Extractor}) + *
  • AC4 ({@link Ac4Extractor}) + *
  • AMR ({@link AmrExtractor}) + *
  • FLAC + *
      + *
    • If available, the FLAC extension extractor is used. + *
    • Otherwise, the core {@link FlacExtractor} is used. Note that Android devices do not + * generally include a FLAC decoder before API 27. This can be worked around by using + * the FLAC extension or the FFmpeg extension. + *
    *
*/ public final class DefaultExtractorsFactory implements ExtractorsFactory { - private static final Constructor FLAC_EXTRACTOR_CONSTRUCTOR; + private static final Constructor FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR; + static { - Constructor flacExtractorConstructor = null; + Constructor flacExtensionExtractorConstructor = null; try { - flacExtractorConstructor = - Class.forName("org.mozilla.thirdparty.com.google.android.exoplayer2.ext.flac.FlacExtractor") - .asSubclass(Extractor.class).getConstructor(); + // LINT.IfChange + @SuppressWarnings("nullness:argument.type.incompatible") + boolean isFlacNativeLibraryAvailable = + Boolean.TRUE.equals( + Class.forName("com.google.android.exoplayer2.ext.flac.FlacLibrary") + .getMethod("isAvailable") + .invoke(/* obj= */ null)); + if (isFlacNativeLibraryAvailable) { + flacExtensionExtractorConstructor = + Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") + .asSubclass(Extractor.class) + .getConstructor(); + } + // LINT.ThenChange(../../../../../../../../proguard-rules.txt) } catch (ClassNotFoundException e) { - // Extractor not found. - } catch (NoSuchMethodException e) { - // Constructor not found. + // Expected if the app was built without the FLAC extension. + } catch (Exception e) { + // The FLAC extension is present, but instantiation failed. + throw new RuntimeException("Error instantiating FLAC extension", e); } - FLAC_EXTRACTOR_CONSTRUCTOR = flacExtractorConstructor; + FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR = flacExtensionExtractorConstructor; } + private boolean constantBitrateSeekingEnabled; + private @AdtsExtractor.Flags int adtsFlags; + private @AmrExtractor.Flags int amrFlags; private @MatroskaExtractor.Flags int matroskaFlags; + private @Mp4Extractor.Flags int mp4Flags; private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags; private @Mp3Extractor.Flags int mp3Flags; + private @TsExtractor.Mode int tsMode; private @DefaultTsPayloadReaderFactory.Flags int tsFlags; + public DefaultExtractorsFactory() { + tsMode = TsExtractor.MODE_SINGLE_PMT; + } + + /** + * Convenience method to set whether approximate seeking using constant bitrate assumptions should + * be enabled for all extractors that support it. If set to true, the flags required to enable + * this functionality will be OR'd with those passed to the setters when creating extractor + * instances. If set to false then the flags passed to the setters will be used without + * modification. + * + * @param constantBitrateSeekingEnabled Whether approximate seeking using a constant bitrate + * assumption should be enabled for all extractors that support it. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setConstantBitrateSeekingEnabled( + boolean constantBitrateSeekingEnabled) { + this.constantBitrateSeekingEnabled = constantBitrateSeekingEnabled; + return this; + } + + /** + * Sets flags for {@link AdtsExtractor} instances created by the factory. + * + * @see AdtsExtractor#AdtsExtractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setAdtsExtractorFlags( + @AdtsExtractor.Flags int flags) { + this.adtsFlags = flags; + return this; + } + + /** + * Sets flags for {@link AmrExtractor} instances created by the factory. + * + * @see AmrExtractor#AmrExtractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setAmrExtractorFlags(@AmrExtractor.Flags int flags) { + this.amrFlags = flags; + return this; + } + /** * Sets flags for {@link MatroskaExtractor} instances created by the factory. * @@ -82,6 +159,18 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { return this; } + /** + * Sets flags for {@link Mp4Extractor} instances created by the factory. + * + * @see Mp4Extractor#Mp4Extractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setMp4ExtractorFlags(@Mp4Extractor.Flags int flags) { + this.mp4Flags = flags; + return this; + } + /** * Sets flags for {@link FragmentedMp4Extractor} instances created by the factory. * @@ -107,6 +196,18 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { return this; } + /** + * Sets the mode for {@link TsExtractor} instances created by the factory. + * + * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory) + * @param mode The mode to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setTsExtractorMode(@TsExtractor.Mode int mode) { + tsMode = mode; + return this; + } + /** * Sets flags for {@link DefaultTsPayloadReaderFactory}s used by {@link TsExtractor} instances * created by the factory. @@ -123,25 +224,44 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { @Override public synchronized Extractor[] createExtractors() { - Extractor[] extractors = new Extractor[FLAC_EXTRACTOR_CONSTRUCTOR == null ? 11 : 12]; + Extractor[] extractors = new Extractor[14]; extractors[0] = new MatroskaExtractor(matroskaFlags); extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags); - extractors[2] = new Mp4Extractor(); - extractors[3] = new Mp3Extractor(mp3Flags); - extractors[4] = new AdtsExtractor(); + extractors[2] = new Mp4Extractor(mp4Flags); + extractors[3] = + new Mp3Extractor( + mp3Flags + | (constantBitrateSeekingEnabled + ? Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0)); + extractors[4] = + new AdtsExtractor( + adtsFlags + | (constantBitrateSeekingEnabled + ? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0)); extractors[5] = new Ac3Extractor(); - extractors[6] = new TsExtractor(tsFlags); + extractors[6] = new TsExtractor(tsMode, tsFlags); extractors[7] = new FlvExtractor(); extractors[8] = new OggExtractor(); extractors[9] = new PsExtractor(); extractors[10] = new WavExtractor(); - if (FLAC_EXTRACTOR_CONSTRUCTOR != null) { + extractors[11] = + new AmrExtractor( + amrFlags + | (constantBitrateSeekingEnabled + ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0)); + extractors[12] = new Ac4Extractor(); + if (FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR != null) { try { - extractors[11] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance(); + extractors[13] = FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR.newInstance(); } catch (Exception e) { // Should never happen. throw new IllegalStateException("Unexpected error creating FLAC extractor", e); } + } else { + extractors[13] = new FlacExtractor(); } return extractors; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java deleted file mode 100644 index 1954b72b8376..000000000000 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java +++ /dev/null @@ -1,997 +0,0 @@ -/* - * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; - -import org.mozilla.thirdparty.com.google.android.exoplayer2.C; -import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; -import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; -import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocation; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; -import java.io.EOFException; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * A {@link TrackOutput} that buffers extracted samples in a queue and allows for consumption from - * that queue. - */ -public final class DefaultTrackOutput implements TrackOutput { - - /** - * A listener for changes to the upstream format. - */ - public interface UpstreamFormatChangedListener { - - /** - * Called on the loading thread when an upstream format change occurs. - * - * @param format The new upstream format. - */ - void onUpstreamFormatChanged(Format format); - - } - - private static final int INITIAL_SCRATCH_SIZE = 32; - - private static final int STATE_ENABLED = 0; - private static final int STATE_ENABLED_WRITING = 1; - private static final int STATE_DISABLED = 2; - - private final Allocator allocator; - private final int allocationLength; - - private final InfoQueue infoQueue; - private final LinkedBlockingDeque dataQueue; - private final BufferExtrasHolder extrasHolder; - private final ParsableByteArray scratch; - private final AtomicInteger state; - - // Accessed only by the consuming thread. - private long totalBytesDropped; - private Format downstreamFormat; - - // Accessed only by the loading thread (or the consuming thread when there is no loading thread). - private boolean pendingFormatAdjustment; - private Format lastUnadjustedFormat; - private long sampleOffsetUs; - private long totalBytesWritten; - private Allocation lastAllocation; - private int lastAllocationOffset; - private boolean pendingSplice; - private UpstreamFormatChangedListener upstreamFormatChangeListener; - - /** - * @param allocator An {@link Allocator} from which allocations for sample data can be obtained. - */ - public DefaultTrackOutput(Allocator allocator) { - this.allocator = allocator; - allocationLength = allocator.getIndividualAllocationLength(); - infoQueue = new InfoQueue(); - dataQueue = new LinkedBlockingDeque<>(); - extrasHolder = new BufferExtrasHolder(); - scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); - state = new AtomicInteger(); - lastAllocationOffset = allocationLength; - } - - // Called by the consuming thread, but only when there is no loading thread. - - /** - * Resets the output. - * - * @param enable Whether the output should be enabled. False if it should be disabled. - */ - public void reset(boolean enable) { - int previousState = state.getAndSet(enable ? STATE_ENABLED : STATE_DISABLED); - clearSampleData(); - infoQueue.resetLargestParsedTimestamps(); - if (previousState == STATE_DISABLED) { - downstreamFormat = null; - } - } - - /** - * Sets a source identifier for subsequent samples. - * - * @param sourceId The source identifier. - */ - public void sourceId(int sourceId) { - infoQueue.sourceId(sourceId); - } - - /** - * Indicates that samples subsequently queued to the buffer should be spliced into those already - * queued. - */ - public void splice() { - pendingSplice = true; - } - - /** - * Returns the current absolute write index. - */ - public int getWriteIndex() { - return infoQueue.getWriteIndex(); - } - - /** - * Discards samples from the write side of the buffer. - * - * @param discardFromIndex The absolute index of the first sample to be discarded. - */ - public void discardUpstreamSamples(int discardFromIndex) { - totalBytesWritten = infoQueue.discardUpstreamSamples(discardFromIndex); - dropUpstreamFrom(totalBytesWritten); - } - - /** - * Discards data from the write side of the buffer. Data is discarded from the specified absolute - * position. Any allocations that are fully discarded are returned to the allocator. - * - * @param absolutePosition The absolute position (inclusive) from which to discard data. - */ - private void dropUpstreamFrom(long absolutePosition) { - int relativePosition = (int) (absolutePosition - totalBytesDropped); - // Calculate the index of the allocation containing the position, and the offset within it. - int allocationIndex = relativePosition / allocationLength; - int allocationOffset = relativePosition % allocationLength; - // We want to discard any allocations after the one at allocationIdnex. - int allocationDiscardCount = dataQueue.size() - allocationIndex - 1; - if (allocationOffset == 0) { - // If the allocation at allocationIndex is empty, we should discard that one too. - allocationDiscardCount++; - } - // Discard the allocations. - for (int i = 0; i < allocationDiscardCount; i++) { - allocator.release(dataQueue.removeLast()); - } - // Update lastAllocation and lastAllocationOffset to reflect the new position. - lastAllocation = dataQueue.peekLast(); - lastAllocationOffset = allocationOffset == 0 ? allocationLength : allocationOffset; - } - - // Called by the consuming thread. - - /** - * Disables buffering of sample data and metadata. - */ - public void disable() { - if (state.getAndSet(STATE_DISABLED) == STATE_ENABLED) { - clearSampleData(); - } - } - - /** - * Returns whether the buffer is empty. - */ - public boolean isEmpty() { - return infoQueue.isEmpty(); - } - - /** - * Returns the current absolute read index. - */ - public int getReadIndex() { - return infoQueue.getReadIndex(); - } - - /** - * Peeks the source id of the next sample, or the current upstream source id if the buffer is - * empty. - * - * @return The source id. - */ - public int peekSourceId() { - return infoQueue.peekSourceId(); - } - - /** - * Returns the upstream {@link Format} in which samples are being queued. - */ - public Format getUpstreamFormat() { - return infoQueue.getUpstreamFormat(); - } - - /** - * Returns the largest sample timestamp that has been queued since the last {@link #reset}. - *

- * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not - * considered as having been queued. Samples that were dequeued from the front of the queue are - * considered as having been queued. - * - * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no - * samples have been queued. - */ - public long getLargestQueuedTimestampUs() { - return infoQueue.getLargestQueuedTimestampUs(); - } - - /** - * Skips all samples currently in the buffer. - */ - public void skipAll() { - long nextOffset = infoQueue.skipAll(); - if (nextOffset != C.POSITION_UNSET) { - dropDownstreamTo(nextOffset); - } - } - - /** - * Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer - * contains a keyframe with a timestamp of {@code timeUs} or earlier. If - * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs} - * falls within the buffer. - * - * @param timeUs The seek time. - * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end - * of the buffer. - * @return Whether the skip was successful. - */ - public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { - long nextOffset = infoQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer); - if (nextOffset == C.POSITION_UNSET) { - return false; - } - dropDownstreamTo(nextOffset); - return true; - } - - /** - * Attempts to read from the queue. - * - * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. - * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the - * end of the stream. If the end of the stream has been reached, the - * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. - * @param formatRequired Whether the caller requires that the format of the stream be read even if - * it's not changing. A sample will never be read if set to true, however it is still possible - * for the end of stream or nothing to be read. - * @param loadingFinished True if an empty queue should be considered the end of the stream. - * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will - * be set if the buffer's timestamp is less than this value. - * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or - * {@link C#RESULT_BUFFER_READ}. - */ - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, - boolean loadingFinished, long decodeOnlyUntilUs) { - int result = infoQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, - downstreamFormat, extrasHolder); - switch (result) { - case C.RESULT_FORMAT_READ: - downstreamFormat = formatHolder.format; - return C.RESULT_FORMAT_READ; - case C.RESULT_BUFFER_READ: - if (!buffer.isEndOfStream()) { - if (buffer.timeUs < decodeOnlyUntilUs) { - buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); - } - // Read encryption data if the sample is encrypted. - if (buffer.isEncrypted()) { - readEncryptionData(buffer, extrasHolder); - } - // Write the sample data into the holder. - buffer.ensureSpaceForWrite(extrasHolder.size); - readData(extrasHolder.offset, buffer.data, extrasHolder.size); - // Advance the read head. - dropDownstreamTo(extrasHolder.nextOffset); - } - return C.RESULT_BUFFER_READ; - case C.RESULT_NOTHING_READ: - return C.RESULT_NOTHING_READ; - default: - throw new IllegalStateException(); - } - } - - /** - * Reads encryption data for the current sample. - *

- * The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and - * {@link BufferExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The - * same value is added to {@link BufferExtrasHolder#offset}. - * - * @param buffer The buffer into which the encryption data should be written. - * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. - */ - private void readEncryptionData(DecoderInputBuffer buffer, BufferExtrasHolder extrasHolder) { - long offset = extrasHolder.offset; - - // Read the signal byte. - scratch.reset(1); - readData(offset, scratch.data, 1); - offset++; - byte signalByte = scratch.data[0]; - boolean subsampleEncryption = (signalByte & 0x80) != 0; - int ivSize = signalByte & 0x7F; - - // Read the initialization vector. - if (buffer.cryptoInfo.iv == null) { - buffer.cryptoInfo.iv = new byte[16]; - } - readData(offset, buffer.cryptoInfo.iv, ivSize); - offset += ivSize; - - // Read the subsample count, if present. - int subsampleCount; - if (subsampleEncryption) { - scratch.reset(2); - readData(offset, scratch.data, 2); - offset += 2; - subsampleCount = scratch.readUnsignedShort(); - } else { - subsampleCount = 1; - } - - // Write the clear and encrypted subsample sizes. - int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData; - if (clearDataSizes == null || clearDataSizes.length < subsampleCount) { - clearDataSizes = new int[subsampleCount]; - } - int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData; - if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) { - encryptedDataSizes = new int[subsampleCount]; - } - if (subsampleEncryption) { - int subsampleDataLength = 6 * subsampleCount; - scratch.reset(subsampleDataLength); - readData(offset, scratch.data, subsampleDataLength); - offset += subsampleDataLength; - scratch.setPosition(0); - for (int i = 0; i < subsampleCount; i++) { - clearDataSizes[i] = scratch.readUnsignedShort(); - encryptedDataSizes[i] = scratch.readUnsignedIntToInt(); - } - } else { - clearDataSizes[0] = 0; - encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset); - } - - // Populate the cryptoInfo. - buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes, - extrasHolder.encryptionKeyId, buffer.cryptoInfo.iv, C.CRYPTO_MODE_AES_CTR); - - // Adjust the offset and size to take into account the bytes read. - int bytesRead = (int) (offset - extrasHolder.offset); - extrasHolder.offset += bytesRead; - extrasHolder.size -= bytesRead; - } - - /** - * Reads data from the front of the rolling buffer. - * - * @param absolutePosition The absolute position from which data should be read. - * @param target The buffer into which data should be written. - * @param length The number of bytes to read. - */ - private void readData(long absolutePosition, ByteBuffer target, int length) { - int remaining = length; - while (remaining > 0) { - dropDownstreamTo(absolutePosition); - int positionInAllocation = (int) (absolutePosition - totalBytesDropped); - int toCopy = Math.min(remaining, allocationLength - positionInAllocation); - Allocation allocation = dataQueue.peek(); - target.put(allocation.data, allocation.translateOffset(positionInAllocation), toCopy); - absolutePosition += toCopy; - remaining -= toCopy; - } - } - - /** - * Reads data from the front of the rolling buffer. - * - * @param absolutePosition The absolute position from which data should be read. - * @param target The array into which data should be written. - * @param length The number of bytes to read. - */ - private void readData(long absolutePosition, byte[] target, int length) { - int bytesRead = 0; - while (bytesRead < length) { - dropDownstreamTo(absolutePosition); - int positionInAllocation = (int) (absolutePosition - totalBytesDropped); - int toCopy = Math.min(length - bytesRead, allocationLength - positionInAllocation); - Allocation allocation = dataQueue.peek(); - System.arraycopy(allocation.data, allocation.translateOffset(positionInAllocation), target, - bytesRead, toCopy); - absolutePosition += toCopy; - bytesRead += toCopy; - } - } - - /** - * Discard any allocations that hold data prior to the specified absolute position, returning - * them to the allocator. - * - * @param absolutePosition The absolute position up to which allocations can be discarded. - */ - private void dropDownstreamTo(long absolutePosition) { - int relativePosition = (int) (absolutePosition - totalBytesDropped); - int allocationIndex = relativePosition / allocationLength; - for (int i = 0; i < allocationIndex; i++) { - allocator.release(dataQueue.remove()); - totalBytesDropped += allocationLength; - } - } - - // Called by the loading thread. - - /** - * Sets a listener to be notified of changes to the upstream format. - * - * @param listener The listener. - */ - public void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) { - upstreamFormatChangeListener = listener; - } - - /** - * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples - * subsequently queued to the buffer. - * - * @param sampleOffsetUs The timestamp offset in microseconds. - */ - public void setSampleOffsetUs(long sampleOffsetUs) { - if (this.sampleOffsetUs != sampleOffsetUs) { - this.sampleOffsetUs = sampleOffsetUs; - pendingFormatAdjustment = true; - } - } - - @Override - public void format(Format format) { - Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs); - boolean formatChanged = infoQueue.format(adjustedFormat); - lastUnadjustedFormat = format; - pendingFormatAdjustment = false; - if (upstreamFormatChangeListener != null && formatChanged) { - upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat); - } - } - - @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { - if (!startWriteOperation()) { - int bytesSkipped = input.skip(length); - if (bytesSkipped == C.RESULT_END_OF_INPUT) { - if (allowEndOfInput) { - return C.RESULT_END_OF_INPUT; - } - throw new EOFException(); - } - return bytesSkipped; - } - try { - length = prepareForAppend(length); - int bytesAppended = input.read(lastAllocation.data, - lastAllocation.translateOffset(lastAllocationOffset), length); - if (bytesAppended == C.RESULT_END_OF_INPUT) { - if (allowEndOfInput) { - return C.RESULT_END_OF_INPUT; - } - throw new EOFException(); - } - lastAllocationOffset += bytesAppended; - totalBytesWritten += bytesAppended; - return bytesAppended; - } finally { - endWriteOperation(); - } - } - - @Override - public void sampleData(ParsableByteArray buffer, int length) { - if (!startWriteOperation()) { - buffer.skipBytes(length); - return; - } - while (length > 0) { - int thisAppendLength = prepareForAppend(length); - buffer.readBytes(lastAllocation.data, lastAllocation.translateOffset(lastAllocationOffset), - thisAppendLength); - lastAllocationOffset += thisAppendLength; - totalBytesWritten += thisAppendLength; - length -= thisAppendLength; - } - endWriteOperation(); - } - - @Override - public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { - if (pendingFormatAdjustment) { - format(lastUnadjustedFormat); - } - if (!startWriteOperation()) { - infoQueue.commitSampleTimestamp(timeUs); - return; - } - try { - if (pendingSplice) { - if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !infoQueue.attemptSplice(timeUs)) { - return; - } - pendingSplice = false; - } - timeUs += sampleOffsetUs; - long absoluteOffset = totalBytesWritten - size - offset; - infoQueue.commitSample(timeUs, flags, absoluteOffset, size, encryptionKey); - } finally { - endWriteOperation(); - } - } - - // Private methods. - - private boolean startWriteOperation() { - return state.compareAndSet(STATE_ENABLED, STATE_ENABLED_WRITING); - } - - private void endWriteOperation() { - if (!state.compareAndSet(STATE_ENABLED_WRITING, STATE_ENABLED)) { - clearSampleData(); - } - } - - private void clearSampleData() { - infoQueue.clearSampleData(); - allocator.release(dataQueue.toArray(new Allocation[dataQueue.size()])); - dataQueue.clear(); - allocator.trim(); - totalBytesDropped = 0; - totalBytesWritten = 0; - lastAllocation = null; - lastAllocationOffset = allocationLength; - } - - /** - * Prepares the rolling sample buffer for an append of up to {@code length} bytes, returning the - * number of bytes that can actually be appended. - */ - private int prepareForAppend(int length) { - if (lastAllocationOffset == allocationLength) { - lastAllocationOffset = 0; - lastAllocation = allocator.allocate(); - dataQueue.add(lastAllocation); - } - return Math.min(length, allocationLength - lastAllocationOffset); - } - - /** - * Adjusts a {@link Format} to incorporate a sample offset into {@link Format#subsampleOffsetUs}. - * - * @param format The {@link Format} to adjust. - * @param sampleOffsetUs The offset to apply. - * @return The adjusted {@link Format}. - */ - private static Format getAdjustedSampleFormat(Format format, long sampleOffsetUs) { - if (format == null) { - return null; - } - if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) { - format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs); - } - return format; - } - - /** - * Holds information about the samples in the rolling buffer. - */ - private static final class InfoQueue { - - private static final int SAMPLE_CAPACITY_INCREMENT = 1000; - - private int capacity; - - private int[] sourceIds; - private long[] offsets; - private int[] sizes; - private int[] flags; - private long[] timesUs; - private byte[][] encryptionKeys; - private Format[] formats; - - private int queueSize; - private int absoluteReadIndex; - private int relativeReadIndex; - private int relativeWriteIndex; - - private long largestDequeuedTimestampUs; - private long largestQueuedTimestampUs; - private boolean upstreamKeyframeRequired; - private boolean upstreamFormatRequired; - private Format upstreamFormat; - private int upstreamSourceId; - - public InfoQueue() { - capacity = SAMPLE_CAPACITY_INCREMENT; - sourceIds = new int[capacity]; - offsets = new long[capacity]; - timesUs = new long[capacity]; - flags = new int[capacity]; - sizes = new int[capacity]; - encryptionKeys = new byte[capacity][]; - formats = new Format[capacity]; - largestDequeuedTimestampUs = Long.MIN_VALUE; - largestQueuedTimestampUs = Long.MIN_VALUE; - upstreamFormatRequired = true; - upstreamKeyframeRequired = true; - } - - public void clearSampleData() { - absoluteReadIndex = 0; - relativeReadIndex = 0; - relativeWriteIndex = 0; - queueSize = 0; - upstreamKeyframeRequired = true; - } - - // Called by the consuming thread, but only when there is no loading thread. - - public void resetLargestParsedTimestamps() { - largestDequeuedTimestampUs = Long.MIN_VALUE; - largestQueuedTimestampUs = Long.MIN_VALUE; - } - - /** - * Returns the current absolute write index. - */ - public int getWriteIndex() { - return absoluteReadIndex + queueSize; - } - - /** - * Discards samples from the write side of the buffer. - * - * @param discardFromIndex The absolute index of the first sample to be discarded. - * @return The reduced total number of bytes written, after the samples have been discarded. - */ - public long discardUpstreamSamples(int discardFromIndex) { - int discardCount = getWriteIndex() - discardFromIndex; - Assertions.checkArgument(0 <= discardCount && discardCount <= queueSize); - - if (discardCount == 0) { - if (absoluteReadIndex == 0) { - // queueSize == absoluteReadIndex == 0, so nothing has been written to the queue. - return 0; - } - int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1; - return offsets[lastWriteIndex] + sizes[lastWriteIndex]; - } - - queueSize -= discardCount; - relativeWriteIndex = (relativeWriteIndex + capacity - discardCount) % capacity; - // Update the largest queued timestamp, assuming that the timestamps prior to a keyframe are - // always less than the timestamp of the keyframe itself, and of subsequent frames. - largestQueuedTimestampUs = Long.MIN_VALUE; - for (int i = queueSize - 1; i >= 0; i--) { - int sampleIndex = (relativeReadIndex + i) % capacity; - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timesUs[sampleIndex]); - if ((flags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { - break; - } - } - return offsets[relativeWriteIndex]; - } - - public void sourceId(int sourceId) { - upstreamSourceId = sourceId; - } - - // Called by the consuming thread. - - /** - * Returns the current absolute read index. - */ - public int getReadIndex() { - return absoluteReadIndex; - } - - /** - * Peeks the source id of the next sample, or the current upstream source id if the queue is - * empty. - */ - public int peekSourceId() { - return queueSize == 0 ? upstreamSourceId : sourceIds[relativeReadIndex]; - } - - /** - * Returns whether the queue is empty. - */ - public synchronized boolean isEmpty() { - return queueSize == 0; - } - - /** - * Returns the upstream {@link Format} in which samples are being queued. - */ - public synchronized Format getUpstreamFormat() { - return upstreamFormatRequired ? null : upstreamFormat; - } - - /** - * Returns the largest sample timestamp that has been queued since the last {@link #reset}. - *

- * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not - * considered as having been queued. Samples that were dequeued from the front of the queue are - * considered as having been queued. - * - * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no - * samples have been queued. - */ - public synchronized long getLargestQueuedTimestampUs() { - return Math.max(largestDequeuedTimestampUs, largestQueuedTimestampUs); - } - - /** - * Attempts to read from the queue. - * - * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. - * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the - * end of the stream. If a sample is read then the buffer is populated with information - * about the sample, but not its data. The size and absolute position of the data in the - * rolling buffer is stored in {@code extrasHolder}, along with an encryption id if present - * and the absolute position of the first byte that may still be required after the current - * sample has been read. May be null if the caller requires that the format of the stream be - * read even if it's not changing. - * @param formatRequired Whether the caller requires that the format of the stream be read even - * if it's not changing. A sample will never be read if set to true, however it is still - * possible for the end of stream or nothing to be read. - * @param loadingFinished True if an empty queue should be considered the end of the stream. - * @param downstreamFormat The current downstream {@link Format}. If the format of the next - * sample is different to the current downstream format then a format will be read. - * @param extrasHolder The holder into which extra sample information should be written. - * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} - * or {@link C#RESULT_BUFFER_READ}. - */ - @SuppressWarnings("ReferenceEquality") - public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, - boolean formatRequired, boolean loadingFinished, Format downstreamFormat, - BufferExtrasHolder extrasHolder) { - if (queueSize == 0) { - if (loadingFinished) { - buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); - return C.RESULT_BUFFER_READ; - } else if (upstreamFormat != null - && (formatRequired || upstreamFormat != downstreamFormat)) { - formatHolder.format = upstreamFormat; - return C.RESULT_FORMAT_READ; - } else { - return C.RESULT_NOTHING_READ; - } - } - - if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { - formatHolder.format = formats[relativeReadIndex]; - return C.RESULT_FORMAT_READ; - } - - if (buffer.isFlagsOnly()) { - return C.RESULT_NOTHING_READ; - } - - buffer.timeUs = timesUs[relativeReadIndex]; - buffer.setFlags(flags[relativeReadIndex]); - extrasHolder.size = sizes[relativeReadIndex]; - extrasHolder.offset = offsets[relativeReadIndex]; - extrasHolder.encryptionKeyId = encryptionKeys[relativeReadIndex]; - - largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, buffer.timeUs); - queueSize--; - relativeReadIndex++; - absoluteReadIndex++; - if (relativeReadIndex == capacity) { - // Wrap around. - relativeReadIndex = 0; - } - - extrasHolder.nextOffset = queueSize > 0 ? offsets[relativeReadIndex] - : extrasHolder.offset + extrasHolder.size; - return C.RESULT_BUFFER_READ; - } - - /** - * Skips all samples in the buffer. - * - * @return The offset up to which data should be dropped, or {@link C#POSITION_UNSET} if no - * dropping of data is required. - */ - public synchronized long skipAll() { - if (queueSize == 0) { - return C.POSITION_UNSET; - } - - int lastSampleIndex = (relativeReadIndex + queueSize - 1) % capacity; - relativeReadIndex = (relativeReadIndex + queueSize) % capacity; - absoluteReadIndex += queueSize; - queueSize = 0; - return offsets[lastSampleIndex] + sizes[lastSampleIndex]; - } - - /** - * Attempts to locate the keyframe before or at the specified time. If - * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs} - * falls within the buffer. - * - * @param timeUs The seek time. - * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end - * of the buffer. - * @return The offset of the keyframe's data if the keyframe was present. - * {@link C#POSITION_UNSET} otherwise. - */ - public synchronized long skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { - if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) { - return C.POSITION_UNSET; - } - - if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) { - return C.POSITION_UNSET; - } - - // This could be optimized to use a binary search, however in practice callers to this method - // often pass times near to the start of the buffer. Hence it's unclear whether switching to - // a binary search would yield any real benefit. - int sampleCount = 0; - int sampleCountToKeyframe = -1; - int searchIndex = relativeReadIndex; - while (searchIndex != relativeWriteIndex) { - if (timesUs[searchIndex] > timeUs) { - // We've gone too far. - break; - } else if ((flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { - // We've found a keyframe, and we're still before the seek position. - sampleCountToKeyframe = sampleCount; - } - searchIndex = (searchIndex + 1) % capacity; - sampleCount++; - } - - if (sampleCountToKeyframe == -1) { - return C.POSITION_UNSET; - } - - relativeReadIndex = (relativeReadIndex + sampleCountToKeyframe) % capacity; - absoluteReadIndex += sampleCountToKeyframe; - queueSize -= sampleCountToKeyframe; - return offsets[relativeReadIndex]; - } - - // Called by the loading thread. - - public synchronized boolean format(Format format) { - if (format == null) { - upstreamFormatRequired = true; - return false; - } - upstreamFormatRequired = false; - if (Util.areEqual(format, upstreamFormat)) { - // Suppress changes between equal formats so we can use referential equality in readData. - return false; - } else { - upstreamFormat = format; - return true; - } - } - - public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset, - int size, byte[] encryptionKey) { - if (upstreamKeyframeRequired) { - if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) { - return; - } - upstreamKeyframeRequired = false; - } - Assertions.checkState(!upstreamFormatRequired); - commitSampleTimestamp(timeUs); - timesUs[relativeWriteIndex] = timeUs; - offsets[relativeWriteIndex] = offset; - sizes[relativeWriteIndex] = size; - flags[relativeWriteIndex] = sampleFlags; - encryptionKeys[relativeWriteIndex] = encryptionKey; - formats[relativeWriteIndex] = upstreamFormat; - sourceIds[relativeWriteIndex] = upstreamSourceId; - // Increment the write index. - queueSize++; - if (queueSize == capacity) { - // Increase the capacity. - int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT; - int[] newSourceIds = new int[newCapacity]; - long[] newOffsets = new long[newCapacity]; - long[] newTimesUs = new long[newCapacity]; - int[] newFlags = new int[newCapacity]; - int[] newSizes = new int[newCapacity]; - byte[][] newEncryptionKeys = new byte[newCapacity][]; - Format[] newFormats = new Format[newCapacity]; - int beforeWrap = capacity - relativeReadIndex; - System.arraycopy(offsets, relativeReadIndex, newOffsets, 0, beforeWrap); - System.arraycopy(timesUs, relativeReadIndex, newTimesUs, 0, beforeWrap); - System.arraycopy(flags, relativeReadIndex, newFlags, 0, beforeWrap); - System.arraycopy(sizes, relativeReadIndex, newSizes, 0, beforeWrap); - System.arraycopy(encryptionKeys, relativeReadIndex, newEncryptionKeys, 0, beforeWrap); - System.arraycopy(formats, relativeReadIndex, newFormats, 0, beforeWrap); - System.arraycopy(sourceIds, relativeReadIndex, newSourceIds, 0, beforeWrap); - int afterWrap = relativeReadIndex; - System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap); - System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap); - System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); - System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap); - System.arraycopy(encryptionKeys, 0, newEncryptionKeys, beforeWrap, afterWrap); - System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap); - System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap); - offsets = newOffsets; - timesUs = newTimesUs; - flags = newFlags; - sizes = newSizes; - encryptionKeys = newEncryptionKeys; - formats = newFormats; - sourceIds = newSourceIds; - relativeReadIndex = 0; - relativeWriteIndex = capacity; - queueSize = capacity; - capacity = newCapacity; - } else { - relativeWriteIndex++; - if (relativeWriteIndex == capacity) { - // Wrap around. - relativeWriteIndex = 0; - } - } - } - - public synchronized void commitSampleTimestamp(long timeUs) { - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); - } - - /** - * Attempts to discard samples from the tail of the queue to allow samples starting from the - * specified timestamp to be spliced in. - * - * @param timeUs The timestamp at which the splice occurs. - * @return Whether the splice was successful. - */ - public synchronized boolean attemptSplice(long timeUs) { - if (largestDequeuedTimestampUs >= timeUs) { - return false; - } - int retainCount = queueSize; - while (retainCount > 0 - && timesUs[(relativeReadIndex + retainCount - 1) % capacity] >= timeUs) { - retainCount--; - } - discardUpstreamSamples(absoluteReadIndex + retainCount); - return true; - } - - } - - /** - * Holds additional buffer information not held by {@link DecoderInputBuffer}. - */ - private static final class BufferExtrasHolder { - - public int size; - public long offset; - public long nextOffset; - public byte[] encryptionKeyId; - - } - -} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java new file mode 100644 index 000000000000..06c90ae8748a --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +/** A dummy {@link ExtractorOutput} implementation. */ +public final class DummyExtractorOutput implements ExtractorOutput { + + @Override + public TrackOutput track(int id, int type) { + return new DummyTrackOutput(); + } + + @Override + public void endTracks() { + // Do nothing. + } + + @Override + public void seekMap(SeekMap seekMap) { + // Do nothing. + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java index 9c7bcf72a93c..6df947731d41 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java @@ -15,6 +15,7 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; @@ -50,9 +51,12 @@ public final class DummyTrackOutput implements TrackOutput { } @Override - public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { + public void sampleMetadata( + long timeUs, + @C.BufferFlags int flags, + int size, + int offset, + @Nullable CryptoData cryptoData) { // Do nothing. } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Extractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Extractor.java index 5e6bbcef3429..aeb7028c3f7b 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Extractor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Extractor.java @@ -15,8 +15,12 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; +import androidx.annotation.IntDef; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Extracts media data from a container format. @@ -41,6 +45,15 @@ public interface Extractor { */ int RESULT_END_OF_INPUT = C.RESULT_END_OF_INPUT; + /** + * Result values that can be returned by {@link #read(ExtractorInput, PositionHolder)}. One of + * {@link #RESULT_CONTINUE}, {@link #RESULT_SEEK} or {@link #RESULT_END_OF_INPUT}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {RESULT_CONTINUE, RESULT_SEEK, RESULT_END_OF_INPUT}) + @interface ReadResult {} + /** * Returns whether this extractor can extract samples from the {@link ExtractorInput}, which must * provide data from the start of the stream. @@ -63,18 +76,23 @@ public interface Extractor { void init(ExtractorOutput output); /** - * Extracts data read from a provided {@link ExtractorInput}. - *

- * A single call to this method will block until some progress has been made, but will not block - * for longer than this. Hence each call will consume only a small amount of input data. - *

- * In the common case, {@link #RESULT_CONTINUE} is returned to indicate that the - * {@link ExtractorInput} passed to the next read is required to provide data continuing from the + * Extracts data read from a provided {@link ExtractorInput}. Must not be called before {@link + * #init(ExtractorOutput)}. + * + *

A single call to this method will block until some progress has been made, but will not + * block for longer than this. Hence each call will consume only a small amount of input data. + * + *

In the common case, {@link #RESULT_CONTINUE} is returned to indicate that the {@link + * ExtractorInput} passed to the next read is required to provide data continuing from the * position in the stream reached by the returning call. If the extractor requires data to be * provided from a different position, then that position is set in {@code seekPosition} and * {@link #RESULT_SEEK} is returned. If the extractor reached the end of the data provided by the * {@link ExtractorInput}, then {@link #RESULT_END_OF_INPUT} is returned. * + *

When this method throws an {@link IOException} or an {@link InterruptedException}, + * extraction may continue by providing an {@link ExtractorInput} with an unchanged {@link + * ExtractorInput#getPosition() read position} to a subsequent call to this method. + * * @param input The {@link ExtractorInput} from which data should be read. * @param seekPosition If {@link #RESULT_SEEK} is returned, this holder is updated to hold the * position of the required data. @@ -82,6 +100,7 @@ public interface Extractor { * @throws IOException If an error occurred reading from the input. * @throws InterruptedException If the thread was interrupted. */ + @ReadResult int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.java index a1ed7dfe3106..351df1e79e41 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.java @@ -18,9 +18,50 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import java.io.EOFException; import java.io.IOException; +import java.io.InputStream; /** * Provides data to be consumed by an {@link Extractor}. + * + *

This interface provides two modes of accessing the underlying input. See the subheadings below + * for more info about each mode. + * + *

    + *
  • The {@code read()/peek()} and {@code skip()} methods provide {@link InputStream}-like + * byte-level access operations. + *
  • The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user + * wants to read an entire block/frame/header of known length. + *
+ * + *

{@link InputStream}-like methods

+ * + *

The {@code read()/peek()} and {@code skip()} methods provide {@link InputStream}-like + * byte-level access operations. The {@code length} parameter is a maximum, and each method returns + * the number of bytes actually processed. This may be less than {@code length} because the end of + * the input was reached, or the method was interrupted, or the operation was aborted early for + * another reason. + * + *

Block-based methods

+ * + *

The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user + * wants to read an entire block/frame/header of known length. + * + *

These methods all have a variant that takes a boolean {@code allowEndOfInput} parameter. This + * parameter is intended to be set to true when the caller believes the input might be fully + * exhausted before the call is made (i.e. they've previously read/skipped/peeked the final + * block/frame/header). It's not intended to allow a partial read (i.e. greater than 0 bytes, + * but less than {@code length}) to succeed - this will always throw an {@link EOFException} from + * these methods (a partial read is assumed to indicate a malformed block/frame/header - and + * therefore a malformed file). + * + *

The expected behaviour of the block-based methods is therefore: + * + *

    + *
  • Already at end-of-input and {@code allowEndOfInput=false}: Throw {@link EOFException}. + *
  • Already at end-of-input and {@code allowEndOfInput=true}: Return {@code false}. + *
  • Encounter end-of-input during read/skip/peek/advance: Throw {@link EOFException} + * (regardless of {@code allowEndOfInput}). + *
*/ public interface ExtractorInput { @@ -41,22 +82,16 @@ public interface ExtractorInput { /** * Like {@link #read(byte[], int, int)}, but reads the requested {@code length} in full. - *

- * If the end of the input is found having read no data, then behavior is dependent on - * {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned. - * Otherwise an {@link EOFException} is thrown. - *

- * Encountering the end of input having partially satisfied the read is always considered an - * error, and will result in an {@link EOFException} being thrown. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. * @param length The number of bytes to read from the input. * @param allowEndOfInput True if encountering the end of the input having read no data is * allowed, and should result in {@code false} being returned. False if it should be - * considered an error, causing an {@link EOFException} to be thrown. - * @return True if the read was successful. False if the end of the input was encountered having - * read no data. + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the read was successful. False if {@code allowEndOfInput=true} and the end of + * the input was encountered having read no data. * @throws EOFException If the end of input was encountered having partially satisfied the read * (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were * read and {@code allowEndOfInput} is false. @@ -67,7 +102,8 @@ public interface ExtractorInput { throws IOException, InterruptedException; /** - * Equivalent to {@code readFully(target, offset, length, false)}. + * Equivalent to {@link #readFully(byte[], int, int, boolean) readFully(target, offset, length, + * false)}. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. @@ -94,9 +130,10 @@ public interface ExtractorInput { * @param length The number of bytes to skip from the input. * @param allowEndOfInput True if encountering the end of the input having skipped no data is * allowed, and should result in {@code false} being returned. False if it should be - * considered an error, causing an {@link EOFException} to be thrown. - * @return True if the skip was successful. False if the end of the input was encountered having - * skipped no data. + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the skip was successful. False if {@code allowEndOfInput=true} and the end of + * the input was encountered having skipped no data. * @throws EOFException If the end of input was encountered having partially satisfied the skip * (i.e. having skipped at least one byte, but fewer than {@code length}), or if no bytes were * skipped and {@code allowEndOfInput} is false. @@ -119,25 +156,37 @@ public interface ExtractorInput { void skipFully(int length) throws IOException, InterruptedException; /** - * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index - * {@code offset}. The current read position is left unchanged. - *

- * If the end of the input is found having peeked no data, then behavior is dependent on - * {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned. - * Otherwise an {@link EOFException} is thrown. - *

- * Calling {@link #resetPeekPosition()} resets the peek position to equal the current read + * Peeks up to {@code length} bytes from the peek position. The current read position is left + * unchanged. + * + *

This method blocks until at least one byte of data can be peeked, the end of the input is + * detected, or an exception is thrown. + * + *

Calling {@link #resetPeekPosition()} resets the peek position to equal the current read * position, so the caller can peek the same data again. Reading or skipping also resets the peek * position. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. + * @param length The maximum number of bytes to peek from the input. + * @return The number of bytes peeked, or {@link C#RESULT_END_OF_INPUT} if the input has ended. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + int peek(byte[] target, int offset, int length) throws IOException, InterruptedException; + + /** + * Like {@link #peek(byte[], int, int)}, but peeks the requested {@code length} in full. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. * @param length The number of bytes to peek from the input. * @param allowEndOfInput True if encountering the end of the input having peeked no data is * allowed, and should result in {@code false} being returned. False if it should be - * considered an error, causing an {@link EOFException} to be thrown. - * @return True if the peek was successful. False if the end of the input was encountered having - * peeked no data. + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the peek was successful. False if {@code allowEndOfInput=true} and the end of + * the input was encountered having peeked no data. * @throws EOFException If the end of input was encountered having partially satisfied the peek * (i.e. having peeked at least one byte, but fewer than {@code length}), or if no bytes were * peeked and {@code allowEndOfInput} is false. @@ -148,12 +197,8 @@ public interface ExtractorInput { throws IOException, InterruptedException; /** - * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index - * {@code offset}. The current read position is left unchanged. - *

- * Calling {@link #resetPeekPosition()} resets the peek position to equal the current read - * position, so the caller can peek the same data again. Reading and skipping also reset the peek - * position. + * Equivalent to {@link #peekFully(byte[], int, int, boolean) peekFully(target, offset, length, + * false)}. * * @param target A target array into which data should be written. * @param offset The offset into the target array at which to write. @@ -165,18 +210,16 @@ public interface ExtractorInput { void peekFully(byte[] target, int offset, int length) throws IOException, InterruptedException; /** - * Advances the peek position by {@code length} bytes. - *

- * If the end of the input is encountered before advancing the peek position, then behavior is - * dependent on {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is - * returned. Otherwise an {@link EOFException} is thrown. + * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int, + * boolean)} except the data is skipped instead of read. * * @param length The number of bytes by which to advance the peek position. * @param allowEndOfInput True if encountering the end of the input before advancing is allowed, * and should result in {@code false} being returned. False if it should be considered an - * error, causing an {@link EOFException} to be thrown. - * @return True if advancing the peek position was successful. False if the end of the input was - * encountered before the peek position could be advanced. + * error, causing an {@link EOFException} to be thrown. See note in class Javadoc. + * @return True if advancing the peek position was successful. False if {@code + * allowEndOfInput=true} and the end of the input was encountered before advancing over any + * data. * @throws EOFException If the end of input was encountered having partially advanced (i.e. having * advanced by at least one byte, but fewer than {@code length}), or if the end of input was * encountered before advancing and {@code allowEndOfInput} is false. @@ -187,7 +230,8 @@ public interface ExtractorInput { throws IOException, InterruptedException; /** - * Advances the peek position by {@code length} bytes. + * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int)} + * except the data is skipped instead of read. * * @param length The number of bytes to peek from the input. * @throws EOFException If the end of input was encountered. diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.java index 609160466545..870875826546 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.java @@ -26,7 +26,7 @@ public interface ExtractorOutput { * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}. * * @param id A track identifier. - * @param type The type of the track. Typically one of the {@link org.mozilla.thirdparty.com.google.android.exoplayer2.C} + * @param type The type of the track. Typically one of the {@link org.mozilla.thirdparty.com.google.android.exoplayer2C} * {@code TRACK_TYPE_*} constants. * @return The {@link TrackOutput} for the given track identifier. */ diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorUtil.java new file mode 100644 index 000000000000..6951f7e3111c --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorUtil.java @@ -0,0 +1,52 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import java.io.IOException; + +/** Extractor related utility methods. */ +/* package */ final class ExtractorUtil { + + /** + * Peeks {@code length} bytes from the input peek position, or all the bytes to the end of the + * input if there was less than {@code length} bytes left. + * + *

If an exception is thrown, there is no guarantee on the peek position. + * + * @param input The stream input to peek the data from. + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The maximum number of bytes to peek from the input. + * @return The number of bytes peeked. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + public static int peekToLength(ExtractorInput input, byte[] target, int offset, int length) + throws IOException, InterruptedException { + int totalBytesPeeked = 0; + while (totalBytesPeeked < length) { + int bytesPeeked = input.peek(target, offset + totalBytesPeeked, length - totalBytesPeeked); + if (bytesPeeked == C.RESULT_END_OF_INPUT) { + break; + } + totalBytesPeeked += bytesPeeked; + } + return totalBytesPeeked; + } + + private ExtractorUtil() {} +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.java index f6fe161ef5e5..64b803f65e63 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.java @@ -15,14 +15,9 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; -/** - * Factory for arrays of {@link Extractor}s. - */ +/** Factory for arrays of {@link Extractor} instances. */ public interface ExtractorsFactory { - /** - * Returns an array of new {@link Extractor} instances. - */ + /** Returns an array of new {@link Extractor} instances. */ Extractor[] createExtractors(); - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacFrameReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacFrameReader.java new file mode 100644 index 000000000000..e8d2b4928b0c --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacFrameReader.java @@ -0,0 +1,336 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * Reads and peeks FLAC frame elements according to the FLAC format specification. + */ +public final class FlacFrameReader { + + /** Holds a sample number. */ + public static final class SampleNumberHolder { + /** The sample number. */ + public long sampleNumber; + } + + /** + * Checks whether the given FLAC frame header is valid and, if so, reads it and writes the frame + * first sample number in {@code sampleNumberHolder}. + * + *

If the header is valid, the position of {@code data} is moved to the byte following it. + * Otherwise, there is no guarantee on the position. + * + * @param data The array to read the data from, whose position must correspond to the frame + * header. + * @param flacStreamMetadata The stream metadata. + * @param frameStartMarker The frame start marker of the stream. + * @param sampleNumberHolder The holder used to contain the sample number. + * @return Whether the frame header is valid. + */ + public static boolean checkAndReadFrameHeader( + ParsableByteArray data, + FlacStreamMetadata flacStreamMetadata, + int frameStartMarker, + SampleNumberHolder sampleNumberHolder) { + int frameStartPosition = data.getPosition(); + + long frameHeaderBytes = data.readUnsignedInt(); + if (frameHeaderBytes >>> 16 != frameStartMarker) { + return false; + } + + boolean isBlockSizeVariable = (frameHeaderBytes >>> 16 & 1) == 1; + int blockSizeKey = (int) (frameHeaderBytes >> 12 & 0xF); + int sampleRateKey = (int) (frameHeaderBytes >> 8 & 0xF); + int channelAssignmentKey = (int) (frameHeaderBytes >> 4 & 0xF); + int bitsPerSampleKey = (int) (frameHeaderBytes >> 1 & 0x7); + boolean reservedBit = (frameHeaderBytes & 1) == 1; + return checkChannelAssignment(channelAssignmentKey, flacStreamMetadata) + && checkBitsPerSample(bitsPerSampleKey, flacStreamMetadata) + && !reservedBit + && checkAndReadFirstSampleNumber( + data, flacStreamMetadata, isBlockSizeVariable, sampleNumberHolder) + && checkAndReadBlockSizeSamples(data, flacStreamMetadata, blockSizeKey) + && checkAndReadSampleRate(data, flacStreamMetadata, sampleRateKey) + && checkAndReadCrc(data, frameStartPosition); + } + + /** + * Checks whether the given FLAC frame header is valid and, if so, writes the frame first sample + * number in {@code sampleNumberHolder}. + * + *

The {@code input} peek position is left unchanged. + * + * @param input The input to get the data from, whose peek position must correspond to the frame + * header. + * @param flacStreamMetadata The stream metadata. + * @param frameStartMarker The frame start marker of the stream. + * @param sampleNumberHolder The holder used to contain the sample number. + * @return Whether the frame header is valid. + */ + public static boolean checkFrameHeaderFromPeek( + ExtractorInput input, + FlacStreamMetadata flacStreamMetadata, + int frameStartMarker, + SampleNumberHolder sampleNumberHolder) + throws IOException, InterruptedException { + long originalPeekPosition = input.getPeekPosition(); + + byte[] frameStartBytes = new byte[2]; + input.peekFully(frameStartBytes, 0, 2); + int frameStart = (frameStartBytes[0] & 0xFF) << 8 | (frameStartBytes[1] & 0xFF); + if (frameStart != frameStartMarker) { + input.resetPeekPosition(); + input.advancePeekPosition((int) (originalPeekPosition - input.getPosition())); + return false; + } + + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + System.arraycopy( + frameStartBytes, /* srcPos= */ 0, scratch.data, /* destPos= */ 0, /* length= */ 2); + + int totalBytesPeeked = + ExtractorUtil.peekToLength(input, scratch.data, 2, FlacConstants.MAX_FRAME_HEADER_SIZE - 2); + scratch.setLimit(totalBytesPeeked); + + input.resetPeekPosition(); + input.advancePeekPosition((int) (originalPeekPosition - input.getPosition())); + + return checkAndReadFrameHeader( + scratch, flacStreamMetadata, frameStartMarker, sampleNumberHolder); + } + + /** + * Returns the number of the first sample in the given frame. + * + *

The read position of {@code input} is left unchanged. + * + *

If no exception is thrown, the peek position is aligned with the read position. Otherwise, + * there is no guarantee on the peek position. + * + * @param input Input stream to get the sample number from (starting from the read position). + * @return The frame first sample number. + * @throws ParserException If an error occurs parsing the sample number. + * @throws IOException If peeking from the input fails. + * @throws InterruptedException If interrupted while peeking from input. + */ + public static long getFirstSampleNumber( + ExtractorInput input, FlacStreamMetadata flacStreamMetadata) + throws IOException, InterruptedException { + input.resetPeekPosition(); + input.advancePeekPosition(1); + byte[] blockingStrategyByte = new byte[1]; + input.peekFully(blockingStrategyByte, 0, 1); + boolean isBlockSizeVariable = (blockingStrategyByte[0] & 1) == 1; + input.advancePeekPosition(2); + + int maxUtf8SampleNumberSize = isBlockSizeVariable ? 7 : 6; + ParsableByteArray scratch = new ParsableByteArray(maxUtf8SampleNumberSize); + int totalBytesPeeked = + ExtractorUtil.peekToLength(input, scratch.data, 0, maxUtf8SampleNumberSize); + scratch.setLimit(totalBytesPeeked); + input.resetPeekPosition(); + + SampleNumberHolder sampleNumberHolder = new SampleNumberHolder(); + if (!checkAndReadFirstSampleNumber( + scratch, flacStreamMetadata, isBlockSizeVariable, sampleNumberHolder)) { + throw new ParserException(); + } + + return sampleNumberHolder.sampleNumber; + } + + /** + * Reads the given block size. + * + * @param data The array to read the data from, whose position must correspond to the block size + * bits. + * @param blockSizeKey The key in the block size lookup table. + * @return The block size in samples, or -1 if the {@code blockSizeKey} is invalid. + */ + public static int readFrameBlockSizeSamplesFromKey(ParsableByteArray data, int blockSizeKey) { + switch (blockSizeKey) { + case 1: + return 192; + case 2: + case 3: + case 4: + case 5: + return 576 << (blockSizeKey - 2); + case 6: + return data.readUnsignedByte() + 1; + case 7: + return data.readUnsignedShort() + 1; + case 8: + case 9: + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + return 256 << (blockSizeKey - 8); + default: + return -1; + } + } + + /** + * Checks whether the given channel assignment is valid. + * + * @param channelAssignmentKey The channel assignment lookup key. + * @param flacStreamMetadata The stream metadata. + * @return Whether the channel assignment is valid. + */ + private static boolean checkChannelAssignment( + int channelAssignmentKey, FlacStreamMetadata flacStreamMetadata) { + if (channelAssignmentKey <= 7) { + return channelAssignmentKey == flacStreamMetadata.channels - 1; + } else if (channelAssignmentKey <= 10) { + return flacStreamMetadata.channels == 2; + } else { + return false; + } + } + + /** + * Checks whether the given number of bits per sample is valid. + * + * @param bitsPerSampleKey The bits per sample lookup key. + * @param flacStreamMetadata The stream metadata. + * @return Whether the number of bits per sample is valid. + */ + private static boolean checkBitsPerSample( + int bitsPerSampleKey, FlacStreamMetadata flacStreamMetadata) { + if (bitsPerSampleKey == 0) { + return true; + } + return bitsPerSampleKey == flacStreamMetadata.bitsPerSampleLookupKey; + } + + /** + * Checks whether the given sample number is valid and, if so, reads it and writes it in {@code + * sampleNumberHolder}. + * + *

If the sample number is valid, the position of {@code data} is moved to the byte following + * it. Otherwise, there is no guarantee on the position. + * + * @param data The array to read the data from, whose position must correspond to the sample + * number data. + * @param flacStreamMetadata The stream metadata. + * @param isBlockSizeVariable Whether the stream blocking strategy is variable block size or fixed + * block size. + * @param sampleNumberHolder The holder used to contain the sample number. + * @return Whether the sample number is valid. + */ + private static boolean checkAndReadFirstSampleNumber( + ParsableByteArray data, + FlacStreamMetadata flacStreamMetadata, + boolean isBlockSizeVariable, + SampleNumberHolder sampleNumberHolder) { + long utf8Value; + try { + utf8Value = data.readUtf8EncodedLong(); + } catch (NumberFormatException e) { + return false; + } + + sampleNumberHolder.sampleNumber = + isBlockSizeVariable ? utf8Value : utf8Value * flacStreamMetadata.maxBlockSizeSamples; + return true; + } + + /** + * Checks whether the given frame block size key and block size bits are valid and, if so, reads + * the block size bits. + * + *

If the block size is valid, the position of {@code data} is moved to the byte following the + * block size bits. Otherwise, there is no guarantee on the position. + * + * @param data The array to read the data from, whose position must correspond to the block size + * bits. + * @param flacStreamMetadata The stream metadata. + * @param blockSizeKey The key in the block size lookup table. + * @return Whether the block size is valid. + */ + private static boolean checkAndReadBlockSizeSamples( + ParsableByteArray data, FlacStreamMetadata flacStreamMetadata, int blockSizeKey) { + int blockSizeSamples = readFrameBlockSizeSamplesFromKey(data, blockSizeKey); + return blockSizeSamples != -1 && blockSizeSamples <= flacStreamMetadata.maxBlockSizeSamples; + } + + /** + * Checks whether the given sample rate key and sample rate bits are valid and, if so, reads the + * sample rate bits. + * + *

If the sample rate is valid, the position of {@code data} is moved to the byte following the + * sample rate bits. Otherwise, there is no guarantee on the position. + * + * @param data The array to read the data from, whose position must indicate the sample rate bits. + * @param flacStreamMetadata The stream metadata. + * @param sampleRateKey The key in the sample rate lookup table. + * @return Whether the sample rate is valid. + */ + private static boolean checkAndReadSampleRate( + ParsableByteArray data, FlacStreamMetadata flacStreamMetadata, int sampleRateKey) { + int expectedSampleRate = flacStreamMetadata.sampleRate; + if (sampleRateKey == 0) { + return true; + } else if (sampleRateKey <= 11) { + return sampleRateKey == flacStreamMetadata.sampleRateLookupKey; + } else if (sampleRateKey == 12) { + return data.readUnsignedByte() * 1000 == expectedSampleRate; + } else if (sampleRateKey <= 14) { + int sampleRate = data.readUnsignedShort(); + if (sampleRateKey == 14) { + sampleRate *= 10; + } + return sampleRate == expectedSampleRate; + } else { + return false; + } + } + + /** + * Checks whether the given CRC is valid and, if so, reads it. + * + *

If the CRC is valid, the position of {@code data} is moved to the byte following it. + * Otherwise, there is no guarantee on the position. + * + *

The {@code data} array must contain the whole frame header. + * + * @param data The array to read the data from, whose position must indicate the CRC. + * @param frameStartPosition The frame start offset in {@code data}. + * @return Whether the CRC is valid. + */ + private static boolean checkAndReadCrc(ParsableByteArray data, int frameStartPosition) { + int crc = data.readUnsignedByte(); + int frameEndPosition = data.getPosition(); + int expectedCrc = + Util.crc8(data.data, frameStartPosition, frameEndPosition - 1, /* initialValue= */ 0); + return crc == expectedCrc; + } + + private FlacFrameReader() {} +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java new file mode 100644 index 000000000000..c5413cf45966 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java @@ -0,0 +1,312 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.VorbisUtil.CommentHeader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.flac.PictureFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Reads and peeks FLAC stream metadata elements according to the FLAC format specification. + */ +public final class FlacMetadataReader { + + /** Holds a {@link FlacStreamMetadata}. */ + public static final class FlacStreamMetadataHolder { + /** The FLAC stream metadata. */ + @Nullable public FlacStreamMetadata flacStreamMetadata; + + public FlacStreamMetadataHolder(@Nullable FlacStreamMetadata flacStreamMetadata) { + this.flacStreamMetadata = flacStreamMetadata; + } + } + + private static final int STREAM_MARKER = 0x664C6143; // ASCII for "fLaC" + private static final int SYNC_CODE = 0x3FFE; + private static final int SEEK_POINT_SIZE = 18; + + /** + * Peeks ID3 Data. + * + * @param input Input stream to peek the ID3 data from. + * @param parseData Whether to parse the ID3 frames. + * @return The parsed ID3 data, or {@code null} if there is no such data or if {@code parseData} + * is {@code false}. + * @throws IOException If peeking from the input fails. In this case, there is no guarantee on the + * peek position. + * @throws InterruptedException If interrupted while peeking from input. In this case, there is no + * guarantee on the peek position. + */ + @Nullable + public static Metadata peekId3Metadata(ExtractorInput input, boolean parseData) + throws IOException, InterruptedException { + @Nullable + Id3Decoder.FramePredicate id3FramePredicate = parseData ? null : Id3Decoder.NO_FRAMES_PREDICATE; + @Nullable Metadata id3Metadata = new Id3Peeker().peekId3Data(input, id3FramePredicate); + return id3Metadata == null || id3Metadata.length() == 0 ? null : id3Metadata; + } + + /** + * Peeks the FLAC stream marker. + * + * @param input Input stream to peek the stream marker from. + * @return Whether the data peeked is the FLAC stream marker. + * @throws IOException If peeking from the input fails. In this case, the peek position is left + * unchanged. + * @throws InterruptedException If interrupted while peeking from input. In this case, the peek + * position is left unchanged. + */ + public static boolean checkAndPeekStreamMarker(ExtractorInput input) + throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE); + input.peekFully(scratch.data, 0, FlacConstants.STREAM_MARKER_SIZE); + return scratch.readUnsignedInt() == STREAM_MARKER; + } + + /** + * Reads ID3 Data. + * + *

If no exception is thrown, the peek position of {@code input} is aligned with the read + * position. + * + * @param input Input stream to read the ID3 data from. + * @param parseData Whether to parse the ID3 frames. + * @return The parsed ID3 data, or {@code null} if there is no such data or if {@code parseData} + * is {@code false}. + * @throws IOException If reading from the input fails. In this case, the read position is left + * unchanged and there is no guarantee on the peek position. + * @throws InterruptedException If interrupted while reading from input. In this case, the read + * position is left unchanged and there is no guarantee on the peek position. + */ + @Nullable + public static Metadata readId3Metadata(ExtractorInput input, boolean parseData) + throws IOException, InterruptedException { + input.resetPeekPosition(); + long startingPeekPosition = input.getPeekPosition(); + @Nullable Metadata id3Metadata = peekId3Metadata(input, parseData); + int peekedId3Bytes = (int) (input.getPeekPosition() - startingPeekPosition); + input.skipFully(peekedId3Bytes); + return id3Metadata; + } + + /** + * Reads the FLAC stream marker. + * + * @param input Input stream to read the stream marker from. + * @throws ParserException If an error occurs parsing the stream marker. In this case, the + * position of {@code input} is advanced by {@link FlacConstants#STREAM_MARKER_SIZE} bytes. + * @throws IOException If reading from the input fails. In this case, the position is left + * unchanged. + * @throws InterruptedException If interrupted while reading from input. In this case, the + * position is left unchanged. + */ + public static void readStreamMarker(ExtractorInput input) + throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE); + input.readFully(scratch.data, 0, FlacConstants.STREAM_MARKER_SIZE); + if (scratch.readUnsignedInt() != STREAM_MARKER) { + throw new ParserException("Failed to read FLAC stream marker."); + } + } + + /** + * Reads one FLAC metadata block. + * + *

If no exception is thrown, the peek position of {@code input} is aligned with the read + * position. + * + * @param input Input stream to read the metadata block from (header included). + * @param metadataHolder A holder for the metadata read. If the stream info block (which must be + * the first metadata block) is read, the holder contains a new instance representing the + * stream info data. If the block read is a Vorbis comment block or a picture block, the + * holder contains a copy of the existing stream metadata with the corresponding metadata + * added. Otherwise, the metadata in the holder is unchanged. + * @return Whether the block read is the last metadata block. + * @throws IllegalArgumentException If the block read is not a stream info block and the metadata + * in {@code metadataHolder} is {@code null}. In this case, the read position will be at the + * start of a metadata block and there is no guarantee on the peek position. + * @throws IOException If reading from the input fails. In this case, the read position will be at + * the start of a metadata block and there is no guarantee on the peek position. + * @throws InterruptedException If interrupted while reading from input. In this case, the read + * position will be at the start of a metadata block and there is no guarantee on the peek + * position. + */ + public static boolean readMetadataBlock( + ExtractorInput input, FlacStreamMetadataHolder metadataHolder) + throws IOException, InterruptedException { + input.resetPeekPosition(); + ParsableBitArray scratch = new ParsableBitArray(new byte[4]); + input.peekFully(scratch.data, 0, FlacConstants.METADATA_BLOCK_HEADER_SIZE); + + boolean isLastMetadataBlock = scratch.readBit(); + int type = scratch.readBits(7); + int length = FlacConstants.METADATA_BLOCK_HEADER_SIZE + scratch.readBits(24); + if (type == FlacConstants.METADATA_TYPE_STREAM_INFO) { + metadataHolder.flacStreamMetadata = readStreamInfoBlock(input); + } else { + FlacStreamMetadata flacStreamMetadata = metadataHolder.flacStreamMetadata; + if (flacStreamMetadata == null) { + throw new IllegalArgumentException(); + } + if (type == FlacConstants.METADATA_TYPE_SEEK_TABLE) { + FlacStreamMetadata.SeekTable seekTable = readSeekTableMetadataBlock(input, length); + metadataHolder.flacStreamMetadata = flacStreamMetadata.copyWithSeekTable(seekTable); + } else if (type == FlacConstants.METADATA_TYPE_VORBIS_COMMENT) { + List vorbisComments = readVorbisCommentMetadataBlock(input, length); + metadataHolder.flacStreamMetadata = + flacStreamMetadata.copyWithVorbisComments(vorbisComments); + } else if (type == FlacConstants.METADATA_TYPE_PICTURE) { + PictureFrame pictureFrame = readPictureMetadataBlock(input, length); + metadataHolder.flacStreamMetadata = + flacStreamMetadata.copyWithPictureFrames(Collections.singletonList(pictureFrame)); + } else { + input.skipFully(length); + } + } + + return isLastMetadataBlock; + } + + /** + * Reads a FLAC seek table metadata block. + * + *

The position of {@code data} is moved to the byte following the seek table metadata block + * (placeholder points included). + * + * @param data The array to read the data from, whose position must correspond to the seek table + * metadata block (header included). + * @return The seek table, without the placeholder points. + */ + public static FlacStreamMetadata.SeekTable readSeekTableMetadataBlock(ParsableByteArray data) { + data.skipBytes(1); + int length = data.readUnsignedInt24(); + + long seekTableEndPosition = data.getPosition() + length; + int seekPointCount = length / SEEK_POINT_SIZE; + long[] pointSampleNumbers = new long[seekPointCount]; + long[] pointOffsets = new long[seekPointCount]; + for (int i = 0; i < seekPointCount; i++) { + // The sample number is expected to fit in a signed long, except if it is a placeholder, in + // which case its value is -1. + long sampleNumber = data.readLong(); + if (sampleNumber == -1) { + pointSampleNumbers = Arrays.copyOf(pointSampleNumbers, i); + pointOffsets = Arrays.copyOf(pointOffsets, i); + break; + } + pointSampleNumbers[i] = sampleNumber; + pointOffsets[i] = data.readLong(); + data.skipBytes(2); + } + + data.skipBytes((int) (seekTableEndPosition - data.getPosition())); + return new FlacStreamMetadata.SeekTable(pointSampleNumbers, pointOffsets); + } + + /** + * Returns the frame start marker, consisting of the 2 first bytes of the first frame. + * + *

The read position of {@code input} is left unchanged and the peek position is aligned with + * the read position. + * + * @param input Input stream to get the start marker from (starting from the read position). + * @return The frame start marker (which must be the same for all the frames in the stream). + * @throws ParserException If an error occurs parsing the frame start marker. + * @throws IOException If peeking from the input fails. + * @throws InterruptedException If interrupted while peeking from input. + */ + public static int getFrameStartMarker(ExtractorInput input) + throws IOException, InterruptedException { + input.resetPeekPosition(); + ParsableByteArray scratch = new ParsableByteArray(2); + input.peekFully(scratch.data, 0, 2); + + int frameStartMarker = scratch.readUnsignedShort(); + int syncCode = frameStartMarker >> 2; + if (syncCode != SYNC_CODE) { + input.resetPeekPosition(); + throw new ParserException("First frame does not start with sync code."); + } + + input.resetPeekPosition(); + return frameStartMarker; + } + + private static FlacStreamMetadata readStreamInfoBlock(ExtractorInput input) + throws IOException, InterruptedException { + byte[] scratchData = new byte[FlacConstants.STREAM_INFO_BLOCK_SIZE]; + input.readFully(scratchData, 0, FlacConstants.STREAM_INFO_BLOCK_SIZE); + return new FlacStreamMetadata( + scratchData, /* offset= */ FlacConstants.METADATA_BLOCK_HEADER_SIZE); + } + + private static FlacStreamMetadata.SeekTable readSeekTableMetadataBlock( + ExtractorInput input, int length) throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(length); + input.readFully(scratch.data, 0, length); + return readSeekTableMetadataBlock(scratch); + } + + private static List readVorbisCommentMetadataBlock(ExtractorInput input, int length) + throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(length); + input.readFully(scratch.data, 0, length); + scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE); + CommentHeader commentHeader = + VorbisUtil.readVorbisCommentHeader( + scratch, /* hasMetadataHeader= */ false, /* hasFramingBit= */ false); + return Arrays.asList(commentHeader.comments); + } + + private static PictureFrame readPictureMetadataBlock(ExtractorInput input, int length) + throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(length); + input.readFully(scratch.data, 0, length); + scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE); + + int pictureType = scratch.readInt(); + int mimeTypeLength = scratch.readInt(); + String mimeType = scratch.readString(mimeTypeLength, Charset.forName(C.ASCII_NAME)); + int descriptionLength = scratch.readInt(); + String description = scratch.readString(descriptionLength); + int width = scratch.readInt(); + int height = scratch.readInt(); + int depth = scratch.readInt(); + int colors = scratch.readInt(); + int pictureDataLength = scratch.readInt(); + byte[] pictureData = new byte[pictureDataLength]; + scratch.readBytes(pictureData, 0, pictureDataLength); + + return new PictureFrame( + pictureType, mimeType, description, width, height, depth, colors, pictureData); + } + + private FlacMetadataReader() {} +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java new file mode 100644 index 000000000000..56d54596ac4f --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java @@ -0,0 +1,84 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * A {@link SeekMap} implementation for FLAC streams that contain a seek table. + */ +public final class FlacSeekTableSeekMap implements SeekMap { + + private final FlacStreamMetadata flacStreamMetadata; + private final long firstFrameOffset; + + /** + * Creates a seek map from the FLAC stream seek table. + * + * @param flacStreamMetadata The stream metadata. + * @param firstFrameOffset The byte offset of the first frame in the stream. + */ + public FlacSeekTableSeekMap(FlacStreamMetadata flacStreamMetadata, long firstFrameOffset) { + this.flacStreamMetadata = flacStreamMetadata; + this.firstFrameOffset = firstFrameOffset; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getDurationUs() { + return flacStreamMetadata.getDurationUs(); + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + Assertions.checkNotNull(flacStreamMetadata.seekTable); + long[] pointSampleNumbers = flacStreamMetadata.seekTable.pointSampleNumbers; + long[] pointOffsets = flacStreamMetadata.seekTable.pointOffsets; + + long targetSampleNumber = flacStreamMetadata.getSampleNumber(timeUs); + int index = + Util.binarySearchFloor( + pointSampleNumbers, + targetSampleNumber, + /* inclusive= */ true, + /* stayInBounds= */ false); + + long seekPointSampleNumber = index == -1 ? 0 : pointSampleNumbers[index]; + long seekPointOffsetFromFirstFrame = index == -1 ? 0 : pointOffsets[index]; + SeekPoint seekPoint = getSeekPoint(seekPointSampleNumber, seekPointOffsetFromFirstFrame); + if (seekPoint.timeUs == timeUs || index == pointSampleNumbers.length - 1) { + return new SeekPoints(seekPoint); + } else { + SeekPoint secondSeekPoint = + getSeekPoint(pointSampleNumbers[index + 1], pointOffsets[index + 1]); + return new SeekPoints(seekPoint, secondSeekPoint); + } + } + + private SeekPoint getSeekPoint(long sampleNumber, long offsetFromFirstFrame) { + long seekTimeUs = sampleNumber * C.MICROS_PER_SECOND / flacStreamMetadata.sampleRate; + long seekPosition = firstFrameOffset + offsetFromFirstFrame; + return new SeekPoint(seekTimeUs, seekPosition); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java index e29872a2f2a7..11893d6136f0 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -18,7 +18,7 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.CommentFrame; -import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.InternalFrame; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -27,19 +27,8 @@ import java.util.regex.Pattern; */ public final class GaplessInfoHolder { - /** - * A {@link FramePredicate} suitable for use when decoding {@link Metadata} that will be passed - * to {@link #setFromMetadata(Metadata)}. Only frames that might contain gapless playback - * information are decoded. - */ - public static final FramePredicate GAPLESS_INFO_ID3_FRAME_PREDICATE = new FramePredicate() { - @Override - public boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3) { - return id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2); - } - }; - - private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; + private static final String GAPLESS_DOMAIN = "com.apple.iTunes"; + private static final String GAPLESS_DESCRIPTION = "iTunSMPB"; private static final Pattern GAPLESS_COMMENT_PATTERN = Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); @@ -91,7 +80,15 @@ public final class GaplessInfoHolder { Metadata.Entry entry = metadata.get(i); if (entry instanceof CommentFrame) { CommentFrame commentFrame = (CommentFrame) entry; - if (setFromComment(commentFrame.description, commentFrame.text)) { + if (GAPLESS_DESCRIPTION.equals(commentFrame.description) + && setFromComment(commentFrame.text)) { + return true; + } + } else if (entry instanceof InternalFrame) { + InternalFrame internalFrame = (InternalFrame) entry; + if (GAPLESS_DOMAIN.equals(internalFrame.domain) + && GAPLESS_DESCRIPTION.equals(internalFrame.description) + && setFromComment(internalFrame.text)) { return true; } } @@ -103,14 +100,10 @@ public final class GaplessInfoHolder { * Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header * or MPEG 4 user data), if valid and non-zero. * - * @param name The comment's identifier. * @param data The comment's payload data. * @return Whether the holder was populated. */ - private boolean setFromComment(String name, String data) { - if (!GAPLESS_COMMENT_ID.equals(name)) { - return false; - } + private boolean setFromComment(String data) { Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); if (matcher.find()) { try { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Id3Peeker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Id3Peeker.java new file mode 100644 index 000000000000..a0a26c76d851 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Id3Peeker.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; +import java.io.IOException; + +/** + * Peeks data from the beginning of an {@link ExtractorInput} to determine if there is any ID3 tag. + */ +public final class Id3Peeker { + + private final ParsableByteArray scratch; + + public Id3Peeker() { + scratch = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); + } + + /** + * Peeks ID3 data from the input and parses the first ID3 tag. + * + * @param input The {@link ExtractorInput} from which data should be peeked. + * @param id3FramePredicate Determines which ID3 frames are decoded. May be null to decode all + * frames. + * @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not + * present in the input. + * @throws IOException If an error occurred peeking from the input. + * @throws InterruptedException If the thread was interrupted. + */ + @Nullable + public Metadata peekId3Data( + ExtractorInput input, @Nullable Id3Decoder.FramePredicate id3FramePredicate) + throws IOException, InterruptedException { + int peekedId3Bytes = 0; + Metadata metadata = null; + while (true) { + try { + input.peekFully(scratch.data, /* offset= */ 0, Id3Decoder.ID3_HEADER_LENGTH); + } catch (EOFException e) { + // If input has less than ID3_HEADER_LENGTH, ignore the rest. + break; + } + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) { + // Not an ID3 tag. + break; + } + scratch.skipBytes(3); // Skip major version, minor version and flags. + int framesLength = scratch.readSynchSafeInt(); + int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength; + + if (metadata == null) { + byte[] id3Data = new byte[tagLength]; + System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH); + input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength); + + metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength); + } else { + input.advancePeekPosition(framesLength); + } + + peekedId3Bytes += tagLength; + } + + input.resetPeekPosition(); + input.advancePeekPosition(peekedId3Bytes); + return metadata; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.java index 8bf8a966dc5f..66c3411094a1 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.java @@ -15,6 +15,7 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; @@ -34,23 +35,38 @@ public final class MpegAudioHeader { private static final String[] MIME_TYPE_BY_LAYER = new String[] {MimeTypes.AUDIO_MPEG_L1, MimeTypes.AUDIO_MPEG_L2, MimeTypes.AUDIO_MPEG}; private static final int[] SAMPLING_RATE_V1 = {44100, 48000, 32000}; - private static final int[] BITRATE_V1_L1 = - {32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448}; - private static final int[] BITRATE_V2_L1 = - {32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256}; - private static final int[] BITRATE_V1_L2 = - {32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384}; - private static final int[] BITRATE_V1_L3 = - {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320}; - private static final int[] BITRATE_V2 = - {8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}; + private static final int[] BITRATE_V1_L1 = { + 32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000, 384000, + 416000, 448000 + }; + private static final int[] BITRATE_V2_L1 = { + 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000, 192000, + 224000, 256000 + }; + private static final int[] BITRATE_V1_L2 = { + 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, + 320000, 384000 + }; + private static final int[] BITRATE_V1_L3 = { + 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, + 320000 + }; + private static final int[] BITRATE_V2 = { + 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, + 160000 + }; + + private static final int SAMPLES_PER_FRAME_L1 = 384; + private static final int SAMPLES_PER_FRAME_L2 = 1152; + private static final int SAMPLES_PER_FRAME_L3_V1 = 1152; + private static final int SAMPLES_PER_FRAME_L3_V2 = 576; /** * Returns the size of the frame associated with {@code header}, or {@link C#LENGTH_UNSET} if it * is invalid. */ public static int getFrameSize(int header) { - if ((header & 0xFFE00000) != 0xFFE00000) { + if (!isMagicPresent(header)) { return C.LENGTH_UNSET; } @@ -89,7 +105,7 @@ public final class MpegAudioHeader { if (layer == 3) { // Layer I (layer == 3) bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; - return (12000 * bitrate / samplingRate + padding) * 4; + return (12 * bitrate / samplingRate + padding) * 4; } else { // Layer II (layer == 2) or III (layer == 1) if (version == 3) { @@ -102,13 +118,43 @@ public final class MpegAudioHeader { if (version == 3) { // Version 1 - return 144000 * bitrate / samplingRate + padding; + return 144 * bitrate / samplingRate + padding; } else { // Version 2 or 2.5 - return (layer == 1 ? 72000 : 144000) * bitrate / samplingRate + padding; + return (layer == 1 ? 72 : 144) * bitrate / samplingRate + padding; } } + /** + * Returns the number of samples per frame associated with {@code header}, or {@link + * C#LENGTH_UNSET} if it is invalid. + */ + public static int getFrameSampleCount(int header) { + + if (!isMagicPresent(header)) { + return C.LENGTH_UNSET; + } + + int version = (header >>> 19) & 3; + if (version == 1) { + return C.LENGTH_UNSET; + } + + int layer = (header >>> 17) & 3; + if (layer == 0) { + return C.LENGTH_UNSET; + } + + // Those header values are not used but are checked for consistency with the other methods + int bitrateIndex = (header >>> 12) & 15; + int samplingRateIndex = (header >>> 10) & 3; + if (bitrateIndex == 0 || bitrateIndex == 0xF || samplingRateIndex == 3) { + return C.LENGTH_UNSET; + } + + return getFrameSizeInSamples(version, layer); + } + /** * Parses {@code headerData}, populating {@code header} with the parsed data. * @@ -118,7 +164,7 @@ public final class MpegAudioHeader { * is not a valid MPEG audio header. */ public static boolean populateHeader(int headerData, MpegAudioHeader header) { - if ((headerData & 0xFFE00000) != 0xFFE00000) { + if (!isMagicPresent(headerData)) { return false; } @@ -153,38 +199,52 @@ public final class MpegAudioHeader { } int padding = (headerData >>> 9) & 1; - int bitrate, frameSize, samplesPerFrame; + int bitrate; + int frameSize; + int samplesPerFrame = getFrameSizeInSamples(version, layer); if (layer == 3) { // Layer I (layer == 3) bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; - frameSize = (12000 * bitrate / sampleRate + padding) * 4; - samplesPerFrame = 384; + frameSize = (12 * bitrate / sampleRate + padding) * 4; } else { // Layer II (layer == 2) or III (layer == 1) if (version == 3) { // Version 1 bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1]; - samplesPerFrame = 1152; - frameSize = 144000 * bitrate / sampleRate + padding; + frameSize = 144 * bitrate / sampleRate + padding; } else { // Version 2 or 2.5. bitrate = BITRATE_V2[bitrateIndex - 1]; - samplesPerFrame = layer == 1 ? 576 : 1152; - frameSize = (layer == 1 ? 72000 : 144000) * bitrate / sampleRate + padding; + frameSize = (layer == 1 ? 72 : 144) * bitrate / sampleRate + padding; } } String mimeType = MIME_TYPE_BY_LAYER[3 - layer]; int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2; - header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate * 1000, - samplesPerFrame); + header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame); return true; } + private static boolean isMagicPresent(int header) { + return (header & 0xFFE00000) == 0xFFE00000; + } + + private static int getFrameSizeInSamples(int version, int layer) { + switch (layer) { + case 1: + return version == 3 ? SAMPLES_PER_FRAME_L3_V1 : SAMPLES_PER_FRAME_L3_V2; // Layer III + case 2: + return SAMPLES_PER_FRAME_L2; // Layer II + case 3: + return SAMPLES_PER_FRAME_L1; // Layer I + } + throw new IllegalArgumentException(); + } + /** MPEG audio header version. */ public int version; /** The mime type. */ - public String mimeType; + @Nullable public String mimeType; /** Size of the frame associated with this header, in bytes. */ public int frameSize; /** Sample rate in samples per second. */ @@ -196,8 +256,14 @@ public final class MpegAudioHeader { /** Number of samples stored in the frame. */ public int samplesPerFrame; - private void setValues(int version, String mimeType, int frameSize, int sampleRate, int channels, - int bitrate, int samplesPerFrame) { + private void setValues( + int version, + String mimeType, + int frameSize, + int sampleRate, + int channels, + int bitrate, + int samplesPerFrame) { this.version = version; this.mimeType = mimeType; this.frameSize = frameSize; @@ -206,5 +272,4 @@ public final class MpegAudioHeader { this.bitrate = bitrate; this.samplesPerFrame = samplesPerFrame; } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.java index e2bf6ba5ab7a..b3ccad214de9 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.java @@ -15,26 +15,38 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; /** * Maps seek positions (in microseconds) to corresponding positions (byte offsets) in the stream. */ public interface SeekMap { - /** - * A {@link SeekMap} that does not support seeking. - */ - final class Unseekable implements SeekMap { + /** A {@link SeekMap} that does not support seeking. */ + class Unseekable implements SeekMap { private final long durationUs; + private final SeekPoints startSeekPoints; /** - * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if - * the duration is unknown. + * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the + * duration is unknown. */ public Unseekable(long durationUs) { + this(durationUs, 0); + } + + /** + * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the + * duration is unknown. + * @param startPosition The position (byte offset) of the start of the media. + */ + public Unseekable(long durationUs, long startPosition) { this.durationUs = durationUs; + startSeekPoints = + new SeekPoints(startPosition == 0 ? SeekPoint.START : new SeekPoint(0, startPosition)); } @Override @@ -48,17 +60,58 @@ public interface SeekMap { } @Override - public long getPosition(long timeUs) { - return 0; + public SeekPoints getSeekPoints(long timeUs) { + return startSeekPoints; + } + } + + /** Contains one or two {@link SeekPoint}s. */ + final class SeekPoints { + + /** The first seek point. */ + public final SeekPoint first; + /** The second seek point, or {@link #first} if there's only one seek point. */ + public final SeekPoint second; + + /** @param point The single seek point. */ + public SeekPoints(SeekPoint point) { + this(point, point); } + /** + * @param first The first seek point. + * @param second The second seek point. + */ + public SeekPoints(SeekPoint first, SeekPoint second) { + this.first = Assertions.checkNotNull(first); + this.second = Assertions.checkNotNull(second); + } + + @Override + public String toString() { + return "[" + first + (first.equals(second) ? "" : (", " + second)) + "]"; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SeekPoints other = (SeekPoints) obj; + return first.equals(other.first) && second.equals(other.second); + } + + @Override + public int hashCode() { + return (31 * first.hashCode()) + second.hashCode(); + } } /** * Returns whether seeking is supported. - *

- * If seeking is not supported then the only valid seek position is the start of the file, and so - * {@link #getPosition(long)} will return 0 for all input values. * * @return Whether seeking is supported. */ @@ -67,19 +120,22 @@ public interface SeekMap { /** * Returns the duration of the stream in microseconds. * - * @return The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the - * duration is unknown. + * @return The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the duration is + * unknown. */ long getDurationUs(); /** - * Maps a seek position in microseconds to a corresponding position (byte offset) in the stream - * from which data can be provided to the extractor. + * Obtains seek points for the specified seek time in microseconds. The returned {@link + * SeekPoints} will contain one or two distinct seek points. * - * @param timeUs A seek position in microseconds. - * @return The corresponding position (byte offset) in the stream from which data can be provided - * to the extractor, or 0 if {@code #isSeekable()} returns false. + *

Two seek points [A, B] are returned in the case that seeking can only be performed to + * discrete points in time, there does not exist a seek point at exactly the requested time, and + * there exist seek points on both sides of it. In this case A and B are the closest seek points + * before and after the requested time. A single seek point is returned in all other cases. + * + * @param timeUs A seek time in microseconds. + * @return The corresponding seek points. */ - long getPosition(long timeUs); - + SeekPoints getSeekPoints(long timeUs); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekPoint.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekPoint.java new file mode 100644 index 000000000000..1c4db352036f --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekPoint.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; + +import androidx.annotation.Nullable; + +/** Defines a seek point in a media stream. */ +public final class SeekPoint { + + /** A {@link SeekPoint} whose time and byte offset are both set to 0. */ + public static final SeekPoint START = new SeekPoint(0, 0); + + /** The time of the seek point, in microseconds. */ + public final long timeUs; + + /** The byte offset of the seek point. */ + public final long position; + + /** + * @param timeUs The time of the seek point, in microseconds. + * @param position The byte offset of the seek point. + */ + public SeekPoint(long timeUs, long position) { + this.timeUs = timeUs; + this.position = position; + } + + @Override + public String toString() { + return "[timeUs=" + timeUs + ", position=" + position + "]"; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SeekPoint other = (SeekPoint) obj; + return timeUs == other.timeUs && position == other.position; + } + + @Override + public int hashCode() { + int result = (int) timeUs; + result = 31 * result + (int) position; + return result; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java index 81eeb1c6d68d..fd33bd6027d0 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java @@ -15,17 +15,84 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import java.io.EOFException; import java.io.IOException; +import java.util.Arrays; /** * Receives track level data extracted by an {@link Extractor}. */ public interface TrackOutput { + /** + * Holds data required to decrypt a sample. + */ + final class CryptoData { + + /** + * The encryption mode used for the sample. + */ + @C.CryptoMode public final int cryptoMode; + + /** + * The encryption key associated with the sample. Its contents must not be modified. + */ + public final byte[] encryptionKey; + + /** + * The number of encrypted blocks in the encryption pattern, 0 if pattern encryption does not + * apply. + */ + public final int encryptedBlocks; + + /** + * The number of clear blocks in the encryption pattern, 0 if pattern encryption does not + * apply. + */ + public final int clearBlocks; + + /** + * @param cryptoMode See {@link #cryptoMode}. + * @param encryptionKey See {@link #encryptionKey}. + * @param encryptedBlocks See {@link #encryptedBlocks}. + * @param clearBlocks See {@link #clearBlocks}. + */ + public CryptoData(@C.CryptoMode int cryptoMode, byte[] encryptionKey, int encryptedBlocks, + int clearBlocks) { + this.cryptoMode = cryptoMode; + this.encryptionKey = encryptionKey; + this.encryptedBlocks = encryptedBlocks; + this.clearBlocks = clearBlocks; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CryptoData other = (CryptoData) obj; + return cryptoMode == other.cryptoMode && encryptedBlocks == other.encryptedBlocks + && clearBlocks == other.clearBlocks && Arrays.equals(encryptionKey, other.encryptionKey); + } + + @Override + public int hashCode() { + int result = cryptoMode; + result = 31 * result + Arrays.hashCode(encryptionKey); + result = 31 * result + encryptedBlocks; + result = 31 * result + clearBlocks; + return result; + } + + } + /** * Called when the {@link Format} of the track has been extracted from the stream. * @@ -52,27 +119,29 @@ public interface TrackOutput { * Called to write sample data to the output. * * @param data A {@link ParsableByteArray} from which to read the sample data. - * @param length The number of bytes to read. + * @param length The number of bytes to read, starting from {@code data.getPosition()}. */ void sampleData(ParsableByteArray data, int length); /** * Called when metadata associated with a sample has been extracted from the stream. - *

- * The corresponding sample data will have already been passed to the output via calls to - * {@link #sampleData(ExtractorInput, int, boolean)} or - * {@link #sampleData(ParsableByteArray, int)}. + * + *

The corresponding sample data will have already been passed to the output via calls to + * {@link #sampleData(ExtractorInput, int, boolean)} or {@link #sampleData(ParsableByteArray, + * int)}. * * @param timeUs The media timestamp associated with the sample, in microseconds. * @param flags Flags associated with the sample. See {@code C.BUFFER_FLAG_*}. * @param size The size of the sample data, in bytes. - * @param offset The number of bytes that have been passed to - * {@link #sampleData(ExtractorInput, int, boolean)} or - * {@link #sampleData(ParsableByteArray, int)} since the last byte belonging to the sample - * whose metadata is being passed. - * @param encryptionKey The encryption key associated with the sample. May be null. + * @param offset The number of bytes that have been passed to {@link #sampleData(ExtractorInput, + * int, boolean)} or {@link #sampleData(ParsableByteArray, int)} since the last byte belonging + * to the sample whose metadata is being passed. + * @param encryptionData The encryption data required to decrypt the sample. May be null. */ - void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey); - + void sampleMetadata( + long timeUs, + @C.BufferFlags int flags, + int size, + int offset, + @Nullable CryptoData encryptionData); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisBitArray.java similarity index 56% rename from mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java rename to mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisBitArray.java index 272add46fa8c..4ea27c014915 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisBitArray.java @@ -13,20 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg; +package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; /** - * Wraps a byte array, providing methods that allow it to be read as a vorbis bitstream. + * Wraps a byte array, providing methods that allow it to be read as a Vorbis bitstream. * * @see Vorbis bitpacking * specification */ -/* package */ final class VorbisBitArray { +public final class VorbisBitArray { + + private final byte[] data; + private final int byteLimit; - public final byte[] data; - private final int limit; private int byteOffset; private int bitOffset; @@ -36,18 +37,8 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; * @param data the array to wrap. */ public VorbisBitArray(byte[] data) { - this(data, data.length); - } - - /** - * Creates a new instance that wraps an existing array. - * - * @param data the array to wrap. - * @param limit the limit in bytes. - */ - public VorbisBitArray(byte[] data, int limit) { this.data = data; - this.limit = limit * 8; + byteLimit = data.length; } /** @@ -64,7 +55,9 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; * @return {@code true} if the bit is set, {@code false} otherwise. */ public boolean readBit() { - return readBits(1) == 1; + boolean returnValue = (((data[byteOffset] & 0xFF) >> bitOffset) & 0x01) == 1; + skipBits(1); + return returnValue; } /** @@ -74,53 +67,32 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; * @return An integer whose bottom {@code numBits} bits hold the read data. */ public int readBits(int numBits) { - Assertions.checkState(getPosition() + numBits <= limit); - if (numBits == 0) { - return 0; + int tempByteOffset = byteOffset; + int bitsRead = Math.min(numBits, 8 - bitOffset); + int returnValue = ((data[tempByteOffset++] & 0xFF) >> bitOffset) & (0xFF >> (8 - bitsRead)); + while (bitsRead < numBits) { + returnValue |= (data[tempByteOffset++] & 0xFF) << bitsRead; + bitsRead += 8; } - int result = 0; - int bitCount = 0; - if (bitOffset != 0) { - bitCount = Math.min(numBits, 8 - bitOffset); - int mask = 0xFF >>> (8 - bitCount); - result = (data[byteOffset] >>> bitOffset) & mask; - bitOffset += bitCount; - if (bitOffset == 8) { - byteOffset++; - bitOffset = 0; - } - } - - if (numBits - bitCount > 7) { - int numBytes = (numBits - bitCount) / 8; - for (int i = 0; i < numBytes; i++) { - result |= (data[byteOffset++] & 0xFFL) << bitCount; - bitCount += 8; - } - } - - if (numBits > bitCount) { - int bitsOnNextByte = numBits - bitCount; - int mask = 0xFF >>> (8 - bitsOnNextByte); - result |= (data[byteOffset] & mask) << bitCount; - bitOffset += bitsOnNextByte; - } - return result; + returnValue &= 0xFFFFFFFF >>> (32 - numBits); + skipBits(numBits); + return returnValue; } /** * Skips {@code numberOfBits} bits. * - * @param numberOfBits The number of bits to skip. + * @param numBits The number of bits to skip. */ - public void skipBits(int numberOfBits) { - Assertions.checkState(getPosition() + numberOfBits <= limit); - byteOffset += numberOfBits / 8; - bitOffset += numberOfBits % 8; + public void skipBits(int numBits) { + int numBytes = numBits / 8; + byteOffset += numBytes; + bitOffset += numBits - (numBytes * 8); if (bitOffset > 7) { byteOffset++; bitOffset -= 8; } + assertValidOffset(); } /** @@ -136,23 +108,22 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; * @param position The new reading position in bits. */ public void setPosition(int position) { - Assertions.checkArgument(position < limit && position >= 0); byteOffset = position / 8; bitOffset = position - (byteOffset * 8); + assertValidOffset(); } /** * Returns the number of remaining bits. */ public int bitsLeft() { - return limit - getPosition(); + return (byteLimit - byteOffset) * 8 - bitOffset; } - /** - * Returns the limit in bits. - **/ - public int limit() { - return limit; + private void assertValidOffset() { + // It is fine for position to be at the end of the array, but no further. + Assertions.checkState(byteOffset >= 0 + && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0))); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisUtil.java similarity index 78% rename from mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java rename to mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisUtil.java index a9acf1b881b7..bdd3e13b996d 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisUtil.java @@ -13,17 +13,87 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg; +package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor; -import android.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import java.util.Arrays; -/** - * Utility methods for parsing vorbis streams. - */ -/* package */ final class VorbisUtil { +/** Utility methods for parsing Vorbis streams. */ +public final class VorbisUtil { + + /** Vorbis comment header. */ + public static final class CommentHeader { + + public final String vendor; + public final String[] comments; + public final int length; + + public CommentHeader(String vendor, String[] comments, int length) { + this.vendor = vendor; + this.comments = comments; + this.length = length; + } + } + + /** Vorbis identification header. */ + public static final class VorbisIdHeader { + + public final long version; + public final int channels; + public final long sampleRate; + public final int bitrateMax; + public final int bitrateNominal; + public final int bitrateMin; + public final int blockSize0; + public final int blockSize1; + public final boolean framingFlag; + public final byte[] data; + + public VorbisIdHeader( + long version, + int channels, + long sampleRate, + int bitrateMax, + int bitrateNominal, + int bitrateMin, + int blockSize0, + int blockSize1, + boolean framingFlag, + byte[] data) { + this.version = version; + this.channels = channels; + this.sampleRate = sampleRate; + this.bitrateMax = bitrateMax; + this.bitrateNominal = bitrateNominal; + this.bitrateMin = bitrateMin; + this.blockSize0 = blockSize0; + this.blockSize1 = blockSize1; + this.framingFlag = framingFlag; + this.data = data; + } + + public int getApproximateBitrate() { + return bitrateNominal == 0 ? (bitrateMin + bitrateMax) / 2 : bitrateNominal; + } + } + + /** Vorbis setup header modes. */ + public static final class Mode { + + public final boolean blockFlag; + public final int windowType; + public final int transformType; + public final int mapping; + + public Mode(boolean blockFlag, int windowType, int transformType, int mapping) { + this.blockFlag = blockFlag; + this.windowType = windowType; + this.transformType = transformType; + this.mapping = mapping; + } + } private static final String TAG = "VorbisUtil"; @@ -45,7 +115,7 @@ import java.util.Arrays; } /** - * Reads a vorbis identification header from {@code headerData}. + * Reads a Vorbis identification header from {@code headerData}. * * @see Vorbis * spec/Identification header @@ -70,7 +140,7 @@ import java.util.Arrays; int blockSize1 = (int) Math.pow(2, (blockSize & 0xF0) >> 4); boolean framingFlag = (headerData.readUnsignedByte() & 0x01) > 0; - // raw data of vorbis setup header has to be passed to decoder as CSD buffer #1 + // raw data of Vorbis setup header has to be passed to decoder as CSD buffer #1 byte[] data = Arrays.copyOf(headerData.data, headerData.limit()); return new VorbisIdHeader(version, channels, sampleRate, bitrateMax, bitrateNominal, bitrateMin, @@ -78,18 +148,41 @@ import java.util.Arrays; } /** - * Reads a vorbis comment header. + * Reads a Vorbis comment header. * - * @see - * Vorbis spec/Comment header - * @param headerData a {@link ParsableByteArray} wrapping the header data. - * @return a {@link VorbisUtil.CommentHeader} with all the comments. - * @throws ParserException thrown if invalid capture pattern is detected. + * @see Vorbis + * spec/Comment header + * @param headerData A {@link ParsableByteArray} wrapping the header data. + * @return A {@link VorbisUtil.CommentHeader} with all the comments. + * @throws ParserException If an error occurs parsing the comment header. */ public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData) throws ParserException { + return readVorbisCommentHeader( + headerData, /* hasMetadataHeader= */ true, /* hasFramingBit= */ true); + } - verifyVorbisHeaderCapturePattern(0x03, headerData, false); + /** + * Reads a Vorbis comment header. + * + *

The data provided may not contain the Vorbis metadata common header and the framing bit. + * + * @see Vorbis + * spec/Comment header + * @param headerData A {@link ParsableByteArray} wrapping the header data. + * @param hasMetadataHeader Whether the {@code headerData} contains a Vorbis metadata common + * header preceding the comment header. + * @param hasFramingBit Whether the {@code headerData} contains a framing bit. + * @return A {@link VorbisUtil.CommentHeader} with all the comments. + * @throws ParserException If an error occurs parsing the comment header. + */ + public static CommentHeader readVorbisCommentHeader( + ParsableByteArray headerData, boolean hasMetadataHeader, boolean hasFramingBit) + throws ParserException { + + if (hasMetadataHeader) { + verifyVorbisHeaderCapturePattern(/* headerType= */ 0x03, headerData, /* quiet= */ false); + } int length = 7; int len = (int) headerData.readLittleEndianUnsignedInt(); @@ -106,7 +199,7 @@ import java.util.Arrays; comments[i] = headerData.readString(len); length += comments[i].length(); } - if ((headerData.readUnsignedByte() & 0x01) == 0) { + if (hasFramingBit && (headerData.readUnsignedByte() & 0x01) == 0) { throw new ParserException("framing bit expected to be set"); } length += 1; @@ -114,8 +207,8 @@ import java.util.Arrays; } /** - * Verifies whether the next bytes in {@code header} are a vorbis header of the given - * {@code headerType}. + * Verifies whether the next bytes in {@code header} are a Vorbis header of the given {@code + * headerType}. * * @param headerType the type of the header expected. * @param header the alleged header bytes. @@ -123,9 +216,8 @@ import java.util.Arrays; * @return the number of bytes read. * @throws ParserException thrown if header type or capture pattern is not as expected. */ - public static boolean verifyVorbisHeaderCapturePattern(int headerType, ParsableByteArray header, - boolean quiet) - throws ParserException { + public static boolean verifyVorbisHeaderCapturePattern( + int headerType, ParsableByteArray header, boolean quiet) throws ParserException { if (header.bytesLeft() < 7) { if (quiet) { return false; @@ -158,12 +250,12 @@ import java.util.Arrays; } /** - * This method reads the modes which are located at the very end of the vorbis setup header. - * That's why we need to partially decode or at least read the entire setup header to know - * where to start reading the modes. + * This method reads the modes which are located at the very end of the Vorbis setup header. + * That's why we need to partially decode or at least read the entire setup header to know where + * to start reading the modes. * - * @see - * Vorbis spec/Setup header + * @see Vorbis + * spec/Setup header * @param headerData a {@link ParsableByteArray} containing setup header data. * @param channels the number of channels. * @return an array of {@link Mode}s. @@ -218,40 +310,38 @@ import java.util.Arrays; int mappingsCount = bitArray.readBits(6) + 1; for (int i = 0; i < mappingsCount; i++) { int mappingType = bitArray.readBits(16); - switch (mappingType) { - case 0: - int submaps; - if (bitArray.readBit()) { - submaps = bitArray.readBits(4) + 1; - } else { - submaps = 1; - } - int couplingSteps; - if (bitArray.readBit()) { - couplingSteps = bitArray.readBits(8) + 1; - for (int j = 0; j < couplingSteps; j++) { - bitArray.skipBits(iLog(channels - 1)); // magnitude - bitArray.skipBits(iLog(channels - 1)); // angle - } - } /*else { - couplingSteps = 0; - }*/ - if (bitArray.readBits(2) != 0x00) { - throw new ParserException("to reserved bits must be zero after mapping coupling steps"); - } - if (submaps > 1) { - for (int j = 0; j < channels; j++) { - bitArray.skipBits(4); // mappingMux - } - } - for (int j = 0; j < submaps; j++) { - bitArray.skipBits(8); // discard - bitArray.skipBits(8); // submapFloor - bitArray.skipBits(8); // submapResidue - } - break; - default: - Log.e(TAG, "mapping type other than 0 not supported: " + mappingType); + if (mappingType != 0) { + Log.e(TAG, "mapping type other than 0 not supported: " + mappingType); + continue; + } + int submaps; + if (bitArray.readBit()) { + submaps = bitArray.readBits(4) + 1; + } else { + submaps = 1; + } + int couplingSteps; + if (bitArray.readBit()) { + couplingSteps = bitArray.readBits(8) + 1; + for (int j = 0; j < couplingSteps; j++) { + bitArray.skipBits(iLog(channels - 1)); // magnitude + bitArray.skipBits(iLog(channels - 1)); // angle + } + } /*else { + couplingSteps = 0; + }*/ + if (bitArray.readBits(2) != 0x00) { + throw new ParserException("to reserved bits must be zero after mapping coupling steps"); + } + if (submaps > 1) { + for (int j = 0; j < channels; j++) { + bitArray.skipBits(4); // mappingMux + } + } + for (int j = 0; j < submaps; j++) { + bitArray.skipBits(8); // discard + bitArray.skipBits(8); // submapFloor + bitArray.skipBits(8); // submapResidue } } } @@ -357,12 +447,12 @@ import java.util.Arrays; for (int i = 0; i < lengthMap.length; i++) { if (isSparse) { if (bitArray.readBit()) { - lengthMap[i] = bitArray.readBits(5) + 1; + lengthMap[i] = (long) (bitArray.readBits(5) + 1); } else { // entry unused lengthMap[i] = 0; } } else { // not sparse - lengthMap[i] = bitArray.readBits(5) + 1; + lengthMap[i] = (long) (bitArray.readBits(5) + 1); } } } else { @@ -392,7 +482,7 @@ import java.util.Arrays; lookupValuesCount = 0; } } else { - lookupValuesCount = entries * dimensions; + lookupValuesCount = (long) entries * dimensions; } // discard (no decoding required yet) bitArray.skipBits((int) (lookupValuesCount * valueBits)); @@ -407,7 +497,11 @@ import java.util.Arrays; return (long) Math.floor(Math.pow(entries, 1.d / dimension)); } - public static final class CodeBook { + private VorbisUtil() { + // Prevent instantiation. + } + + private static final class CodeBook { public final int dimensions; public final int entries; @@ -425,69 +519,4 @@ import java.util.Arrays; } } - - public static final class CommentHeader { - - public final String vendor; - public final String[] comments; - public final int length; - - public CommentHeader(String vendor, String[] comments, int length) { - this.vendor = vendor; - this.comments = comments; - this.length = length; - } - - } - - public static final class VorbisIdHeader { - - public final long version; - public final int channels; - public final long sampleRate; - public final int bitrateMax; - public final int bitrateNominal; - public final int bitrateMin; - public final int blockSize0; - public final int blockSize1; - public final boolean framingFlag; - public final byte[] data; - - public VorbisIdHeader(long version, int channels, long sampleRate, int bitrateMax, - int bitrateNominal, int bitrateMin, int blockSize0, int blockSize1, boolean framingFlag, - byte[] data) { - this.version = version; - this.channels = channels; - this.sampleRate = sampleRate; - this.bitrateMax = bitrateMax; - this.bitrateNominal = bitrateNominal; - this.bitrateMin = bitrateMin; - this.blockSize0 = blockSize0; - this.blockSize1 = blockSize1; - this.framingFlag = framingFlag; - this.data = data; - } - - public int getApproximateBitrate() { - return bitrateNominal == 0 ? (bitrateMin + bitrateMax) / 2 : bitrateNominal; - } - - } - - public static final class Mode { - - public final boolean blockFlag; - public final int windowType; - public final int transformType; - public final int mapping; - - public Mode(boolean blockFlag, int windowType, int transformType, int mapping) { - this.blockFlag = blockFlag; - this.windowType = windowType; - this.transformType = transformType; - this.mapping = mapping; - } - - } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java new file mode 100644 index 000000000000..35f539a39451 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java @@ -0,0 +1,383 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.amr; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.EOFException; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; + +/** + * Extracts data from the AMR containers format (either AMR or AMR-WB). This follows RFC-4867, + * section 5. + * + *

This extractor only supports single-channel AMR container formats. + */ +public final class AmrExtractor implements Extractor { + + /** Factory for {@link AmrExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AmrExtractor()}; + + /** + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}) + public @interface Flags {} + /** + * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would + * otherwise not be possible. + */ + public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; + + /** + * The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR + * narrow band. + */ + private static final int[] frameSizeBytesByTypeNb = { + 13, + 14, + 16, + 18, + 20, + 21, + 27, + 32, + 6, // AMR SID + 7, // GSM-EFR SID + 6, // TDMA-EFR SID + 6, // PDC-EFR SID + 1, // Future use + 1, // Future use + 1, // Future use + 1 // No data + }; + + /** + * The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR wide + * band. + */ + private static final int[] frameSizeBytesByTypeWb = { + 18, + 24, + 33, + 37, + 41, + 47, + 51, + 59, + 61, + 6, // AMR-WB SID + 1, // Future use + 1, // Future use + 1, // Future use + 1, // Future use + 1, // speech lost + 1 // No data + }; + + private static final byte[] amrSignatureNb = Util.getUtf8Bytes("#!AMR\n"); + private static final byte[] amrSignatureWb = Util.getUtf8Bytes("#!AMR-WB\n"); + + /** Theoretical maximum frame size for a AMR frame. */ + private static final int MAX_FRAME_SIZE_BYTES = frameSizeBytesByTypeWb[8]; + /** + * The required number of samples in the stream with same sample size to classify the stream as a + * constant-bitrate-stream. + */ + private static final int NUM_SAME_SIZE_CONSTANT_BIT_RATE_THRESHOLD = 20; + + private static final int SAMPLE_RATE_WB = 16_000; + private static final int SAMPLE_RATE_NB = 8_000; + private static final int SAMPLE_TIME_PER_FRAME_US = 20_000; + + private final byte[] scratch; + private final @Flags int flags; + + private boolean isWideBand; + private long currentSampleTimeUs; + private int currentSampleSize; + private int currentSampleBytesRemaining; + private boolean hasOutputSeekMap; + private long firstSamplePosition; + private int firstSampleSize; + private int numSamplesWithSameSize; + private long timeOffsetUs; + + private ExtractorOutput extractorOutput; + private TrackOutput trackOutput; + @Nullable private SeekMap seekMap; + private boolean hasOutputFormat; + + public AmrExtractor() { + this(/* flags= */ 0); + } + + /** @param flags Flags that control the extractor's behavior. */ + public AmrExtractor(@Flags int flags) { + this.flags = flags; + scratch = new byte[1]; + firstSampleSize = C.LENGTH_UNSET; + } + + // Extractor implementation. + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return readAmrHeader(input); + } + + @Override + public void init(ExtractorOutput extractorOutput) { + this.extractorOutput = extractorOutput; + trackOutput = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_AUDIO); + extractorOutput.endTracks(); + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + if (input.getPosition() == 0) { + if (!readAmrHeader(input)) { + throw new ParserException("Could not find AMR header."); + } + } + maybeOutputFormat(); + int sampleReadResult = readSample(input); + maybeOutputSeekMap(input.getLength(), sampleReadResult); + return sampleReadResult; + } + + @Override + public void seek(long position, long timeUs) { + currentSampleTimeUs = 0; + currentSampleSize = 0; + currentSampleBytesRemaining = 0; + if (position != 0 && seekMap instanceof ConstantBitrateSeekMap) { + timeOffsetUs = ((ConstantBitrateSeekMap) seekMap).getTimeUsAtPosition(position); + } else { + timeOffsetUs = 0; + } + } + + @Override + public void release() { + // Do nothing + } + + /* package */ static int frameSizeBytesByTypeNb(int frameType) { + return frameSizeBytesByTypeNb[frameType]; + } + + /* package */ static int frameSizeBytesByTypeWb(int frameType) { + return frameSizeBytesByTypeWb[frameType]; + } + + /* package */ static byte[] amrSignatureNb() { + return Arrays.copyOf(amrSignatureNb, amrSignatureNb.length); + } + + /* package */ static byte[] amrSignatureWb() { + return Arrays.copyOf(amrSignatureWb, amrSignatureWb.length); + } + + // Internal methods. + + /** + * Peeks the AMR header from the beginning of the input, and consumes it if it exists. + * + * @param input The {@link ExtractorInput} from which data should be peeked/read. + * @return Whether the AMR header has been read. + */ + private boolean readAmrHeader(ExtractorInput input) throws IOException, InterruptedException { + if (peekAmrSignature(input, amrSignatureNb)) { + isWideBand = false; + input.skipFully(amrSignatureNb.length); + return true; + } else if (peekAmrSignature(input, amrSignatureWb)) { + isWideBand = true; + input.skipFully(amrSignatureWb.length); + return true; + } + return false; + } + + /** Peeks from the beginning of the input to see if the given AMR signature exists. */ + private boolean peekAmrSignature(ExtractorInput input, byte[] amrSignature) + throws IOException, InterruptedException { + input.resetPeekPosition(); + byte[] header = new byte[amrSignature.length]; + input.peekFully(header, 0, amrSignature.length); + return Arrays.equals(header, amrSignature); + } + + private void maybeOutputFormat() { + if (!hasOutputFormat) { + hasOutputFormat = true; + String mimeType = isWideBand ? MimeTypes.AUDIO_AMR_WB : MimeTypes.AUDIO_AMR_NB; + int sampleRate = isWideBand ? SAMPLE_RATE_WB : SAMPLE_RATE_NB; + trackOutput.format( + Format.createAudioSampleFormat( + /* id= */ null, + mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + MAX_FRAME_SIZE_BYTES, + /* channelCount= */ 1, + sampleRate, + /* pcmEncoding= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null)); + } + } + + private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException { + if (currentSampleBytesRemaining == 0) { + try { + currentSampleSize = peekNextSampleSize(extractorInput); + } catch (EOFException e) { + return RESULT_END_OF_INPUT; + } + currentSampleBytesRemaining = currentSampleSize; + if (firstSampleSize == C.LENGTH_UNSET) { + firstSamplePosition = extractorInput.getPosition(); + firstSampleSize = currentSampleSize; + } + if (firstSampleSize == currentSampleSize) { + numSamplesWithSameSize++; + } + } + + int bytesAppended = + trackOutput.sampleData( + extractorInput, currentSampleBytesRemaining, /* allowEndOfInput= */ true); + if (bytesAppended == C.RESULT_END_OF_INPUT) { + return RESULT_END_OF_INPUT; + } + currentSampleBytesRemaining -= bytesAppended; + if (currentSampleBytesRemaining > 0) { + return RESULT_CONTINUE; + } + + trackOutput.sampleMetadata( + timeOffsetUs + currentSampleTimeUs, + C.BUFFER_FLAG_KEY_FRAME, + currentSampleSize, + /* offset= */ 0, + /* encryptionData= */ null); + currentSampleTimeUs += SAMPLE_TIME_PER_FRAME_US; + return RESULT_CONTINUE; + } + + private int peekNextSampleSize(ExtractorInput extractorInput) + throws IOException, InterruptedException { + extractorInput.resetPeekPosition(); + extractorInput.peekFully(scratch, /* offset= */ 0, /* length= */ 1); + + byte frameHeader = scratch[0]; + if ((frameHeader & 0x83) > 0) { + // The padding bits are at bit-1 positions in the following pattern: 1000 0011 + // Padding bits must be 0. + throw new ParserException("Invalid padding bits for frame header " + frameHeader); + } + + int frameType = (frameHeader >> 3) & 0x0f; + return getFrameSizeInBytes(frameType); + } + + private int getFrameSizeInBytes(int frameType) throws ParserException { + if (!isValidFrameType(frameType)) { + throw new ParserException( + "Illegal AMR " + (isWideBand ? "WB" : "NB") + " frame type " + frameType); + } + + return isWideBand ? frameSizeBytesByTypeWb[frameType] : frameSizeBytesByTypeNb[frameType]; + } + + private boolean isValidFrameType(int frameType) { + return frameType >= 0 + && frameType <= 15 + && (isWideBandValidFrameType(frameType) || isNarrowBandValidFrameType(frameType)); + } + + private boolean isWideBandValidFrameType(int frameType) { + // For wide band, type 10-13 are for future use. + return isWideBand && (frameType < 10 || frameType > 13); + } + + private boolean isNarrowBandValidFrameType(int frameType) { + // For narrow band, type 12-14 are for future use. + return !isWideBand && (frameType < 12 || frameType > 14); + } + + private void maybeOutputSeekMap(long inputLength, int sampleReadResult) { + if (hasOutputSeekMap) { + return; + } + + if ((flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) == 0 + || inputLength == C.LENGTH_UNSET + || (firstSampleSize != C.LENGTH_UNSET && firstSampleSize != currentSampleSize)) { + seekMap = new SeekMap.Unseekable(C.TIME_UNSET); + extractorOutput.seekMap(seekMap); + hasOutputSeekMap = true; + } else if (numSamplesWithSameSize >= NUM_SAME_SIZE_CONSTANT_BIT_RATE_THRESHOLD + || sampleReadResult == RESULT_END_OF_INPUT) { + seekMap = getConstantBitrateSeekMap(inputLength); + extractorOutput.seekMap(seekMap); + hasOutputSeekMap = true; + } + } + + private SeekMap getConstantBitrateSeekMap(long inputLength) { + int bitrate = getBitrateFromFrameSize(firstSampleSize, SAMPLE_TIME_PER_FRAME_US); + return new ConstantBitrateSeekMap(inputLength, firstSamplePosition, bitrate, firstSampleSize); + } + + /** + * Returns the stream bitrate, given a frame size and the duration of that frame in microseconds. + * + * @param frameSize The size of each frame in the stream. + * @param durationUsPerFrame The duration of the given frame in microseconds. + * @return The stream bitrate. + */ + private static int getBitrateFromFrameSize(int frameSize, long durationUsPerFrame) { + return (int) ((frameSize * C.BITS_PER_BYTE * C.MICROS_PER_SECOND) / durationUsPerFrame); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java new file mode 100644 index 000000000000..d13b1f394dcf --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java @@ -0,0 +1,131 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flac; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.BinarySearchSeeker; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata; +import java.io.IOException; + +/** + * A {@link SeekMap} implementation for FLAC stream using binary search. + * + *

This seeker performs seeking by using binary search within the stream, until it finds the + * frame that contains the target sample. + */ +/* package */ final class FlacBinarySearchSeeker extends BinarySearchSeeker { + + /** + * Creates a {@link FlacBinarySearchSeeker}. + * + * @param flacStreamMetadata The stream metadata. + * @param frameStartMarker The frame start marker, consisting of the 2 bytes by which every frame + * in the stream must start. + * @param firstFramePosition The byte offset of the first frame in the stream. + * @param inputLength The length of the stream in bytes. + */ + public FlacBinarySearchSeeker( + FlacStreamMetadata flacStreamMetadata, + int frameStartMarker, + long firstFramePosition, + long inputLength) { + super( + /* seekTimestampConverter= */ flacStreamMetadata::getSampleNumber, + new FlacTimestampSeeker(flacStreamMetadata, frameStartMarker), + flacStreamMetadata.getDurationUs(), + /* floorTimePosition= */ 0, + /* ceilingTimePosition= */ flacStreamMetadata.totalSamples, + /* floorBytePosition= */ firstFramePosition, + /* ceilingBytePosition= */ inputLength, + /* approxBytesPerFrame= */ flacStreamMetadata.getApproxBytesPerFrame(), + /* minimumSearchRange= */ Math.max( + FlacConstants.MIN_FRAME_HEADER_SIZE, flacStreamMetadata.minFrameSize)); + } + + private static final class FlacTimestampSeeker implements TimestampSeeker { + + private final FlacStreamMetadata flacStreamMetadata; + private final int frameStartMarker; + private final SampleNumberHolder sampleNumberHolder; + + private FlacTimestampSeeker(FlacStreamMetadata flacStreamMetadata, int frameStartMarker) { + this.flacStreamMetadata = flacStreamMetadata; + this.frameStartMarker = frameStartMarker; + sampleNumberHolder = new SampleNumberHolder(); + } + + @Override + public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetSampleNumber) + throws IOException, InterruptedException { + long searchPosition = input.getPosition(); + + // Find left frame. + long leftFrameFirstSampleNumber = findNextFrame(input); + long leftFramePosition = input.getPeekPosition(); + + input.advancePeekPosition( + Math.max(FlacConstants.MIN_FRAME_HEADER_SIZE, flacStreamMetadata.minFrameSize)); + + // Find right frame. + long rightFrameFirstSampleNumber = findNextFrame(input); + long rightFramePosition = input.getPeekPosition(); + + if (leftFrameFirstSampleNumber <= targetSampleNumber + && rightFrameFirstSampleNumber > targetSampleNumber) { + return TimestampSearchResult.targetFoundResult(leftFramePosition); + } else if (rightFrameFirstSampleNumber <= targetSampleNumber) { + return TimestampSearchResult.underestimatedResult( + rightFrameFirstSampleNumber, rightFramePosition); + } else { + return TimestampSearchResult.overestimatedResult( + leftFrameFirstSampleNumber, searchPosition); + } + } + + /** + * Searches for the next frame in {@code input}. + * + *

The peek position is advanced to the start of the found frame, or at the end of the stream + * if no frame was found. + * + * @param input The input from which to search (starting from the peek position). + * @return The number of the first sample in the found frame, or the total number of samples in + * the stream if no frame was found. + * @throws IOException If peeking from the input fails. In this case, there is no guarantee on + * the peek position. + * @throws InterruptedException If interrupted while peeking from input. In this case, there is + * no guarantee on the peek position. + */ + private long findNextFrame(ExtractorInput input) throws IOException, InterruptedException { + while (input.getPeekPosition() < input.getLength() - FlacConstants.MIN_FRAME_HEADER_SIZE + && !FlacFrameReader.checkFrameHeaderFromPeek( + input, flacStreamMetadata, frameStartMarker, sampleNumberHolder)) { + input.advancePeekPosition(1); + } + + if (input.getPeekPosition() >= input.getLength() - FlacConstants.MIN_FRAME_HEADER_SIZE) { + input.advancePeekPosition((int) (input.getLength() - input.getPeekPosition())); + return flacStreamMetadata.totalSamples; + } + + return sampleNumberHolder.sampleNumber; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java new file mode 100644 index 000000000000..fa997001e82f --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java @@ -0,0 +1,411 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flac; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacMetadataReader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacSeekTableSeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Extracts data from FLAC container format. + * + *

The format specification can be found at https://xiph.org/flac/format.html. + */ +public final class FlacExtractor implements Extractor { + + /** Factory for {@link FlacExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()}; + + /** + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_DISABLE_ID3_METADATA}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_DISABLE_ID3_METADATA}) + public @interface Flags {} + + /** + * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not + * required. + */ + public static final int FLAG_DISABLE_ID3_METADATA = 1; + + /** Parser state. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + STATE_READ_ID3_METADATA, + STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES, + STATE_READ_STREAM_MARKER, + STATE_READ_METADATA_BLOCKS, + STATE_GET_FRAME_START_MARKER, + STATE_READ_FRAMES + }) + private @interface State {} + + private static final int STATE_READ_ID3_METADATA = 0; + private static final int STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES = 1; + private static final int STATE_READ_STREAM_MARKER = 2; + private static final int STATE_READ_METADATA_BLOCKS = 3; + private static final int STATE_GET_FRAME_START_MARKER = 4; + private static final int STATE_READ_FRAMES = 5; + + /** Arbitrary buffer length of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */ + private static final int BUFFER_LENGTH = 32 * 1024; + + /** Value of an unknown sample number. */ + private static final int SAMPLE_NUMBER_UNKNOWN = -1; + + private final byte[] streamMarkerAndInfoBlock; + private final ParsableByteArray buffer; + private final boolean id3MetadataDisabled; + + private final SampleNumberHolder sampleNumberHolder; + + @MonotonicNonNull private ExtractorOutput extractorOutput; + @MonotonicNonNull private TrackOutput trackOutput; + + private @State int state; + @Nullable private Metadata id3Metadata; + @MonotonicNonNull private FlacStreamMetadata flacStreamMetadata; + private int minFrameSize; + private int frameStartMarker; + @MonotonicNonNull private FlacBinarySearchSeeker binarySearchSeeker; + private int currentFrameBytesWritten; + private long currentFrameFirstSampleNumber; + + /** Constructs an instance with {@code flags = 0}. */ + public FlacExtractor() { + this(/* flags= */ 0); + } + + /** + * Constructs an instance. + * + * @param flags Flags that control the extractor's behavior. Possible flags are described by + * {@link Flags}. + */ + public FlacExtractor(int flags) { + streamMarkerAndInfoBlock = + new byte[FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE]; + buffer = new ParsableByteArray(new byte[BUFFER_LENGTH], /* limit= */ 0); + id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; + sampleNumberHolder = new SampleNumberHolder(); + state = STATE_READ_ID3_METADATA; + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); + return FlacMetadataReader.checkAndPeekStreamMarker(input); + } + + @Override + public void init(ExtractorOutput output) { + extractorOutput = output; + trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_AUDIO); + output.endTracks(); + } + + @Override + public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + switch (state) { + case STATE_READ_ID3_METADATA: + readId3Metadata(input); + return Extractor.RESULT_CONTINUE; + case STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES: + getStreamMarkerAndInfoBlockBytes(input); + return Extractor.RESULT_CONTINUE; + case STATE_READ_STREAM_MARKER: + readStreamMarker(input); + return Extractor.RESULT_CONTINUE; + case STATE_READ_METADATA_BLOCKS: + readMetadataBlocks(input); + return Extractor.RESULT_CONTINUE; + case STATE_GET_FRAME_START_MARKER: + getFrameStartMarker(input); + return Extractor.RESULT_CONTINUE; + case STATE_READ_FRAMES: + return readFrames(input, seekPosition); + default: + throw new IllegalStateException(); + } + } + + @Override + public void seek(long position, long timeUs) { + if (position == 0) { + state = STATE_READ_ID3_METADATA; + } else if (binarySearchSeeker != null) { + binarySearchSeeker.setSeekTargetUs(timeUs); + } + currentFrameFirstSampleNumber = timeUs == 0 ? 0 : SAMPLE_NUMBER_UNKNOWN; + currentFrameBytesWritten = 0; + buffer.reset(); + } + + @Override + public void release() { + // Do nothing. + } + + // Private methods. + + private void readId3Metadata(ExtractorInput input) throws IOException, InterruptedException { + id3Metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ !id3MetadataDisabled); + state = STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES; + } + + private void getStreamMarkerAndInfoBlockBytes(ExtractorInput input) + throws IOException, InterruptedException { + input.peekFully(streamMarkerAndInfoBlock, 0, streamMarkerAndInfoBlock.length); + input.resetPeekPosition(); + state = STATE_READ_STREAM_MARKER; + } + + private void readStreamMarker(ExtractorInput input) throws IOException, InterruptedException { + FlacMetadataReader.readStreamMarker(input); + state = STATE_READ_METADATA_BLOCKS; + } + + private void readMetadataBlocks(ExtractorInput input) throws IOException, InterruptedException { + boolean isLastMetadataBlock = false; + FlacMetadataReader.FlacStreamMetadataHolder metadataHolder = + new FlacMetadataReader.FlacStreamMetadataHolder(flacStreamMetadata); + while (!isLastMetadataBlock) { + isLastMetadataBlock = FlacMetadataReader.readMetadataBlock(input, metadataHolder); + // Save the current metadata in case an exception occurs. + flacStreamMetadata = castNonNull(metadataHolder.flacStreamMetadata); + } + + Assertions.checkNotNull(flacStreamMetadata); + minFrameSize = Math.max(flacStreamMetadata.minFrameSize, FlacConstants.MIN_FRAME_HEADER_SIZE); + castNonNull(trackOutput) + .format(flacStreamMetadata.getFormat(streamMarkerAndInfoBlock, id3Metadata)); + + state = STATE_GET_FRAME_START_MARKER; + } + + private void getFrameStartMarker(ExtractorInput input) throws IOException, InterruptedException { + frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + castNonNull(extractorOutput) + .seekMap( + getSeekMap( + /* firstFramePosition= */ input.getPosition(), + /* streamLength= */ input.getLength())); + + state = STATE_READ_FRAMES; + } + + private @ReadResult int readFrames(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + Assertions.checkNotNull(trackOutput); + Assertions.checkNotNull(flacStreamMetadata); + + // Handle pending binary search seek if necessary. + if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) { + return binarySearchSeeker.handlePendingSeek(input, seekPosition); + } + + // Set current frame first sample number if it became unknown after seeking. + if (currentFrameFirstSampleNumber == SAMPLE_NUMBER_UNKNOWN) { + currentFrameFirstSampleNumber = + FlacFrameReader.getFirstSampleNumber(input, flacStreamMetadata); + return Extractor.RESULT_CONTINUE; + } + + // Copy more bytes into the buffer. + int currentLimit = buffer.limit(); + boolean foundEndOfInput = false; + if (currentLimit < BUFFER_LENGTH) { + int bytesRead = + input.read( + buffer.data, /* offset= */ currentLimit, /* length= */ BUFFER_LENGTH - currentLimit); + foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT; + if (!foundEndOfInput) { + buffer.setLimit(currentLimit + bytesRead); + } else if (buffer.bytesLeft() == 0) { + outputSampleMetadata(); + return Extractor.RESULT_END_OF_INPUT; + } + } + + // Search for a frame. + int positionBeforeFindingAFrame = buffer.getPosition(); + + // Skip frame search on the bytes within the minimum frame size. + if (currentFrameBytesWritten < minFrameSize) { + buffer.skipBytes(Math.min(minFrameSize - currentFrameBytesWritten, buffer.bytesLeft())); + } + + long nextFrameFirstSampleNumber = findFrame(buffer, foundEndOfInput); + int numberOfFrameBytes = buffer.getPosition() - positionBeforeFindingAFrame; + buffer.setPosition(positionBeforeFindingAFrame); + trackOutput.sampleData(buffer, numberOfFrameBytes); + currentFrameBytesWritten += numberOfFrameBytes; + + // Frame found. + if (nextFrameFirstSampleNumber != SAMPLE_NUMBER_UNKNOWN) { + outputSampleMetadata(); + currentFrameBytesWritten = 0; + currentFrameFirstSampleNumber = nextFrameFirstSampleNumber; + } + + if (buffer.bytesLeft() < FlacConstants.MAX_FRAME_HEADER_SIZE) { + // The next frame header may not fit in the rest of the buffer, so put the trailing bytes at + // the start of the buffer, and reset the position and limit. + System.arraycopy( + buffer.data, buffer.getPosition(), buffer.data, /* destPos= */ 0, buffer.bytesLeft()); + buffer.reset(buffer.bytesLeft()); + } + + return Extractor.RESULT_CONTINUE; + } + + private SeekMap getSeekMap(long firstFramePosition, long streamLength) { + Assertions.checkNotNull(flacStreamMetadata); + if (flacStreamMetadata.seekTable != null) { + return new FlacSeekTableSeekMap(flacStreamMetadata, firstFramePosition); + } else if (streamLength != C.LENGTH_UNSET && flacStreamMetadata.totalSamples > 0) { + binarySearchSeeker = + new FlacBinarySearchSeeker( + flacStreamMetadata, frameStartMarker, firstFramePosition, streamLength); + return binarySearchSeeker.getSeekMap(); + } else { + return new SeekMap.Unseekable(flacStreamMetadata.getDurationUs()); + } + } + + /** + * Searches for the start of a frame in {@code data}. + * + *

    + *
  • If the search is successful, the position is set to the start of the found frame. + *
  • Otherwise, the position is set to the first unsearched byte. + *
+ * + * @param data The array to be searched. + * @param foundEndOfInput If the end of input was met when filling in the {@code data}. + * @return The number of the first sample in the frame found, or {@code SAMPLE_NUMBER_UNKNOWN} if + * the search was not successful. + */ + private long findFrame(ParsableByteArray data, boolean foundEndOfInput) { + Assertions.checkNotNull(flacStreamMetadata); + + int frameOffset = data.getPosition(); + while (frameOffset <= data.limit() - FlacConstants.MAX_FRAME_HEADER_SIZE) { + data.setPosition(frameOffset); + if (FlacFrameReader.checkAndReadFrameHeader( + data, flacStreamMetadata, frameStartMarker, sampleNumberHolder)) { + data.setPosition(frameOffset); + return sampleNumberHolder.sampleNumber; + } + frameOffset++; + } + + if (foundEndOfInput) { + // Verify whether there is a frame of size < MAX_FRAME_HEADER_SIZE at the end of the stream by + // checking at every position at a distance between MAX_FRAME_HEADER_SIZE and minFrameSize + // from the buffer limit if it corresponds to a valid frame header. + // At every offset, the different possibilities are: + // 1. The current offset indicates the start of a valid frame header. In this case, consider + // that a frame has been found and stop searching. + // 2. A frame starting at the current offset would be invalid. In this case, keep looking for + // a valid frame header. + // 3. The current offset could be the start of a valid frame header, but there is not enough + // bytes remaining to complete the header. As the end of the file has been reached, this + // means that the current offset does not correspond to a new frame and that the last bytes + // of the last frame happen to be a valid partial frame header. This case can occur in two + // ways: + // 3.1. An attempt to read past the buffer is made when reading the potential frame header. + // 3.2. Reading the potential frame header does not exceed the buffer size, but exceeds the + // buffer limit. + // Note that the third case is very unlikely. It never happens if the end of the input has not + // been reached as it is always made sure that the buffer has at least MAX_FRAME_HEADER_SIZE + // bytes available when reading a potential frame header. + while (frameOffset <= data.limit() - minFrameSize) { + data.setPosition(frameOffset); + boolean frameFound; + try { + frameFound = + FlacFrameReader.checkAndReadFrameHeader( + data, flacStreamMetadata, frameStartMarker, sampleNumberHolder); + } catch (IndexOutOfBoundsException e) { + // Case 3.1. + frameFound = false; + } + if (data.getPosition() > data.limit()) { + // TODO: Remove (and update above comments) once [Internal ref: b/147657250] is fixed. + // Case 3.2. + frameFound = false; + } + if (frameFound) { + // Case 1. + data.setPosition(frameOffset); + return sampleNumberHolder.sampleNumber; + } + frameOffset++; + } + // The end of the frame is the end of the file. + data.setPosition(data.limit()); + } else { + data.setPosition(frameOffset); + } + + return SAMPLE_NUMBER_UNKNOWN; + } + + private void outputSampleMetadata() { + long timeUs = + currentFrameFirstSampleNumber + * C.MICROS_PER_SECOND + / castNonNull(flacStreamMetadata).sampleRate; + castNonNull(trackOutput) + .sampleMetadata( + timeUs, + C.BUFFER_FLAG_KEY_FRAME, + currentFrameBytesWritten, + /* offset= */ 0, + /* encryptionData= */ null); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java index 80d952128f9f..54dbaec0035c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java @@ -18,6 +18,7 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flv; import android.util.Pair; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; @@ -67,10 +68,21 @@ import java.util.Collections; hasOutputFormat = true; } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) { String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW - : MimeTypes.AUDIO_ULAW; - int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT; - Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE, - Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null); + : MimeTypes.AUDIO_MLAW; + Format format = + Format.createAudioSampleFormat( + /* id= */ null, + /* sampleMimeType= */ type, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 1, + /* sampleRate= */ 8000, + /* pcmEncoding= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); output.format(format); hasOutputFormat = true; } else if (audioFormat != AUDIO_FORMAT_AAC) { @@ -85,11 +97,12 @@ import java.util.Collections; } @Override - protected void parsePayload(ParsableByteArray data, long timeUs) { + protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { if (audioFormat == AUDIO_FORMAT_MP3) { int sampleSize = data.bytesLeft(); output.sampleData(data, sampleSize); output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + return true; } else { int packetType = data.readUnsignedByte(); if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { @@ -103,12 +116,15 @@ import java.util.Collections; Collections.singletonList(audioSpecificConfig), null, 0, null); output.format(format); hasOutputFormat = true; + return false; } else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) { int sampleSize = data.bytesLeft(); output.sampleData(data, sampleSize); output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + return true; + } else { + return false; } } } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 5fa05b49f59f..a7438b190f43 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -15,6 +15,7 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flv; +import androidx.annotation.IntDef; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; @@ -23,71 +24,72 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractors import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** - * Facilitates the extraction of data from the FLV container format. + * Extracts data from the FLV container format. */ -public final class FlvExtractor implements Extractor, SeekMap { +public final class FlvExtractor implements Extractor { - /** - * Factory for {@link FlvExtractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { + /** Factory for {@link FlvExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlvExtractor()}; - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new FlvExtractor()}; - } + /** Extractor states. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + STATE_READING_FLV_HEADER, + STATE_SKIPPING_TO_TAG_HEADER, + STATE_READING_TAG_HEADER, + STATE_READING_TAG_DATA + }) + private @interface States {} - }; - - // Header sizes. - private static final int FLV_HEADER_SIZE = 9; - private static final int FLV_TAG_HEADER_SIZE = 11; - - // Parser states. private static final int STATE_READING_FLV_HEADER = 1; private static final int STATE_SKIPPING_TO_TAG_HEADER = 2; private static final int STATE_READING_TAG_HEADER = 3; private static final int STATE_READING_TAG_DATA = 4; + // Header sizes. + private static final int FLV_HEADER_SIZE = 9; + private static final int FLV_TAG_HEADER_SIZE = 11; + // Tag types. private static final int TAG_TYPE_AUDIO = 8; private static final int TAG_TYPE_VIDEO = 9; private static final int TAG_TYPE_SCRIPT_DATA = 18; // FLV container identifier. - private static final int FLV_TAG = Util.getIntegerCodeForString("FLV"); + private static final int FLV_TAG = 0x00464c56; - // Temporary buffers. private final ParsableByteArray scratch; private final ParsableByteArray headerBuffer; private final ParsableByteArray tagHeaderBuffer; private final ParsableByteArray tagData; + private final ScriptTagPayloadReader metadataReader; - // Extractor outputs. private ExtractorOutput extractorOutput; - - // State variables. - private int parserState; + private @States int state; + private boolean outputFirstSample; + private long mediaTagTimestampOffsetUs; private int bytesToNextTagHeader; - public int tagType; - public int tagDataSize; - public long tagTimestampUs; - - // Tags readers. + private int tagType; + private int tagDataSize; + private long tagTimestampUs; + private boolean outputSeekMap; private AudioTagPayloadReader audioReader; private VideoTagPayloadReader videoReader; - private ScriptTagPayloadReader metadataReader; public FlvExtractor() { scratch = new ParsableByteArray(4); headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE); tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE); tagData = new ParsableByteArray(); - parserState = STATE_READING_FLV_HEADER; + metadataReader = new ScriptTagPayloadReader(); + state = STATE_READING_FLV_HEADER; } @Override @@ -128,7 +130,8 @@ public final class FlvExtractor implements Extractor, SeekMap { @Override public void seek(long position, long timeUs) { - parserState = STATE_READING_FLV_HEADER; + state = STATE_READING_FLV_HEADER; + outputFirstSample = false; bytesToNextTagHeader = 0; } @@ -141,7 +144,7 @@ public final class FlvExtractor implements Extractor, SeekMap { public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { while (true) { - switch (parserState) { + switch (state) { case STATE_READING_FLV_HEADER: if (!readFlvHeader(input)) { return RESULT_END_OF_INPUT; @@ -160,6 +163,9 @@ public final class FlvExtractor implements Extractor, SeekMap { return RESULT_CONTINUE; } break; + default: + // Never happens. + throw new IllegalStateException(); } } } @@ -191,15 +197,11 @@ public final class FlvExtractor implements Extractor, SeekMap { videoReader = new VideoTagPayloadReader( extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO)); } - if (metadataReader == null) { - metadataReader = new ScriptTagPayloadReader(null); - } extractorOutput.endTracks(); - extractorOutput.seekMap(this); // We need to skip any additional content in the FLV header, plus the 4 byte previous tag size. bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4; - parserState = STATE_SKIPPING_TO_TAG_HEADER; + state = STATE_SKIPPING_TO_TAG_HEADER; return true; } @@ -213,7 +215,7 @@ public final class FlvExtractor implements Extractor, SeekMap { private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException { input.skipFully(bytesToNextTagHeader); bytesToNextTagHeader = 0; - parserState = STATE_READING_TAG_HEADER; + state = STATE_READING_TAG_HEADER; } /** @@ -236,7 +238,7 @@ public final class FlvExtractor implements Extractor, SeekMap { tagTimestampUs = tagHeaderBuffer.readUnsignedInt24(); tagTimestampUs = ((tagHeaderBuffer.readUnsignedByte() << 24) | tagTimestampUs) * 1000L; tagHeaderBuffer.skipBytes(3); // streamId - parserState = STATE_READING_TAG_DATA; + state = STATE_READING_TAG_DATA; return true; } @@ -250,18 +252,32 @@ public final class FlvExtractor implements Extractor, SeekMap { */ private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { boolean wasConsumed = true; + boolean wasSampleOutput = false; + long timestampUs = getCurrentTimestampUs(); if (tagType == TAG_TYPE_AUDIO && audioReader != null) { - audioReader.consume(prepareTagData(input), tagTimestampUs); + ensureReadyForMediaOutput(); + wasSampleOutput = audioReader.consume(prepareTagData(input), timestampUs); } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) { - videoReader.consume(prepareTagData(input), tagTimestampUs); - } else if (tagType == TAG_TYPE_SCRIPT_DATA && metadataReader != null) { - metadataReader.consume(prepareTagData(input), tagTimestampUs); + ensureReadyForMediaOutput(); + wasSampleOutput = videoReader.consume(prepareTagData(input), timestampUs); + } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) { + wasSampleOutput = metadataReader.consume(prepareTagData(input), timestampUs); + long durationUs = metadataReader.getDurationUs(); + if (durationUs != C.TIME_UNSET) { + extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); + outputSeekMap = true; + } } else { input.skipFully(tagDataSize); wasConsumed = false; } + if (!outputFirstSample && wasSampleOutput) { + outputFirstSample = true; + mediaTagTimestampOffsetUs = + metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0; + } bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header. - parserState = STATE_SKIPPING_TO_TAG_HEADER; + state = STATE_SKIPPING_TO_TAG_HEADER; return wasConsumed; } @@ -277,21 +293,16 @@ public final class FlvExtractor implements Extractor, SeekMap { return tagData; } - // SeekMap implementation. - - @Override - public boolean isSeekable() { - return false; + private void ensureReadyForMediaOutput() { + if (!outputSeekMap) { + extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); + outputSeekMap = true; + } } - @Override - public long getDurationUs() { - return metadataReader.getDurationUs(); + private long getCurrentTimestampUs() { + return outputFirstSample + ? (mediaTagTimestampOffsetUs + tagTimestampUs) + : (metadataReader.getDurationUs() == C.TIME_UNSET ? 0 : tagTimestampUs); } - - @Override - public long getPosition(long timeUs) { - return 0; - } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java index 97730b96af62..1494bf1c2e54 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java @@ -15,9 +15,10 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.flv; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DummyTrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.Date; @@ -44,11 +45,8 @@ import java.util.Map; private long durationUs; - /** - * @param output A {@link TrackOutput} to which samples should be written. - */ - public ScriptTagPayloadReader(TrackOutput output) { - super(output); + public ScriptTagPayloadReader() { + super(new DummyTrackOutput()); durationUs = C.TIME_UNSET; } @@ -67,7 +65,7 @@ import java.util.Map; } @Override - protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { int nameType = readAmfType(data); if (nameType != AMF_TYPE_STRING) { // Should never happen. @@ -76,12 +74,12 @@ import java.util.Map; String name = readAmfString(data); if (!NAME_METADATA.equals(name)) { // We're only interested in metadata. - return; + return false; } int type = readAmfType(data); if (type != AMF_TYPE_ECMA_ARRAY) { // We're not interested in this metadata. - return; + return false; } // Set the duration to the value contained in the metadata, if present. Map metadata = readAmfEcmaArray(data); @@ -91,6 +89,7 @@ import java.util.Map; durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND); } } + return false; } private static int readAmfType(ParsableByteArray data) { @@ -141,7 +140,10 @@ import java.util.Map; ArrayList list = new ArrayList<>(count); for (int i = 0; i < count; i++) { int type = readAmfType(data); - list.add(readAmfData(data, type)); + Object value = readAmfData(data, type); + if (value != null) { + list.add(value); + } } return list; } @@ -160,7 +162,10 @@ import java.util.Map; if (type == AMF_TYPE_END_MARKER) { break; } - array.put(key, readAmfData(data, type)); + Object value = readAmfData(data, type); + if (value != null) { + array.put(key, value); + } } return array; } @@ -177,7 +182,10 @@ import java.util.Map; for (int i = 0; i < count; i++) { String key = readAmfString(data); int type = readAmfType(data); - array.put(key, readAmfData(data, type)); + Object value = readAmfData(data, type); + if (value != null) { + array.put(key, value); + } } return array; } @@ -194,6 +202,7 @@ import java.util.Map; return date; } + @Nullable private static Object readAmfData(ParsableByteArray data, int type) { switch (type) { case AMF_TYPE_NUMBER: @@ -211,8 +220,8 @@ import java.util.Map; case AMF_TYPE_DATE: return readAmfDate(data); default: + // We don't log a warning because there are types that we knowingly don't support. return null; } } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java index 9a85bdcadada..3f8b51244a16 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java @@ -58,12 +58,11 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArr * * @param data The payload data to consume. * @param timeUs The timestamp associated with the payload. + * @return Whether a sample was output. * @throws ParserException If an error occurs parsing the data. */ - public final void consume(ParsableByteArray data, long timeUs) throws ParserException { - if (parseHeader(data)) { - parsePayload(data, timeUs); - } + public final boolean consume(ParsableByteArray data, long timeUs) throws ParserException { + return parseHeader(data) && parsePayload(data, timeUs); } /** @@ -78,10 +77,11 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArr /** * Parses tag payload. * - * @param data Buffer where tag payload is stored - * @param timeUs Time position of the frame + * @param data Buffer where tag payload is stored. + * @param timeUs Time position of the frame. + * @return Whether a sample was output. * @throws ParserException If an error occurs parsing the payload. */ - protected abstract void parsePayload(ParsableByteArray data, long timeUs) throws ParserException; - + protected abstract boolean parsePayload(ParsableByteArray data, long timeUs) + throws ParserException; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java index 0d8d54b00168..6ed5206144b7 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java @@ -47,6 +47,7 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.video.AvcConfig; // State variables. private boolean hasOutputFormat; + private boolean hasOutputKeyframe; private int frameType; /** @@ -60,7 +61,7 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.video.AvcConfig; @Override public void seek() { - // Do nothing. + hasOutputKeyframe = false; } @Override @@ -77,9 +78,10 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.video.AvcConfig; } @Override - protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { int packetType = data.readUnsignedByte(); - int compositionTimeMs = data.readUnsignedInt24(); + int compositionTimeMs = data.readInt24(); + timeUs += compositionTimeMs * 1000L; // Parse avc sequence header in case this was not done before. if (packetType == AVC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { @@ -93,7 +95,12 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.video.AvcConfig; avcConfig.initializationData, Format.NO_VALUE, avcConfig.pixelWidthAspectRatio, null); output.format(format); hasOutputFormat = true; + return false; } else if (packetType == AVC_PACKET_TYPE_AVC_NALU && hasOutputFormat) { + boolean isKeyframe = frameType == VIDEO_FRAME_KEYFRAME; + if (!hasOutputKeyframe && !isKeyframe) { + return false; + } // TODO: Deduplicate with Mp4Extractor. // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // they're only 1 or 2 bytes long. @@ -122,8 +129,12 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.video.AvcConfig; output.sampleData(data, bytesToWrite); bytesWritten += bytesToWrite; } - output.sampleMetadata(timeUs, frameType == VIDEO_FRAME_KEYFRAME ? C.BUFFER_FLAG_KEY_FRAME : 0, - bytesWritten, 0, null); + output.sampleMetadata( + timeUs, isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0, bytesWritten, 0, null); + hasOutputKeyframe = true; + return true; + } else { + return false; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java index 58405e3e65c5..b4e160fa74fa 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java @@ -15,19 +15,28 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv; +import androidx.annotation.IntDef; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; import java.io.IOException; -import java.util.Stack; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayDeque; /** * Default implementation of {@link EbmlReader}. */ /* package */ final class DefaultEbmlReader implements EbmlReader { + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ELEMENT_STATE_READ_ID, ELEMENT_STATE_READ_CONTENT_SIZE, ELEMENT_STATE_READ_CONTENT}) + private @interface ElementState {} + private static final int ELEMENT_STATE_READ_ID = 0; private static final int ELEMENT_STATE_READ_CONTENT_SIZE = 1; private static final int ELEMENT_STATE_READ_CONTENT = 2; @@ -39,18 +48,24 @@ import java.util.Stack; private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4; private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8; - private final byte[] scratch = new byte[8]; - private final Stack masterElementsStack = new Stack<>(); - private final VarintReader varintReader = new VarintReader(); + private final byte[] scratch; + private final ArrayDeque masterElementsStack; + private final VarintReader varintReader; - private EbmlReaderOutput output; - private int elementState; + private EbmlProcessor processor; + private @ElementState int elementState; private int elementId; private long elementContentSize; + public DefaultEbmlReader() { + scratch = new byte[8]; + masterElementsStack = new ArrayDeque<>(); + varintReader = new VarintReader(); + } + @Override - public void init(EbmlReaderOutput eventHandler) { - this.output = eventHandler; + public void init(EbmlProcessor processor) { + this.processor = processor; } @Override @@ -62,11 +77,11 @@ import java.util.Stack; @Override public boolean read(ExtractorInput input) throws IOException, InterruptedException { - Assertions.checkState(output != null); + Assertions.checkNotNull(processor); while (true) { if (!masterElementsStack.isEmpty() && input.getPosition() >= masterElementsStack.peek().elementEndPosition) { - output.endMasterElement(masterElementsStack.pop().elementId); + processor.endMasterElement(masterElementsStack.pop().elementId); return true; } @@ -88,42 +103,42 @@ import java.util.Stack; elementState = ELEMENT_STATE_READ_CONTENT; } - int type = output.getElementType(elementId); + @EbmlProcessor.ElementType int type = processor.getElementType(elementId); switch (type) { - case TYPE_MASTER: + case EbmlProcessor.ELEMENT_TYPE_MASTER: long elementContentPosition = input.getPosition(); long elementEndPosition = elementContentPosition + elementContentSize; - masterElementsStack.add(new MasterElement(elementId, elementEndPosition)); - output.startMasterElement(elementId, elementContentPosition, elementContentSize); + masterElementsStack.push(new MasterElement(elementId, elementEndPosition)); + processor.startMasterElement(elementId, elementContentPosition, elementContentSize); elementState = ELEMENT_STATE_READ_ID; return true; - case TYPE_UNSIGNED_INT: + case EbmlProcessor.ELEMENT_TYPE_UNSIGNED_INT: if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) { throw new ParserException("Invalid integer size: " + elementContentSize); } - output.integerElement(elementId, readInteger(input, (int) elementContentSize)); + processor.integerElement(elementId, readInteger(input, (int) elementContentSize)); elementState = ELEMENT_STATE_READ_ID; return true; - case TYPE_FLOAT: + case EbmlProcessor.ELEMENT_TYPE_FLOAT: if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES && elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) { throw new ParserException("Invalid float size: " + elementContentSize); } - output.floatElement(elementId, readFloat(input, (int) elementContentSize)); + processor.floatElement(elementId, readFloat(input, (int) elementContentSize)); elementState = ELEMENT_STATE_READ_ID; return true; - case TYPE_STRING: + case EbmlProcessor.ELEMENT_TYPE_STRING: if (elementContentSize > Integer.MAX_VALUE) { throw new ParserException("String element size: " + elementContentSize); } - output.stringElement(elementId, readString(input, (int) elementContentSize)); + processor.stringElement(elementId, readString(input, (int) elementContentSize)); elementState = ELEMENT_STATE_READ_ID; return true; - case TYPE_BINARY: - output.binaryElement(elementId, (int) elementContentSize, input); + case EbmlProcessor.ELEMENT_TYPE_BINARY: + processor.binaryElement(elementId, (int) elementContentSize, input); elementState = ELEMENT_STATE_READ_ID; return true; - case TYPE_UNKNOWN: + case EbmlProcessor.ELEMENT_TYPE_UNKNOWN: input.skipFully((int) elementContentSize); elementState = ELEMENT_STATE_READ_ID; break; @@ -152,7 +167,7 @@ import java.util.Stack; int varintLength = VarintReader.parseUnsignedVarintLength(scratch[0]); if (varintLength != C.LENGTH_UNSET && varintLength <= MAX_ID_BYTES) { int potentialId = (int) VarintReader.assembleVarint(scratch, varintLength, false); - if (output.isLevel1Element(potentialId)) { + if (processor.isLevel1Element(potentialId)) { input.skipFully(varintLength); return potentialId; } @@ -202,10 +217,11 @@ import java.util.Stack; } /** - * Reads and returns a string of length {@code byteLength} from the {@link ExtractorInput}. + * Reads a string of length {@code byteLength} from the {@link ExtractorInput}. Zero padding is + * removed, so the returned string may be shorter than {@code byteLength}. * * @param input The {@link ExtractorInput} from which to read. - * @param byteLength The length of the float being read. + * @param byteLength The length of the string being read, including zero padding. * @return The read string value. * @throws IOException If an error occurs reading from the input. * @throws InterruptedException If the thread is interrupted. @@ -217,12 +233,17 @@ import java.util.Stack; } byte[] stringBytes = new byte[byteLength]; input.readFully(stringBytes, 0, byteLength); - return new String(stringBytes); + // Remove zero padding. + int trimmedLength = byteLength; + while (trimmedLength > 0 && stringBytes[trimmedLength - 1] == 0) { + trimmedLength--; + } + return new String(stringBytes, 0, trimmedLength); } /** * Used in {@link #masterElementsStack} to track when the current master element ends, so that - * {@link EbmlReaderOutput#endMasterElement(int)} can be called. + * {@link EbmlProcessor#endMasterElement(int)} can be called. */ private static final class MasterElement { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java similarity index 71% rename from mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java rename to mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java index 901ae3d65327..188ced05549b 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * 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. @@ -15,24 +15,58 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv; +import androidx.annotation.IntDef; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; -/** - * Defines EBML element IDs/types and reacts to events. - */ -/* package */ interface EbmlReaderOutput { +/** Defines EBML element IDs/types and processes events. */ +public interface EbmlProcessor { + + /** + * EBML element types. One of {@link #ELEMENT_TYPE_UNKNOWN}, {@link #ELEMENT_TYPE_MASTER}, {@link + * #ELEMENT_TYPE_UNSIGNED_INT}, {@link #ELEMENT_TYPE_STRING}, {@link #ELEMENT_TYPE_BINARY} or + * {@link #ELEMENT_TYPE_FLOAT}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + ELEMENT_TYPE_UNKNOWN, + ELEMENT_TYPE_MASTER, + ELEMENT_TYPE_UNSIGNED_INT, + ELEMENT_TYPE_STRING, + ELEMENT_TYPE_BINARY, + ELEMENT_TYPE_FLOAT + }) + @interface ElementType {} + /** Type for unknown elements. */ + int ELEMENT_TYPE_UNKNOWN = 0; + /** Type for elements that contain child elements. */ + int ELEMENT_TYPE_MASTER = 1; + /** Type for integer value elements of up to 8 bytes. */ + int ELEMENT_TYPE_UNSIGNED_INT = 2; + /** Type for string elements. */ + int ELEMENT_TYPE_STRING = 3; + /** Type for binary elements. */ + int ELEMENT_TYPE_BINARY = 4; + /** Type for IEEE floating point value elements of either 4 or 8 bytes. */ + int ELEMENT_TYPE_FLOAT = 5; /** * Maps an element ID to a corresponding type. - *

- * If {@link EbmlReader#TYPE_UNKNOWN} is returned then the element is skipped. Note that all + * + *

If {@link #ELEMENT_TYPE_UNKNOWN} is returned then the element is skipped. Note that all * children of a skipped element are also skipped. * * @param id The element ID to map. - * @return One of the {@code TYPE_} constants defined in {@link EbmlReader}. + * @return One of {@link #ELEMENT_TYPE_UNKNOWN}, {@link #ELEMENT_TYPE_MASTER}, {@link + * #ELEMENT_TYPE_UNSIGNED_INT}, {@link #ELEMENT_TYPE_STRING}, {@link #ELEMENT_TYPE_BINARY} and + * {@link #ELEMENT_TYPE_FLOAT}. */ + @ElementType int getElementType(int id); /** diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java index 1e280981f535..1416a9087ef7 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java @@ -20,45 +20,20 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorI import java.io.IOException; /** - * Event-driven EBML reader that delivers events to an {@link EbmlReaderOutput}. - *

- * EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers. It was - * originally designed for the Matroska container format. More information about EBML and - * Matroska is available here. + * Event-driven EBML reader that delivers events to an {@link EbmlProcessor}. + * + *

EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers. It was + * originally designed for the Matroska container format. More information about EBML and Matroska + * is available here. */ /* package */ interface EbmlReader { /** - * Type for unknown elements. - */ - int TYPE_UNKNOWN = 0; - /** - * Type for elements that contain child elements. - */ - int TYPE_MASTER = 1; - /** - * Type for integer value elements of up to 8 bytes. - */ - int TYPE_UNSIGNED_INT = 2; - /** - * Type for string elements. - */ - int TYPE_STRING = 3; - /** - * Type for binary elements. - */ - int TYPE_BINARY = 4; - /** - * Type for IEEE floating point value elements of either 4 or 8 bytes. - */ - int TYPE_FLOAT = 5; - - /** - * Initializes the extractor with an {@link EbmlReaderOutput}. + * Initializes the extractor with an {@link EbmlProcessor}. * - * @param output An {@link EbmlReaderOutput} to receive events. + * @param processor An {@link EbmlProcessor} to process events. */ - void init(EbmlReaderOutput output); + void init(EbmlProcessor processor); /** * Resets the state of the reader. diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 42e6c8224764..d9587cd27e9e 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -15,11 +15,15 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mkv; -import android.support.annotation.IntDef; +import android.util.Pair; import android.util.SparseArray; +import androidx.annotation.CallSuper; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ChunkIndex; @@ -31,6 +35,8 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioH import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.LongArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil; @@ -40,6 +46,7 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.video.AvcConfig; import org.mozilla.thirdparty.com.google.android.exoplayer2.video.ColorInfo; import org.mozilla.thirdparty.com.google.android.exoplayer2.video.HevcConfig; import java.io.IOException; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; @@ -51,28 +58,21 @@ import java.util.List; import java.util.Locale; import java.util.UUID; -/** - * Extracts data from a Matroska or WebM file. - */ -public final class MatroskaExtractor implements Extractor { +/** Extracts data from the Matroska and WebM container formats. */ +public class MatroskaExtractor implements Extractor { + + /** Factory for {@link MatroskaExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new MatroskaExtractor()}; /** - * Factory for {@link MatroskaExtractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new MatroskaExtractor()}; - } - - }; - - /** - * Flags controlling the behavior of the extractor. + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_DISABLE_SEEK_FOR_CUES}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_DISABLE_SEEK_FOR_CUES}) + @IntDef( + flag = true, + value = {FLAG_DISABLE_SEEK_FOR_CUES}) public @interface Flags {} /** * Flag to disable seeking for cues. @@ -84,6 +84,8 @@ public final class MatroskaExtractor implements Extractor { */ public static final int FLAG_DISABLE_SEEK_FOR_CUES = 1; + private static final String TAG = "MatroskaExtractor"; + private static final int UNSET_ENTRY_ID = -1; private static final int BLOCK_STATE_START = 0; @@ -94,6 +96,7 @@ public final class MatroskaExtractor implements Extractor { private static final String DOC_TYPE_WEBM = "webm"; private static final String CODEC_ID_VP8 = "V_VP8"; private static final String CODEC_ID_VP9 = "V_VP9"; + private static final String CODEC_ID_AV1 = "V_AV1"; private static final String CODEC_ID_MPEG2 = "V_MPEG2"; private static final String CODEC_ID_MPEG4_SP = "V_MPEG4/ISO/SP"; private static final String CODEC_ID_MPEG4_ASP = "V_MPEG4/ISO/ASP"; @@ -117,6 +120,7 @@ public final class MatroskaExtractor implements Extractor { private static final String CODEC_ID_ACM = "A_MS/ACM"; private static final String CODEC_ID_PCM_INT_LIT = "A_PCM/INT/LIT"; private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8"; + private static final String CODEC_ID_ASS = "S_TEXT/ASS"; private static final String CODEC_ID_VOBSUB = "S_VOBSUB"; private static final String CODEC_ID_PGS = "S_HDMV/PGS"; private static final String CODEC_ID_DVBSUB = "S_DVBSUB"; @@ -145,6 +149,10 @@ public final class MatroskaExtractor implements Extractor { private static final int ID_BLOCK_GROUP = 0xA0; private static final int ID_BLOCK = 0xA1; private static final int ID_BLOCK_DURATION = 0x9B; + private static final int ID_BLOCK_ADDITIONS = 0x75A1; + private static final int ID_BLOCK_MORE = 0xA6; + private static final int ID_BLOCK_ADD_ID = 0xEE; + private static final int ID_BLOCK_ADDITIONAL = 0xA5; private static final int ID_REFERENCE_BLOCK = 0xFB; private static final int ID_TRACKS = 0x1654AE6B; private static final int ID_TRACK_ENTRY = 0xAE; @@ -153,6 +161,8 @@ public final class MatroskaExtractor implements Extractor { private static final int ID_FLAG_DEFAULT = 0x88; private static final int ID_FLAG_FORCED = 0x55AA; private static final int ID_DEFAULT_DURATION = 0x23E383; + private static final int ID_MAX_BLOCK_ADDITION_ID = 0x55EE; + private static final int ID_NAME = 0x536E; private static final int ID_CODEC_ID = 0x86; private static final int ID_CODEC_PRIVATE = 0x63A2; private static final int ID_CODEC_DELAY = 0x56AA; @@ -186,7 +196,11 @@ public final class MatroskaExtractor implements Extractor { private static final int ID_CUE_CLUSTER_POSITION = 0xF1; private static final int ID_LANGUAGE = 0x22B59C; private static final int ID_PROJECTION = 0x7670; + private static final int ID_PROJECTION_TYPE = 0x7671; private static final int ID_PROJECTION_PRIVATE = 0x7672; + private static final int ID_PROJECTION_POSE_YAW = 0x7673; + private static final int ID_PROJECTION_POSE_PITCH = 0x7674; + private static final int ID_PROJECTION_POSE_ROLL = 0x7675; private static final int ID_STEREO_MODE = 0x53B8; private static final int ID_COLOUR = 0x55B0; private static final int ID_COLOUR_RANGE = 0x55B9; @@ -206,38 +220,85 @@ public final class MatroskaExtractor implements Extractor { private static final int ID_LUMNINANCE_MAX = 0x55D9; private static final int ID_LUMNINANCE_MIN = 0x55DA; + /** + * BlockAddID value for ITU T.35 metadata in a VP9 track. See also + * https://www.webmproject.org/docs/container/. + */ + private static final int BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 = 4; + private static final int LACING_NONE = 0; private static final int LACING_XIPH = 1; private static final int LACING_FIXED_SIZE = 2; private static final int LACING_EBML = 3; + private static final int FOURCC_COMPRESSION_DIVX = 0x58564944; + private static final int FOURCC_COMPRESSION_H263 = 0x33363248; private static final int FOURCC_COMPRESSION_VC1 = 0x31435657; /** - * A template for the prefix that must be added to each subrip sample. The 12 byte end timecode - * starting at {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be - * replaced with the duration of the subtitle. - *

- * Equivalent to the UTF-8 string: "1\n00:00:00,000 --> 00:00:00,000\n". + * A template for the prefix that must be added to each subrip sample. + * + *

The display time of each subtitle is passed as {@code timeUs} to {@link + * TrackOutput#sampleMetadata}. The start and end timecodes in this template are relative to + * {@code timeUs}. Hence the start timecode is always zero. The 12 byte end timecode starting at + * {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be replaced with + * the duration of the subtitle. + * + *

Equivalent to the UTF-8 string: "1\n00:00:00,000 --> 00:00:00,000\n". */ - private static final byte[] SUBRIP_PREFIX = new byte[] {49, 10, 48, 48, 58, 48, 48, 58, 48, 48, - 44, 48, 48, 48, 32, 45, 45, 62, 32, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 48, 48, 10}; - /** - * A special end timecode indicating that a subtitle should be displayed until the next subtitle, - * or until the end of the media in the case of the last subtitle. - *

- * Equivalent to the UTF-8 string: " ". - */ - private static final byte[] SUBRIP_TIMECODE_EMPTY = - new byte[] {32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}; + private static final byte[] SUBRIP_PREFIX = + new byte[] { + 49, 10, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 48, 48, 32, 45, 45, 62, 32, 48, 48, 58, 48, + 48, 58, 48, 48, 44, 48, 48, 48, 10 + }; /** * The byte offset of the end timecode in {@link #SUBRIP_PREFIX}. */ private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19; /** - * The length in bytes of a timecode in a subrip prefix. + * The value by which to divide a time in microseconds to convert it to the unit of the last value + * in a subrip timecode (milliseconds). */ - private static final int SUBRIP_TIMECODE_LENGTH = 12; + private static final long SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR = 1000; + /** + * The format of a subrip timecode. + */ + private static final String SUBRIP_TIMECODE_FORMAT = "%02d:%02d:%02d,%03d"; + + /** + * Matroska specific format line for SSA subtitles. + */ + private static final byte[] SSA_DIALOGUE_FORMAT = Util.getUtf8Bytes("Format: Start, End, " + + "ReadOrder, Layer, Style, Name, MarginL, MarginR, MarginV, Effect, Text"); + /** + * A template for the prefix that must be added to each SSA sample. + * + *

The display time of each subtitle is passed as {@code timeUs} to {@link + * TrackOutput#sampleMetadata}. The start and end timecodes in this template are relative to + * {@code timeUs}. Hence the start timecode is always zero. The 12 byte end timecode starting at + * {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be replaced with + * the duration of the subtitle. + * + *

Equivalent to the UTF-8 string: "Dialogue: 0:00:00:00,0:00:00:00,". + */ + private static final byte[] SSA_PREFIX = + new byte[] { + 68, 105, 97, 108, 111, 103, 117, 101, 58, 32, 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44, + 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44 + }; + /** + * The byte offset of the end timecode in {@link #SSA_PREFIX}. + */ + private static final int SSA_PREFIX_END_TIMECODE_OFFSET = 21; + /** + * The value by which to divide a time in microseconds to convert it to the unit of the last value + * in an SSA timecode (1/100ths of a second). + */ + private static final long SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR = 10000; + /** + * The format of an SSA timecode. + */ + private static final String SSA_TIMECODE_FORMAT = "%01d:%02d:%02d:%02d"; /** * The length in bytes of a WAVEFORMATEX structure. @@ -268,9 +329,10 @@ public final class MatroskaExtractor implements Extractor { private final ParsableByteArray vorbisNumPageSamples; private final ParsableByteArray seekEntryIdBytes; private final ParsableByteArray sampleStrippedBytes; - private final ParsableByteArray subripSample; + private final ParsableByteArray subtitleSample; private final ParsableByteArray encryptionInitializationVector; private final ParsableByteArray encryptionSubsampleData; + private final ParsableByteArray blockAdditionalData; private ByteBuffer encryptionSubsampleDataBuffer; private long segmentContentSize; @@ -298,30 +360,33 @@ public final class MatroskaExtractor implements Extractor { private LongArray cueClusterPositions; private boolean seenClusterPositionForCurrentCuePoint; + // Reading state. + private boolean haveOutputSample; + // Block reading state. private int blockState; private long blockTimeUs; private long blockDurationUs; - private int blockLacingSampleIndex; - private int blockLacingSampleCount; - private int[] blockLacingSampleSizes; + private int blockSampleIndex; + private int blockSampleCount; + private int[] blockSampleSizes; private int blockTrackNumber; private int blockTrackNumberLength; @C.BufferFlags private int blockFlags; + private int blockAdditionalId; + private boolean blockHasReferenceBlock; - // Sample reading state. + // Sample writing state. private int sampleBytesRead; + private int sampleBytesWritten; + private int sampleCurrentNalBytesRemaining; private boolean sampleEncodingHandled; private boolean sampleSignalByteRead; - private boolean sampleInitializationVectorRead; private boolean samplePartitionCountRead; - private byte sampleSignalByte; private int samplePartitionCount; - private int sampleCurrentNalBytesRemaining; - private int sampleBytesWritten; - private boolean sampleRead; - private boolean sampleSeenReferenceBlock; + private byte sampleSignalByte; + private boolean sampleInitializationVectorRead; // Extractor outputs. private ExtractorOutput extractorOutput; @@ -336,7 +401,7 @@ public final class MatroskaExtractor implements Extractor { /* package */ MatroskaExtractor(EbmlReader reader, @Flags int flags) { this.reader = reader; - this.reader.init(new InnerEbmlReaderOutput()); + this.reader.init(new InnerEbmlProcessor()); seekForCuesEnabled = (flags & FLAG_DISABLE_SEEK_FOR_CUES) == 0; varintReader = new VarintReader(); tracks = new SparseArray<>(); @@ -346,50 +411,68 @@ public final class MatroskaExtractor implements Extractor { nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalLength = new ParsableByteArray(4); sampleStrippedBytes = new ParsableByteArray(); - subripSample = new ParsableByteArray(); + subtitleSample = new ParsableByteArray(); encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE); encryptionSubsampleData = new ParsableByteArray(); + blockAdditionalData = new ParsableByteArray(); } @Override - public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + public final boolean sniff(ExtractorInput input) throws IOException, InterruptedException { return new Sniffer().sniff(input); } @Override - public void init(ExtractorOutput output) { + public final void init(ExtractorOutput output) { extractorOutput = output; } + @CallSuper @Override public void seek(long position, long timeUs) { clusterTimecodeUs = C.TIME_UNSET; blockState = BLOCK_STATE_START; reader.reset(); varintReader.reset(); - resetSample(); + resetWriteSampleData(); + for (int i = 0; i < tracks.size(); i++) { + tracks.valueAt(i).reset(); + } } @Override - public void release() { + public final void release() { // Do nothing } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, - InterruptedException { - sampleRead = false; + public final int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + haveOutputSample = false; boolean continueReading = true; - while (continueReading && !sampleRead) { + while (continueReading && !haveOutputSample) { continueReading = reader.read(input); if (continueReading && maybeSeekForCues(seekPosition, input.getPosition())) { return Extractor.RESULT_SEEK; } } - return continueReading ? Extractor.RESULT_CONTINUE : Extractor.RESULT_END_OF_INPUT; + if (!continueReading) { + for (int i = 0; i < tracks.size(); i++) { + tracks.valueAt(i).outputPendingSampleMetadata(); + } + return Extractor.RESULT_END_OF_INPUT; + } + return Extractor.RESULT_CONTINUE; } - /* package */ int getElementType(int id) { + /** + * Maps an element ID to a corresponding type. + * + * @see EbmlProcessor#getElementType(int) + */ + @CallSuper + @EbmlProcessor.ElementType + protected int getElementType(int id) { switch (id) { case ID_EBML: case ID_SEGMENT: @@ -410,10 +493,12 @@ public final class MatroskaExtractor implements Extractor { case ID_CUE_POINT: case ID_CUE_TRACK_POSITIONS: case ID_BLOCK_GROUP: + case ID_BLOCK_ADDITIONS: + case ID_BLOCK_MORE: case ID_PROJECTION: case ID_COLOUR: case ID_MASTERING_METADATA: - return EbmlReader.TYPE_MASTER; + return EbmlProcessor.ELEMENT_TYPE_MASTER; case ID_EBML_READ_VERSION: case ID_DOC_TYPE_READ_VERSION: case ID_SEEK_POSITION: @@ -430,6 +515,7 @@ public final class MatroskaExtractor implements Extractor { case ID_FLAG_DEFAULT: case ID_FLAG_FORCED: case ID_DEFAULT_DURATION: + case ID_MAX_BLOCK_ADDITION_ID: case ID_CODEC_DELAY: case ID_SEEK_PRE_ROLL: case ID_CHANNELS: @@ -448,11 +534,14 @@ public final class MatroskaExtractor implements Extractor { case ID_COLOUR_PRIMARIES: case ID_MAX_CLL: case ID_MAX_FALL: - return EbmlReader.TYPE_UNSIGNED_INT; + case ID_PROJECTION_TYPE: + case ID_BLOCK_ADD_ID: + return EbmlProcessor.ELEMENT_TYPE_UNSIGNED_INT; case ID_DOC_TYPE: + case ID_NAME: case ID_CODEC_ID: case ID_LANGUAGE: - return EbmlReader.TYPE_STRING; + return EbmlProcessor.ELEMENT_TYPE_STRING; case ID_SEEK_ID: case ID_CONTENT_COMPRESSION_SETTINGS: case ID_CONTENT_ENCRYPTION_KEY_ID: @@ -460,7 +549,8 @@ public final class MatroskaExtractor implements Extractor { case ID_BLOCK: case ID_CODEC_PRIVATE: case ID_PROJECTION_PRIVATE: - return EbmlReader.TYPE_BINARY; + case ID_BLOCK_ADDITIONAL: + return EbmlProcessor.ELEMENT_TYPE_BINARY; case ID_DURATION: case ID_SAMPLING_FREQUENCY: case ID_PRIMARY_R_CHROMATICITY_X: @@ -473,17 +563,32 @@ public final class MatroskaExtractor implements Extractor { case ID_WHITE_POINT_CHROMATICITY_Y: case ID_LUMNINANCE_MAX: case ID_LUMNINANCE_MIN: - return EbmlReader.TYPE_FLOAT; + case ID_PROJECTION_POSE_YAW: + case ID_PROJECTION_POSE_PITCH: + case ID_PROJECTION_POSE_ROLL: + return EbmlProcessor.ELEMENT_TYPE_FLOAT; default: - return EbmlReader.TYPE_UNKNOWN; + return EbmlProcessor.ELEMENT_TYPE_UNKNOWN; } } - /* package */ boolean isLevel1Element(int id) { + /** + * Checks if the given id is that of a level 1 element. + * + * @see EbmlProcessor#isLevel1Element(int) + */ + @CallSuper + protected boolean isLevel1Element(int id) { return id == ID_SEGMENT_INFO || id == ID_CLUSTER || id == ID_CUES || id == ID_TRACKS; } - /* package */ void startMasterElement(int id, long contentPosition, long contentSize) + /** + * Called when the start of a master element is encountered. + * + * @see EbmlProcessor#startMasterElement(int, long, long) + */ + @CallSuper + protected void startMasterElement(int id, long contentPosition, long contentSize) throws ParserException { switch (id) { case ID_SEGMENT: @@ -520,7 +625,7 @@ public final class MatroskaExtractor implements Extractor { } break; case ID_BLOCK_GROUP: - sampleSeenReferenceBlock = false; + blockHasReferenceBlock = false; break; case ID_CONTENT_ENCODING: // TODO: check and fail if more than one content encoding is present. @@ -539,7 +644,13 @@ public final class MatroskaExtractor implements Extractor { } } - /* package */ void endMasterElement(int id) throws ParserException { + /** + * Called when the end of a master element is encountered. + * + * @see EbmlProcessor#endMasterElement(int) + */ + @CallSuper + protected void endMasterElement(int id) throws ParserException { switch (id) { case ID_SEGMENT_INFO: if (timecodeScale == C.TIME_UNSET) { @@ -571,20 +682,33 @@ public final class MatroskaExtractor implements Extractor { // We've skipped this block (due to incompatible track number). return; } - // If the ReferenceBlock element was not found for this sample, then it is a keyframe. - if (!sampleSeenReferenceBlock) { - blockFlags |= C.BUFFER_FLAG_KEY_FRAME; + // Commit sample metadata. + int sampleOffset = 0; + for (int i = 0; i < blockSampleCount; i++) { + sampleOffset += blockSampleSizes[i]; + } + Track track = tracks.get(blockTrackNumber); + for (int i = 0; i < blockSampleCount; i++) { + long sampleTimeUs = blockTimeUs + (i * track.defaultSampleDurationNs) / 1000; + int sampleFlags = blockFlags; + if (i == 0 && !blockHasReferenceBlock) { + // If the ReferenceBlock element was not found in this block, then the first frame is a + // keyframe. + sampleFlags |= C.BUFFER_FLAG_KEY_FRAME; + } + int sampleSize = blockSampleSizes[i]; + sampleOffset -= sampleSize; // The offset is to the end of the sample. + commitSampleToOutput(track, sampleTimeUs, sampleFlags, sampleSize, sampleOffset); } - commitSampleToOutput(tracks.get(blockTrackNumber), blockTimeUs); blockState = BLOCK_STATE_START; break; case ID_CONTENT_ENCODING: if (currentTrack.hasContentEncryption) { - if (currentTrack.encryptionKeyId == null) { + if (currentTrack.cryptoData == null) { throw new ParserException("Encrypted Track found but ContentEncKeyID was not found"); } - currentTrack.drmInitData = new DrmInitData( - new SchemeData(C.UUID_NIL, MimeTypes.VIDEO_WEBM, currentTrack.encryptionKeyId)); + currentTrack.drmInitData = new DrmInitData(new SchemeData(C.UUID_NIL, + MimeTypes.VIDEO_WEBM, currentTrack.cryptoData.encryptionKey)); } break; case ID_CONTENT_ENCODINGS: @@ -610,7 +734,13 @@ public final class MatroskaExtractor implements Extractor { } } - /* package */ void integerElement(int id, long value) throws ParserException { + /** + * Called when an integer element is encountered. + * + * @see EbmlProcessor#integerElement(int, long) + */ + @CallSuper + protected void integerElement(int id, long value) throws ParserException { switch (id) { case ID_EBML_READ_VERSION: // Validate that EBMLReadVersion is supported. This extractor only supports v1. @@ -651,10 +781,10 @@ public final class MatroskaExtractor implements Extractor { currentTrack.number = (int) value; break; case ID_FLAG_DEFAULT: - currentTrack.flagForced = value == 1; + currentTrack.flagDefault = value == 1; break; case ID_FLAG_FORCED: - currentTrack.flagDefault = value == 1; + currentTrack.flagForced = value == 1; break; case ID_TRACK_TYPE: currentTrack.type = (int) value; @@ -662,6 +792,9 @@ public final class MatroskaExtractor implements Extractor { case ID_DEFAULT_DURATION: currentTrack.defaultSampleDurationNs = (int) value; break; + case ID_MAX_BLOCK_ADDITION_ID: + currentTrack.maxBlockAdditionId = (int) value; + break; case ID_CODEC_DELAY: currentTrack.codecDelayNs = value; break; @@ -675,7 +808,7 @@ public final class MatroskaExtractor implements Extractor { currentTrack.audioBitDepth = (int) value; break; case ID_REFERENCE_BLOCK: - sampleSeenReferenceBlock = true; + blockHasReferenceBlock = true; break; case ID_CONTENT_ENCODING_ORDER: // This extractor only supports one ContentEncoding element and hence the order has to be 0. @@ -798,12 +931,39 @@ public final class MatroskaExtractor implements Extractor { case ID_MAX_FALL: currentTrack.maxFrameAverageLuminance = (int) value; break; + case ID_PROJECTION_TYPE: + switch ((int) value) { + case 0: + currentTrack.projectionType = C.PROJECTION_RECTANGULAR; + break; + case 1: + currentTrack.projectionType = C.PROJECTION_EQUIRECTANGULAR; + break; + case 2: + currentTrack.projectionType = C.PROJECTION_CUBEMAP; + break; + case 3: + currentTrack.projectionType = C.PROJECTION_MESH; + break; + default: + break; + } + break; + case ID_BLOCK_ADD_ID: + blockAdditionalId = (int) value; + break; default: break; } } - /* package */ void floatElement(int id, double value) { + /** + * Called when a float element is encountered. + * + * @see EbmlProcessor#floatElement(int, double) + */ + @CallSuper + protected void floatElement(int id, double value) throws ParserException { switch (id) { case ID_DURATION: durationTimecode = (long) value; @@ -841,12 +1001,27 @@ public final class MatroskaExtractor implements Extractor { case ID_LUMNINANCE_MIN: currentTrack.minMasteringLuminance = (float) value; break; + case ID_PROJECTION_POSE_YAW: + currentTrack.projectionPoseYaw = (float) value; + break; + case ID_PROJECTION_POSE_PITCH: + currentTrack.projectionPosePitch = (float) value; + break; + case ID_PROJECTION_POSE_ROLL: + currentTrack.projectionPoseRoll = (float) value; + break; default: break; } } - /* package */ void stringElement(int id, String value) throws ParserException { + /** + * Called when a string element is encountered. + * + * @see EbmlProcessor#stringElement(int, String) + */ + @CallSuper + protected void stringElement(int id, String value) throws ParserException { switch (id) { case ID_DOC_TYPE: // Validate that DocType is supported. @@ -854,6 +1029,9 @@ public final class MatroskaExtractor implements Extractor { throw new ParserException("DocType " + value + " not supported"); } break; + case ID_NAME: + currentTrack.name = value; + break; case ID_CODEC_ID: currentTrack.codecId = value; break; @@ -865,7 +1043,13 @@ public final class MatroskaExtractor implements Extractor { } } - /* package */ void binaryElement(int id, int contentSize, ExtractorInput input) + /** + * Called when a binary element is encountered. + * + * @see EbmlProcessor#binaryElement(int, int, ExtractorInput) + */ + @CallSuper + protected void binaryElement(int id, int contentSize, ExtractorInput input) throws IOException, InterruptedException { switch (id) { case ID_SEEK_ID: @@ -888,8 +1072,10 @@ public final class MatroskaExtractor implements Extractor { input.readFully(currentTrack.sampleStrippedBytes, 0, contentSize); break; case ID_CONTENT_ENCRYPTION_KEY_ID: - currentTrack.encryptionKeyId = new byte[contentSize]; - input.readFully(currentTrack.encryptionKeyId, 0, contentSize); + byte[] encryptionKey = new byte[contentSize]; + input.readFully(encryptionKey, 0, contentSize); + currentTrack.cryptoData = new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, encryptionKey, + 0, 0); // We assume patternless AES-CTR. break; case ID_SIMPLE_BLOCK: case ID_BLOCK: @@ -920,43 +1106,38 @@ public final class MatroskaExtractor implements Extractor { readScratch(input, 3); int lacing = (scratch.data[2] & 0x06) >> 1; if (lacing == LACING_NONE) { - blockLacingSampleCount = 1; - blockLacingSampleSizes = ensureArrayCapacity(blockLacingSampleSizes, 1); - blockLacingSampleSizes[0] = contentSize - blockTrackNumberLength - 3; + blockSampleCount = 1; + blockSampleSizes = ensureArrayCapacity(blockSampleSizes, 1); + blockSampleSizes[0] = contentSize - blockTrackNumberLength - 3; } else { - if (id != ID_SIMPLE_BLOCK) { - throw new ParserException("Lacing only supported in SimpleBlocks."); - } - // Read the sample count (1 byte). readScratch(input, 4); - blockLacingSampleCount = (scratch.data[3] & 0xFF) + 1; - blockLacingSampleSizes = - ensureArrayCapacity(blockLacingSampleSizes, blockLacingSampleCount); + blockSampleCount = (scratch.data[3] & 0xFF) + 1; + blockSampleSizes = ensureArrayCapacity(blockSampleSizes, blockSampleCount); if (lacing == LACING_FIXED_SIZE) { int blockLacingSampleSize = - (contentSize - blockTrackNumberLength - 4) / blockLacingSampleCount; - Arrays.fill(blockLacingSampleSizes, 0, blockLacingSampleCount, blockLacingSampleSize); + (contentSize - blockTrackNumberLength - 4) / blockSampleCount; + Arrays.fill(blockSampleSizes, 0, blockSampleCount, blockLacingSampleSize); } else if (lacing == LACING_XIPH) { int totalSamplesSize = 0; int headerSize = 4; - for (int sampleIndex = 0; sampleIndex < blockLacingSampleCount - 1; sampleIndex++) { - blockLacingSampleSizes[sampleIndex] = 0; + for (int sampleIndex = 0; sampleIndex < blockSampleCount - 1; sampleIndex++) { + blockSampleSizes[sampleIndex] = 0; int byteValue; do { readScratch(input, ++headerSize); byteValue = scratch.data[headerSize - 1] & 0xFF; - blockLacingSampleSizes[sampleIndex] += byteValue; + blockSampleSizes[sampleIndex] += byteValue; } while (byteValue == 0xFF); - totalSamplesSize += blockLacingSampleSizes[sampleIndex]; + totalSamplesSize += blockSampleSizes[sampleIndex]; } - blockLacingSampleSizes[blockLacingSampleCount - 1] = + blockSampleSizes[blockSampleCount - 1] = contentSize - blockTrackNumberLength - headerSize - totalSamplesSize; } else if (lacing == LACING_EBML) { int totalSamplesSize = 0; int headerSize = 4; - for (int sampleIndex = 0; sampleIndex < blockLacingSampleCount - 1; sampleIndex++) { - blockLacingSampleSizes[sampleIndex] = 0; + for (int sampleIndex = 0; sampleIndex < blockSampleCount - 1; sampleIndex++) { + blockSampleSizes[sampleIndex] = 0; readScratch(input, ++headerSize); if (scratch.data[headerSize - 1] == 0) { throw new ParserException("No valid varint length mask found"); @@ -984,11 +1165,13 @@ public final class MatroskaExtractor implements Extractor { throw new ParserException("EBML lacing sample size out of range."); } int intReadValue = (int) readValue; - blockLacingSampleSizes[sampleIndex] = sampleIndex == 0 - ? intReadValue : blockLacingSampleSizes[sampleIndex - 1] + intReadValue; - totalSamplesSize += blockLacingSampleSizes[sampleIndex]; + blockSampleSizes[sampleIndex] = + sampleIndex == 0 + ? intReadValue + : blockSampleSizes[sampleIndex - 1] + intReadValue; + totalSamplesSize += blockSampleSizes[sampleIndex]; } - blockLacingSampleSizes[blockLacingSampleCount - 1] = + blockSampleSizes[blockSampleCount - 1] = contentSize - blockTrackNumberLength - headerSize - totalSamplesSize; } else { // Lacing is always in the range 0--3. @@ -1004,51 +1187,93 @@ public final class MatroskaExtractor implements Extractor { blockFlags = (isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0) | (isInvisible ? C.BUFFER_FLAG_DECODE_ONLY : 0); blockState = BLOCK_STATE_DATA; - blockLacingSampleIndex = 0; + blockSampleIndex = 0; } if (id == ID_SIMPLE_BLOCK) { - // For SimpleBlock, we have metadata for each sample here. - while (blockLacingSampleIndex < blockLacingSampleCount) { - writeSampleData(input, track, blockLacingSampleSizes[blockLacingSampleIndex]); - long sampleTimeUs = this.blockTimeUs - + (blockLacingSampleIndex * track.defaultSampleDurationNs) / 1000; - commitSampleToOutput(track, sampleTimeUs); - blockLacingSampleIndex++; + // For SimpleBlock, we can write sample data and immediately commit the corresponding + // sample metadata. + while (blockSampleIndex < blockSampleCount) { + int sampleSize = writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); + long sampleTimeUs = + blockTimeUs + (blockSampleIndex * track.defaultSampleDurationNs) / 1000; + commitSampleToOutput(track, sampleTimeUs, blockFlags, sampleSize, /* offset= */ 0); + blockSampleIndex++; } blockState = BLOCK_STATE_START; } else { - // For Block, we send the metadata at the end of the BlockGroup element since we'll know - // if the sample is a keyframe or not only at that point. - writeSampleData(input, track, blockLacingSampleSizes[0]); + // For Block, we need to wait until the end of the BlockGroup element before committing + // sample metadata. This is so that we can handle ReferenceBlock (which can be used to + // infer whether the first sample in the block is a keyframe), and BlockAdditions (which + // can contain additional sample data to append) contained in the block group. Just output + // the sample data, storing the final sample sizes for when we commit the metadata. + while (blockSampleIndex < blockSampleCount) { + blockSampleSizes[blockSampleIndex] = + writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); + blockSampleIndex++; + } } break; + case ID_BLOCK_ADDITIONAL: + if (blockState != BLOCK_STATE_DATA) { + return; + } + handleBlockAdditionalData( + tracks.get(blockTrackNumber), blockAdditionalId, input, contentSize); + break; default: throw new ParserException("Unexpected id: " + id); } } - private void commitSampleToOutput(Track track, long timeUs) { - if (CODEC_ID_SUBRIP.equals(track.codecId)) { - writeSubripSample(track); + protected void handleBlockAdditionalData( + Track track, int blockAdditionalId, ExtractorInput input, int contentSize) + throws IOException, InterruptedException { + if (blockAdditionalId == BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 + && CODEC_ID_VP9.equals(track.codecId)) { + blockAdditionalData.reset(contentSize); + input.readFully(blockAdditionalData.data, 0, contentSize); + } else { + // Unhandled block additional data. + input.skipFully(contentSize); } - track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.encryptionKeyId); - sampleRead = true; - resetSample(); } - private void resetSample() { - sampleBytesRead = 0; - sampleBytesWritten = 0; - sampleCurrentNalBytesRemaining = 0; - sampleEncodingHandled = false; - sampleSignalByteRead = false; - samplePartitionCountRead = false; - samplePartitionCount = 0; - sampleSignalByte = (byte) 0; - sampleInitializationVectorRead = false; - sampleStrippedBytes.reset(); + private void commitSampleToOutput( + Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) { + if (track.trueHdSampleRechunker != null) { + track.trueHdSampleRechunker.sampleMetadata(track, timeUs, flags, size, offset); + } else { + if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) { + if (blockSampleCount > 1) { + Log.w(TAG, "Skipping subtitle sample in laced block."); + } else if (blockDurationUs == C.TIME_UNSET) { + Log.w(TAG, "Skipping subtitle sample with no duration."); + } else { + setSubtitleEndTime(track.codecId, blockDurationUs, subtitleSample.data); + // Note: If we ever want to support DRM protected subtitles then we'll need to output the + // appropriate encryption data here. + track.output.sampleData(subtitleSample, subtitleSample.limit()); + size += subtitleSample.limit(); + } + } + + if ((flags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) { + if (blockSampleCount > 1) { + // There were multiple samples in the block. Appending the additional data to the last + // sample doesn't make sense. Skip instead. + flags &= ~C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA; + } else { + // Append supplemental data. + int blockAdditionalSize = blockAdditionalData.limit(); + track.output.sampleData(blockAdditionalData, blockAdditionalSize); + size += blockAdditionalSize; + } + } + track.output.sampleMetadata(timeUs, flags, size, offset, track.cryptoData); + } + haveOutputSample = true; } /** @@ -1068,21 +1293,24 @@ public final class MatroskaExtractor implements Extractor { scratch.setLimit(requiredLength); } - private void writeSampleData(ExtractorInput input, Track track, int size) + /** + * Writes data for a single sample to the track output. + * + * @param input The input from which to read sample data. + * @param track The track to output the sample to. + * @param size The size of the sample data on the input side. + * @return The final size of the written sample. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private int writeSampleData(ExtractorInput input, Track track, int size) throws IOException, InterruptedException { if (CODEC_ID_SUBRIP.equals(track.codecId)) { - int sizeWithPrefix = SUBRIP_PREFIX.length + size; - if (subripSample.capacity() < sizeWithPrefix) { - // Initialize subripSample to contain the required prefix and have space to hold a subtitle - // twice as long as this one. - subripSample.data = Arrays.copyOf(SUBRIP_PREFIX, sizeWithPrefix + size); - } - input.readFully(subripSample.data, SUBRIP_PREFIX.length, size); - subripSample.setPosition(0); - subripSample.setLimit(sizeWithPrefix); - // Defer writing the data to the track output. We need to modify the sample data by setting - // the correct end timecode, which we might not have yet. - return; + writeSubtitleSampleData(input, SUBRIP_PREFIX, size); + return finishWriteSampleData(); + } else if (CODEC_ID_ASS.equals(track.codecId)) { + writeSubtitleSampleData(input, SSA_PREFIX, size); + return finishWriteSampleData(); } TrackOutput output = track.output; @@ -1171,6 +1399,21 @@ public final class MatroskaExtractor implements Extractor { // If the sample has header stripping, prepare to read/output the stripped bytes first. sampleStrippedBytes.reset(track.sampleStrippedBytes, track.sampleStrippedBytes.length); } + + if (track.maxBlockAdditionId > 0) { + blockFlags |= C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA; + blockAdditionalData.reset(); + // If there is supplemental data, the structure of the sample data is: + // sample size (4 bytes) || sample data || supplemental data + scratch.reset(/* limit= */ 4); + scratch.data[0] = (byte) ((size >> 24) & 0xFF); + scratch.data[1] = (byte) ((size >> 16) & 0xFF); + scratch.data[2] = (byte) ((size >> 8) & 0xFF); + scratch.data[3] = (byte) (size & 0xFF); + output.sampleData(scratch, 4); + sampleBytesWritten += 4; + } + sampleEncodingHandled = true; } size += sampleStrippedBytes.limit(); @@ -1192,8 +1435,9 @@ public final class MatroskaExtractor implements Extractor { while (sampleBytesRead < size) { if (sampleCurrentNalBytesRemaining == 0) { // Read the NAL length so that we know where we find the next one. - readToTarget(input, nalLengthData, nalUnitLengthFieldLengthDiff, - nalUnitLengthFieldLength); + writeToTarget( + input, nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + sampleBytesRead += nalUnitLengthFieldLength; nalLength.setPosition(0); sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt(); // Write a start code for the current NAL unit. @@ -1202,13 +1446,21 @@ public final class MatroskaExtractor implements Extractor { sampleBytesWritten += 4; } else { // Write the payload of the NAL unit. - sampleCurrentNalBytesRemaining -= - readToOutput(input, output, sampleCurrentNalBytesRemaining); + int bytesWritten = writeToOutput(input, output, sampleCurrentNalBytesRemaining); + sampleBytesRead += bytesWritten; + sampleBytesWritten += bytesWritten; + sampleCurrentNalBytesRemaining -= bytesWritten; } } } else { + if (track.trueHdSampleRechunker != null) { + Assertions.checkState(sampleStrippedBytes.limit() == 0); + track.trueHdSampleRechunker.startSample(input); + } while (sampleBytesRead < size) { - readToOutput(input, output, size - sampleBytesRead); + int bytesWritten = writeToOutput(input, output, size - sampleBytesRead); + sampleBytesRead += bytesWritten; + sampleBytesWritten += bytesWritten; } } @@ -1223,66 +1475,133 @@ public final class MatroskaExtractor implements Extractor { output.sampleData(vorbisNumPageSamples, 4); sampleBytesWritten += 4; } + + return finishWriteSampleData(); } - private void writeSubripSample(Track track) { - setSubripSampleEndTimecode(subripSample.data, blockDurationUs); - // Note: If we ever want to support DRM protected subtitles then we'll need to output the - // appropriate encryption data here. - track.output.sampleData(subripSample, subripSample.limit()); - sampleBytesWritten += subripSample.limit(); + /** + * Called by {@link #writeSampleData(ExtractorInput, Track, int)} when the sample has been + * written. Returns the final sample size and resets state for the next sample. + */ + private int finishWriteSampleData() { + int sampleSize = sampleBytesWritten; + resetWriteSampleData(); + return sampleSize; } - private static void setSubripSampleEndTimecode(byte[] subripSampleData, long timeUs) { - byte[] timeCodeData; - if (timeUs == C.TIME_UNSET) { - timeCodeData = SUBRIP_TIMECODE_EMPTY; + /** Resets state used by {@link #writeSampleData(ExtractorInput, Track, int)}. */ + private void resetWriteSampleData() { + sampleBytesRead = 0; + sampleBytesWritten = 0; + sampleCurrentNalBytesRemaining = 0; + sampleEncodingHandled = false; + sampleSignalByteRead = false; + samplePartitionCountRead = false; + samplePartitionCount = 0; + sampleSignalByte = (byte) 0; + sampleInitializationVectorRead = false; + sampleStrippedBytes.reset(); + } + + private void writeSubtitleSampleData(ExtractorInput input, byte[] samplePrefix, int size) + throws IOException, InterruptedException { + int sizeWithPrefix = samplePrefix.length + size; + if (subtitleSample.capacity() < sizeWithPrefix) { + // Initialize subripSample to contain the required prefix and have space to hold a subtitle + // twice as long as this one. + subtitleSample.data = Arrays.copyOf(samplePrefix, sizeWithPrefix + size); } else { - int hours = (int) (timeUs / 3600000000L); - timeUs -= (hours * 3600000000L); - int minutes = (int) (timeUs / 60000000); - timeUs -= (minutes * 60000000); - int seconds = (int) (timeUs / 1000000); - timeUs -= (seconds * 1000000); - int milliseconds = (int) (timeUs / 1000); - timeCodeData = Util.getUtf8Bytes(String.format(Locale.US, "%02d:%02d:%02d,%03d", hours, - minutes, seconds, milliseconds)); + System.arraycopy(samplePrefix, 0, subtitleSample.data, 0, samplePrefix.length); } - System.arraycopy(timeCodeData, 0, subripSampleData, SUBRIP_PREFIX_END_TIMECODE_OFFSET, - SUBRIP_TIMECODE_LENGTH); + input.readFully(subtitleSample.data, samplePrefix.length, size); + subtitleSample.reset(sizeWithPrefix); + // Defer writing the data to the track output. We need to modify the sample data by setting + // the correct end timecode, which we might not have yet. + } + + /** + * Overwrites the end timecode in {@code subtitleData} with the correctly formatted time derived + * from {@code durationUs}. + * + *

See documentation on {@link #SSA_DIALOGUE_FORMAT} and {@link #SUBRIP_PREFIX} for why we use + * the duration as the end timecode. + * + * @param codecId The subtitle codec; must be {@link #CODEC_ID_SUBRIP} or {@link #CODEC_ID_ASS}. + * @param durationUs The duration of the sample, in microseconds. + * @param subtitleData The subtitle sample in which to overwrite the end timecode (output + * parameter). + */ + private static void setSubtitleEndTime(String codecId, long durationUs, byte[] subtitleData) { + byte[] endTimecode; + int endTimecodeOffset; + switch (codecId) { + case CODEC_ID_SUBRIP: + endTimecode = + formatSubtitleTimecode( + durationUs, SUBRIP_TIMECODE_FORMAT, SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR); + endTimecodeOffset = SUBRIP_PREFIX_END_TIMECODE_OFFSET; + break; + case CODEC_ID_ASS: + endTimecode = + formatSubtitleTimecode( + durationUs, SSA_TIMECODE_FORMAT, SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR); + endTimecodeOffset = SSA_PREFIX_END_TIMECODE_OFFSET; + break; + default: + throw new IllegalArgumentException(); + } + System.arraycopy(endTimecode, 0, subtitleData, endTimecodeOffset, endTimecode.length); + } + + /** + * Formats {@code timeUs} using {@code timecodeFormat}, and sets it as the end timecode in {@code + * subtitleSampleData}. + */ + private static byte[] formatSubtitleTimecode( + long timeUs, String timecodeFormat, long lastTimecodeValueScalingFactor) { + Assertions.checkArgument(timeUs != C.TIME_UNSET); + byte[] timeCodeData; + int hours = (int) (timeUs / (3600 * C.MICROS_PER_SECOND)); + timeUs -= (hours * 3600 * C.MICROS_PER_SECOND); + int minutes = (int) (timeUs / (60 * C.MICROS_PER_SECOND)); + timeUs -= (minutes * 60 * C.MICROS_PER_SECOND); + int seconds = (int) (timeUs / C.MICROS_PER_SECOND); + timeUs -= (seconds * C.MICROS_PER_SECOND); + int lastValue = (int) (timeUs / lastTimecodeValueScalingFactor); + timeCodeData = + Util.getUtf8Bytes( + String.format(Locale.US, timecodeFormat, hours, minutes, seconds, lastValue)); + return timeCodeData; } /** * Writes {@code length} bytes of sample data into {@code target} at {@code offset}, consisting of * pending {@link #sampleStrippedBytes} and any remaining data read from {@code input}. */ - private void readToTarget(ExtractorInput input, byte[] target, int offset, int length) + private void writeToTarget(ExtractorInput input, byte[] target, int offset, int length) throws IOException, InterruptedException { int pendingStrippedBytes = Math.min(length, sampleStrippedBytes.bytesLeft()); input.readFully(target, offset + pendingStrippedBytes, length - pendingStrippedBytes); if (pendingStrippedBytes > 0) { sampleStrippedBytes.readBytes(target, offset, pendingStrippedBytes); } - sampleBytesRead += length; } /** * Outputs up to {@code length} bytes of sample data to {@code output}, consisting of either * {@link #sampleStrippedBytes} or data read from {@code input}. */ - private int readToOutput(ExtractorInput input, TrackOutput output, int length) + private int writeToOutput(ExtractorInput input, TrackOutput output, int length) throws IOException, InterruptedException { - int bytesRead; + int bytesWritten; int strippedBytesLeft = sampleStrippedBytes.bytesLeft(); if (strippedBytesLeft > 0) { - bytesRead = Math.min(length, strippedBytesLeft); - output.sampleData(sampleStrippedBytes, bytesRead); + bytesWritten = Math.min(length, strippedBytesLeft); + output.sampleData(sampleStrippedBytes, bytesWritten); } else { - bytesRead = output.sampleData(input, length, false); + bytesWritten = output.sampleData(input, length, false); } - sampleBytesRead += bytesRead; - sampleBytesWritten += bytesRead; - return bytesRead; + return bytesWritten; } /** @@ -1316,6 +1635,16 @@ public final class MatroskaExtractor implements Extractor { sizes[cuePointsSize - 1] = (int) (segmentContentPosition + segmentContentSize - offsets[cuePointsSize - 1]); durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1]; + + long lastDurationUs = durationsUs[cuePointsSize - 1]; + if (lastDurationUs <= 0) { + Log.w(TAG, "Discarding last cue point with unexpected duration: " + lastDurationUs); + sizes = Arrays.copyOf(sizes, sizes.length - 1); + offsets = Arrays.copyOf(offsets, offsets.length - 1); + durationsUs = Arrays.copyOf(durationsUs, durationsUs.length - 1); + timesUs = Arrays.copyOf(timesUs, timesUs.length - 1); + } + cueTimesUs = null; cueClusterPositions = null; return new ChunkIndex(sizes, offsets, durationsUs, timesUs); @@ -1357,6 +1686,7 @@ public final class MatroskaExtractor implements Extractor { private static boolean isCodecSupported(String codecId) { return CODEC_ID_VP8.equals(codecId) || CODEC_ID_VP9.equals(codecId) + || CODEC_ID_AV1.equals(codecId) || CODEC_ID_MPEG2.equals(codecId) || CODEC_ID_MPEG4_SP.equals(codecId) || CODEC_ID_MPEG4_ASP.equals(codecId) @@ -1380,6 +1710,7 @@ public final class MatroskaExtractor implements Extractor { || CODEC_ID_ACM.equals(codecId) || CODEC_ID_PCM_INT_LIT.equals(codecId) || CODEC_ID_SUBRIP.equals(codecId) + || CODEC_ID_ASS.equals(codecId) || CODEC_ID_VOBSUB.equals(codecId) || CODEC_ID_PGS.equals(codecId) || CODEC_ID_DVBSUB.equals(codecId); @@ -1400,12 +1731,11 @@ public final class MatroskaExtractor implements Extractor { } } - /** - * Passes events through to the outer {@link MatroskaExtractor}. - */ - private final class InnerEbmlReaderOutput implements EbmlReaderOutput { + /** Passes events through to the outer {@link MatroskaExtractor}. */ + private final class InnerEbmlProcessor implements EbmlProcessor { @Override + @ElementType public int getElementType(int id) { return MatroskaExtractor.this.getElementType(id); } @@ -1446,7 +1776,68 @@ public final class MatroskaExtractor implements Extractor { throws IOException, InterruptedException { MatroskaExtractor.this.binaryElement(id, contentsSize, input); } + } + /** + * Rechunks TrueHD sample data into groups of {@link Ac3Util#TRUEHD_RECHUNK_SAMPLE_COUNT} samples. + */ + private static final class TrueHdSampleRechunker { + + private final byte[] syncframePrefix; + + private boolean foundSyncframe; + private int chunkSampleCount; + private long chunkTimeUs; + private @C.BufferFlags int chunkFlags; + private int chunkSize; + private int chunkOffset; + + public TrueHdSampleRechunker() { + syncframePrefix = new byte[Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH]; + } + + public void reset() { + foundSyncframe = false; + chunkSampleCount = 0; + } + + public void startSample(ExtractorInput input) throws IOException, InterruptedException { + if (foundSyncframe) { + return; + } + input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH); + input.resetPeekPosition(); + if (Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == 0) { + return; + } + foundSyncframe = true; + } + + public void sampleMetadata( + Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) { + if (!foundSyncframe) { + return; + } + if (chunkSampleCount++ == 0) { + // This is the first sample in the chunk. + chunkTimeUs = timeUs; + chunkFlags = flags; + chunkSize = 0; + } + chunkSize += size; + chunkOffset = offset; // The offset is to the end of the sample. + if (chunkSampleCount >= Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { + outputPendingSampleMetadata(track); + } + } + + public void outputPendingSampleMetadata(Track track) { + if (chunkSampleCount > 0) { + track.output.sampleMetadata( + chunkTimeUs, chunkFlags, chunkSize, chunkOffset, track.cryptoData); + chunkSampleCount = 0; + } + } } private static final class Track { @@ -1464,13 +1855,15 @@ public final class MatroskaExtractor implements Extractor { private static final int DEFAULT_MAX_FALL = 200; // nits. // Common elements. + public String name; public String codecId; public int number; public int type; public int defaultSampleDurationNs; + public int maxBlockAdditionId; public boolean hasContentEncryption; public byte[] sampleStrippedBytes; - public byte[] encryptionKeyId; + public TrackOutput.CryptoData cryptoData; public byte[] codecPrivate; public DrmInitData drmInitData; @@ -1480,6 +1873,10 @@ public final class MatroskaExtractor implements Extractor { public int displayWidth = Format.NO_VALUE; public int displayHeight = Format.NO_VALUE; public int displayUnit = DISPLAY_UNIT_PIXELS; + @C.Projection public int projectionType = Format.NO_VALUE; + public float projectionPoseYaw = 0f; + public float projectionPosePitch = 0f; + public float projectionPoseRoll = 0f; public byte[] projectionData = null; @C.StereoMode public int stereoMode = Format.NO_VALUE; @@ -1509,6 +1906,7 @@ public final class MatroskaExtractor implements Extractor { public int sampleRate = 8000; public long codecDelayNs = 0; public long seekPreRollNs = 0; + @Nullable public TrueHdSampleRechunker trueHdSampleRechunker; // Text elements. public boolean flagForced; @@ -1519,9 +1917,7 @@ public final class MatroskaExtractor implements Extractor { public TrackOutput output; public int nalUnitLengthFieldLength; - /** - * Initializes the track with an output. - */ + /** Initializes the track with an output. */ public void initializeOutput(ExtractorOutput output, int trackId) throws ParserException { String mimeType; int maxInputSize = Format.NO_VALUE; @@ -1534,6 +1930,9 @@ public final class MatroskaExtractor implements Extractor { case CODEC_ID_VP9: mimeType = MimeTypes.VIDEO_VP9; break; + case CODEC_ID_AV1: + mimeType = MimeTypes.VIDEO_AV1; + break; case CODEC_ID_MPEG2: mimeType = MimeTypes.VIDEO_MPEG2; break; @@ -1557,8 +1956,9 @@ public final class MatroskaExtractor implements Extractor { nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; break; case CODEC_ID_FOURCC: - initializationData = parseFourCcVc1Private(new ParsableByteArray(codecPrivate)); - mimeType = initializationData == null ? MimeTypes.VIDEO_UNKNOWN : MimeTypes.VIDEO_VC1; + Pair> pair = parseFourCcPrivate(new ParsableByteArray(codecPrivate)); + mimeType = pair.first; + initializationData = pair.second; break; case CODEC_ID_THEORA: // TODO: This can be set to the real mimeType if/when we work out what initializationData @@ -1576,9 +1976,9 @@ public final class MatroskaExtractor implements Extractor { initializationData = new ArrayList<>(3); initializationData.add(codecPrivate); initializationData.add( - ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(codecDelayNs).array()); + ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(codecDelayNs).array()); initializationData.add( - ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(seekPreRollNs).array()); + ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(seekPreRollNs).array()); break; case CODEC_ID_AAC: mimeType = MimeTypes.AUDIO_AAC; @@ -1600,6 +2000,7 @@ public final class MatroskaExtractor implements Extractor { break; case CODEC_ID_TRUEHD: mimeType = MimeTypes.AUDIO_TRUEHD; + trueHdSampleRechunker = new TrueHdSampleRechunker(); break; case CODEC_ID_DTS: case CODEC_ID_DTS_EXPRESS: @@ -1614,24 +2015,35 @@ public final class MatroskaExtractor implements Extractor { break; case CODEC_ID_ACM: mimeType = MimeTypes.AUDIO_RAW; - if (!parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) { - throw new ParserException("Non-PCM MS/ACM is unsupported"); - } - pcmEncoding = Util.getPcmEncoding(audioBitDepth); - if (pcmEncoding == C.ENCODING_INVALID) { - throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth); + if (parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) { + pcmEncoding = Util.getPcmEncoding(audioBitDepth); + if (pcmEncoding == C.ENCODING_INVALID) { + pcmEncoding = Format.NO_VALUE; + mimeType = MimeTypes.AUDIO_UNKNOWN; + Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to " + + mimeType); + } + } else { + mimeType = MimeTypes.AUDIO_UNKNOWN; + Log.w(TAG, "Non-PCM MS/ACM is unsupported. Setting mimeType to " + mimeType); } break; case CODEC_ID_PCM_INT_LIT: mimeType = MimeTypes.AUDIO_RAW; pcmEncoding = Util.getPcmEncoding(audioBitDepth); if (pcmEncoding == C.ENCODING_INVALID) { - throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth); + pcmEncoding = Format.NO_VALUE; + mimeType = MimeTypes.AUDIO_UNKNOWN; + Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to " + + mimeType); } break; case CODEC_ID_SUBRIP: mimeType = MimeTypes.APPLICATION_SUBRIP; break; + case CODEC_ID_ASS: + mimeType = MimeTypes.TEXT_SSA; + break; case CODEC_ID_VOBSUB: mimeType = MimeTypes.APPLICATION_VOBSUB; initializationData = Collections.singletonList(codecPrivate); @@ -1676,20 +2088,75 @@ public final class MatroskaExtractor implements Extractor { byte[] hdrStaticInfo = getHdrStaticInfo(); colorInfo = new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo); } - format = Format.createVideoSampleFormat(Integer.toString(trackId), mimeType, null, - Format.NO_VALUE, maxInputSize, width, height, Format.NO_VALUE, initializationData, - Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, colorInfo, - drmInitData); + int rotationDegrees = Format.NO_VALUE; + // Some HTC devices signal rotation in track names. + if ("htc_video_rotA-000".equals(name)) { + rotationDegrees = 0; + } else if ("htc_video_rotA-090".equals(name)) { + rotationDegrees = 90; + } else if ("htc_video_rotA-180".equals(name)) { + rotationDegrees = 180; + } else if ("htc_video_rotA-270".equals(name)) { + rotationDegrees = 270; + } + if (projectionType == C.PROJECTION_RECTANGULAR + && Float.compare(projectionPoseYaw, 0f) == 0 + && Float.compare(projectionPosePitch, 0f) == 0) { + // The range of projectionPoseRoll is [-180, 180]. + if (Float.compare(projectionPoseRoll, 0f) == 0) { + rotationDegrees = 0; + } else if (Float.compare(projectionPosePitch, 90f) == 0) { + rotationDegrees = 90; + } else if (Float.compare(projectionPosePitch, -180f) == 0 + || Float.compare(projectionPosePitch, 180f) == 0) { + rotationDegrees = 180; + } else if (Float.compare(projectionPosePitch, -90f) == 0) { + rotationDegrees = 270; + } + } + format = + Format.createVideoSampleFormat( + Integer.toString(trackId), + mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + maxInputSize, + width, + height, + /* frameRate= */ Format.NO_VALUE, + initializationData, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + drmInitData); } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) { type = C.TRACK_TYPE_TEXT; + format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, selectionFlags, + language, drmInitData); + } else if (MimeTypes.TEXT_SSA.equals(mimeType)) { + type = C.TRACK_TYPE_TEXT; + initializationData = new ArrayList<>(2); + initializationData.add(SSA_DIALOGUE_FORMAT); + initializationData.add(codecPrivate); format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, - Format.NO_VALUE, selectionFlags, language, drmInitData); + Format.NO_VALUE, selectionFlags, language, Format.NO_VALUE, drmInitData, + Format.OFFSET_SAMPLE_RELATIVE, initializationData); } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType) || MimeTypes.APPLICATION_PGS.equals(mimeType) || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)) { type = C.TRACK_TYPE_TEXT; - format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null, - Format.NO_VALUE, initializationData, language, drmInitData); + format = + Format.createImageSampleFormat( + Integer.toString(trackId), + mimeType, + null, + Format.NO_VALUE, + selectionFlags, + initializationData, + language, + drmInitData); } else { throw new ParserException("Unexpected MIME type."); } @@ -1698,9 +2165,22 @@ public final class MatroskaExtractor implements Extractor { this.output.format(format); } - /** - * Returns the HDR Static Info as defined in CTA-861.3. - */ + /** Forces any pending sample metadata to be flushed to the output. */ + public void outputPendingSampleMetadata() { + if (trueHdSampleRechunker != null) { + trueHdSampleRechunker.outputPendingSampleMetadata(this); + } + } + + /** Resets any state stored in the track in response to a seek. */ + public void reset() { + if (trueHdSampleRechunker != null) { + trueHdSampleRechunker.reset(); + } + } + + /** Returns the HDR Static Info as defined in CTA-861.3. */ + @Nullable private byte[] getHdrStaticInfo() { // Are all fields present. if (primaryRChromaticityX == Format.NO_VALUE || primaryRChromaticityY == Format.NO_VALUE @@ -1713,7 +2193,7 @@ public final class MatroskaExtractor implements Extractor { } byte[] hdrStaticInfoData = new byte[25]; - ByteBuffer hdrStaticInfo = ByteBuffer.wrap(hdrStaticInfoData); + ByteBuffer hdrStaticInfo = ByteBuffer.wrap(hdrStaticInfoData).order(ByteOrder.LITTLE_ENDIAN); hdrStaticInfo.put((byte) 0); // Type. hdrStaticInfo.putShort((short) ((primaryRChromaticityX * MAX_CHROMATICITY) + 0.5f)); hdrStaticInfo.putShort((short) ((primaryRChromaticityY * MAX_CHROMATICITY) + 0.5f)); @@ -1732,39 +2212,44 @@ public final class MatroskaExtractor implements Extractor { /** * Builds initialization data for a {@link Format} from FourCC codec private data. - *

- * VC1 is the only supported compression type. * - * @return The initialization data for the {@link Format}, or null if the compression type is - * not VC1. + * @return The codec mime type and initialization data. If the compression type is not supported + * then the mime type is set to {@link MimeTypes#VIDEO_UNKNOWN} and the initialization data + * is {@code null}. * @throws ParserException If the initialization data could not be built. */ - private static List parseFourCcVc1Private(ParsableByteArray buffer) + private static Pair> parseFourCcPrivate(ParsableByteArray buffer) throws ParserException { try { buffer.skipBytes(16); // size(4), width(4), height(4), planes(2), bitcount(2). long compression = buffer.readLittleEndianUnsignedInt(); - if (compression != FOURCC_COMPRESSION_VC1) { - return null; - } - - // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20 - // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4). - int startOffset = buffer.getPosition() + 20; - byte[] bufferData = buffer.data; - for (int offset = startOffset; offset < bufferData.length - 4; offset++) { - if (bufferData[offset] == 0x00 && bufferData[offset + 1] == 0x00 - && bufferData[offset + 2] == 0x01 && bufferData[offset + 3] == 0x0F) { - // We've found the initialization data. - byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length); - return Collections.singletonList(initializationData); + if (compression == FOURCC_COMPRESSION_DIVX) { + return new Pair<>(MimeTypes.VIDEO_DIVX, null); + } else if (compression == FOURCC_COMPRESSION_H263) { + return new Pair<>(MimeTypes.VIDEO_H263, null); + } else if (compression == FOURCC_COMPRESSION_VC1) { + // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20 + // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4). + int startOffset = buffer.getPosition() + 20; + byte[] bufferData = buffer.data; + for (int offset = startOffset; offset < bufferData.length - 4; offset++) { + if (bufferData[offset] == 0x00 + && bufferData[offset + 1] == 0x00 + && bufferData[offset + 2] == 0x01 + && bufferData[offset + 3] == 0x0F) { + // We've found the initialization data. + byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length); + return new Pair<>(MimeTypes.VIDEO_VC1, Collections.singletonList(initializationData)); + } } + throw new ParserException("Failed to find FourCC VC1 initialization data"); } - - throw new ParserException("Failed to find FourCC VC1 initialization data"); } catch (ArrayIndexOutOfBoundsException e) { - throw new ParserException("Error parsing FourCC VC1 codec private"); + throw new ParserException("Error parsing FourCC private data"); } + + Log.w(TAG, "Unknown FourCC. Setting mimeType to " + MimeTypes.VIDEO_UNKNOWN); + return new Pair<>(MimeTypes.VIDEO_UNKNOWN, null); } /** diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.java index f3e12286daa3..f84cd084a3fe 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.java @@ -40,7 +40,7 @@ import java.io.IOException; } /** - * @see org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor#sniff(ExtractorInput) + * @see com.google.android.exoplayer2.extractor.Extractor#sniff(ExtractorInput) */ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { long inputLength = input.getLength(); @@ -78,8 +78,9 @@ import java.io.IOException; return false; } if (size != 0) { - input.advancePeekPosition((int) size); - peekLength += size; + int sizeInt = (int) size; + input.advancePeekPosition(sizeInt); + peekLength += sizeInt; } } return peekLength == headerStart + headerSize; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java index 9b470ab08240..1a442110e3e2 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java @@ -16,44 +16,31 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader; /** * MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate. */ -/* package */ final class ConstantBitrateSeeker implements Mp3Extractor.Seeker { +/* package */ final class ConstantBitrateSeeker extends ConstantBitrateSeekMap implements Seeker { - private static final int BITS_PER_BYTE = 8; - - private final long firstFramePosition; - private final int bitrate; - private final long durationUs; - - public ConstantBitrateSeeker(long firstFramePosition, int bitrate, long inputLength) { - this.firstFramePosition = firstFramePosition; - this.bitrate = bitrate; - durationUs = inputLength == C.LENGTH_UNSET ? C.TIME_UNSET : getTimeUs(inputLength); - } - - @Override - public boolean isSeekable() { - return durationUs != C.TIME_UNSET; - } - - @Override - public long getPosition(long timeUs) { - return durationUs == C.TIME_UNSET ? 0 - : firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); + /** + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param firstFramePosition The position of the first frame in the stream. + * @param mpegAudioHeader The MPEG audio header associated with the first frame. + */ + public ConstantBitrateSeeker( + long inputLength, long firstFramePosition, MpegAudioHeader mpegAudioHeader) { + super(inputLength, firstFramePosition, mpegAudioHeader.bitrate, mpegAudioHeader.frameSize); } @Override public long getTimeUs(long position) { - return (Math.max(0, position - firstFramePosition) * C.MICROS_PER_SECOND * BITS_PER_BYTE) - / bitrate; + return getTimeUsAtPosition(position); } @Override - public long getDurationUs() { - return durationUs; + public long getDataEndPosition() { + return C.POSITION_UNSET; } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java new file mode 100644 index 000000000000..662ded4ec376 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3; + +import android.util.Pair; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.MlltFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** MP3 seeker that uses metadata from an {@link MlltFrame}. */ +/* package */ final class MlltSeeker implements Seeker { + + /** + * Returns an {@link MlltSeeker} for seeking in the stream. + * + * @param firstFramePosition The position of the start of the first frame in the stream. + * @param mlltFrame The MLLT frame with seeking metadata. + * @return An {@link MlltSeeker} for seeking in the stream. + */ + public static MlltSeeker create(long firstFramePosition, MlltFrame mlltFrame) { + int referenceCount = mlltFrame.bytesDeviations.length; + long[] referencePositions = new long[1 + referenceCount]; + long[] referenceTimesMs = new long[1 + referenceCount]; + referencePositions[0] = firstFramePosition; + referenceTimesMs[0] = 0; + long position = firstFramePosition; + long timeMs = 0; + for (int i = 1; i <= referenceCount; i++) { + position += mlltFrame.bytesBetweenReference + mlltFrame.bytesDeviations[i - 1]; + timeMs += mlltFrame.millisecondsBetweenReference + mlltFrame.millisecondsDeviations[i - 1]; + referencePositions[i] = position; + referenceTimesMs[i] = timeMs; + } + return new MlltSeeker(referencePositions, referenceTimesMs); + } + + private final long[] referencePositions; + private final long[] referenceTimesMs; + private final long durationUs; + + private MlltSeeker(long[] referencePositions, long[] referenceTimesMs) { + this.referencePositions = referencePositions; + this.referenceTimesMs = referenceTimesMs; + // Use the last reference point as the duration, as extrapolating variable bitrate at the end of + // the stream may give a large error. + durationUs = C.msToUs(referenceTimesMs[referenceTimesMs.length - 1]); + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + timeUs = Util.constrainValue(timeUs, 0, durationUs); + Pair timeMsAndPosition = + linearlyInterpolate(C.usToMs(timeUs), referenceTimesMs, referencePositions); + timeUs = C.msToUs(timeMsAndPosition.first); + long position = timeMsAndPosition.second; + return new SeekPoints(new SeekPoint(timeUs, position)); + } + + @Override + public long getTimeUs(long position) { + Pair positionAndTimeMs = + linearlyInterpolate(position, referencePositions, referenceTimesMs); + return C.msToUs(positionAndTimeMs.second); + } + + @Override + public long getDurationUs() { + return durationUs; + } + + /** + * Given a set of reference points as coordinates in {@code xReferences} and {@code yReferences} + * and an x-axis value, linearly interpolates between corresponding reference points to give a + * y-axis value. + * + * @param x The x-axis value for which a y-axis value is needed. + * @param xReferences x coordinates of reference points. + * @param yReferences y coordinates of reference points. + * @return The linearly interpolated y-axis value. + */ + private static Pair linearlyInterpolate( + long x, long[] xReferences, long[] yReferences) { + int previousReferenceIndex = + Util.binarySearchFloor(xReferences, x, /* inclusive= */ true, /* stayInBounds= */ true); + long xPreviousReference = xReferences[previousReferenceIndex]; + long yPreviousReference = yReferences[previousReferenceIndex]; + int nextReferenceIndex = previousReferenceIndex + 1; + if (nextReferenceIndex == xReferences.length) { + return Pair.create(xPreviousReference, yPreviousReference); + } else { + long xNextReference = xReferences[nextReferenceIndex]; + long yNextReference = yReferences[nextReferenceIndex]; + double proportion = + xNextReference == xPreviousReference + ? 0.0 + : ((double) x - xPreviousReference) / (xNextReference - xPreviousReference); + long y = (long) (proportion * (yNextReference - yPreviousReference)) + yPreviousReference; + return Pair.create(x, y); + } + } + + @Override + public long getDataEndPosition() { + return C.POSITION_UNSET; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 0486d52db8ce..2829a1e51913 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -15,7 +15,8 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3; -import android.support.annotation.IntDef; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; @@ -24,41 +25,39 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorI import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.GaplessInfoHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Id3Peeker; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3.Seeker.UnseekableSeeker; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.MlltFrame; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** - * Extracts data from an MP3 file. + * Extracts data from the MP3 container format. */ public final class Mp3Extractor implements Extractor { - /** - * Factory for {@link Mp3Extractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new Mp3Extractor()}; - } - - }; + /** Factory for {@link Mp3Extractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp3Extractor()}; /** - * Flags controlling the behavior of the extractor. + * Flags controlling the behavior of the extractor. Possible flag values are {@link + * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING} and {@link #FLAG_DISABLE_ID3_METADATA}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_DISABLE_ID3_METADATA}) + @IntDef( + flag = true, + value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_DISABLE_ID3_METADATA}) public @interface Flags {} /** * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would @@ -71,6 +70,12 @@ public final class Mp3Extractor implements Extractor { */ public static final int FLAG_DISABLE_ID3_METADATA = 2; + /** Predicate that matches ID3 frames containing only required gapless/seeking metadata. */ + private static final FramePredicate REQUIRED_ID3_FRAME_PREDICATE = + (majorVersion, id0, id1, id2, id3) -> + ((id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2)) + || (id0 == 'M' && id1 == 'L' && id2 == 'L' && (id3 == 'T' || majorVersion == 2))); + /** * The maximum number of bytes to search when synchronizing, before giving up. */ @@ -78,7 +83,7 @@ public final class Mp3Extractor implements Extractor { /** * The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up. */ - private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES; + private static final int MAX_SNIFF_BYTES = 16 * 1024; /** * Maximum length of data read into {@link #scratch}. */ @@ -87,16 +92,19 @@ public final class Mp3Extractor implements Extractor { /** * Mask that includes the audio header values that must match between frames. */ - private static final int HEADER_MASK = 0xFFFE0C00; - private static final int XING_HEADER = Util.getIntegerCodeForString("Xing"); - private static final int INFO_HEADER = Util.getIntegerCodeForString("Info"); - private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI"); + private static final int MPEG_AUDIO_HEADER_MASK = 0xFFFE0C00; + + private static final int SEEK_HEADER_XING = 0x58696e67; + private static final int SEEK_HEADER_INFO = 0x496e666f; + private static final int SEEK_HEADER_VBRI = 0x56425249; + private static final int SEEK_HEADER_UNSET = 0; @Flags private final int flags; private final long forcedFirstSampleTimestampUs; private final ParsableByteArray scratch; private final MpegAudioHeader synchronizedHeader; private final GaplessInfoHolder gaplessInfoHolder; + private final Id3Peeker id3Peeker; // Extractor outputs. private ExtractorOutput extractorOutput; @@ -105,21 +113,18 @@ public final class Mp3Extractor implements Extractor { private int synchronizedHeaderData; private Metadata metadata; - private Seeker seeker; + @Nullable private Seeker seeker; + private boolean disableSeeking; private long basisTimeUs; private long samplesRead; + private long firstSamplePosition; private int sampleBytesRemaining; - /** - * Constructs a new {@link Mp3Extractor}. - */ public Mp3Extractor() { this(0); } /** - * Constructs a new {@link Mp3Extractor}. - * * @param flags Flags that control the extractor's behavior. */ public Mp3Extractor(@Flags int flags) { @@ -127,8 +132,6 @@ public final class Mp3Extractor implements Extractor { } /** - * Constructs a new {@link Mp3Extractor}. - * * @param flags Flags that control the extractor's behavior. * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or * {@link C#TIME_UNSET} if forcing is not required. @@ -140,8 +143,11 @@ public final class Mp3Extractor implements Extractor { synchronizedHeader = new MpegAudioHeader(); gaplessInfoHolder = new GaplessInfoHolder(); basisTimeUs = C.TIME_UNSET; + id3Peeker = new Id3Peeker(); } + // Extractor implementation. + @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { return synchronize(input, true); @@ -178,26 +184,73 @@ public final class Mp3Extractor implements Extractor { } } if (seeker == null) { - seeker = setupSeeker(input); + // Read past any seek frame and set the seeker based on metadata or a seek frame. Metadata + // takes priority as it can provide greater precision. + Seeker seekFrameSeeker = maybeReadSeekFrame(input); + Seeker metadataSeeker = maybeHandleSeekMetadata(metadata, input.getPosition()); + + if (disableSeeking) { + seeker = new UnseekableSeeker(); + } else { + if (metadataSeeker != null) { + seeker = metadataSeeker; + } else if (seekFrameSeeker != null) { + seeker = seekFrameSeeker; + } + if (seeker == null + || (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { + seeker = getConstantBitrateSeeker(input); + } + } extractorOutput.seekMap(seeker); - trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, - Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, - synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding, null, null, 0, null, - (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); + trackOutput.format( + Format.createAudioSampleFormat( + /* id= */ null, + synchronizedHeader.mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + MpegAudioHeader.MAX_FRAME_SIZE_BYTES, + synchronizedHeader.channels, + synchronizedHeader.sampleRate, + /* pcmEncoding= */ Format.NO_VALUE, + gaplessInfoHolder.encoderDelay, + gaplessInfoHolder.encoderPadding, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null, + (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); + firstSamplePosition = input.getPosition(); + } else if (firstSamplePosition != 0) { + long inputPosition = input.getPosition(); + if (inputPosition < firstSamplePosition) { + // Skip past the seek frame. + input.skipFully((int) (firstSamplePosition - inputPosition)); + } } return readSample(input); } + /** + * Disables the extractor from being able to seek through the media. + * + *

Please note that this needs to be called before {@link #read}. + */ + public void disableSeeking() { + disableSeeking = true; + } + + // Internal methods. + private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException { if (sampleBytesRemaining == 0) { extractorInput.resetPeekPosition(); - if (!extractorInput.peekFully(scratch.data, 0, 4, true)) { + if (peekEndOfStreamOrHeader(extractorInput)) { return RESULT_END_OF_INPUT; } scratch.setPosition(0); int sampleHeaderData = scratch.readInt(); - if ((sampleHeaderData & HEADER_MASK) != (synchronizedHeaderData & HEADER_MASK) + if (!headersMatch(sampleHeaderData, synchronizedHeaderData) || MpegAudioHeader.getFrameSize(sampleHeaderData) == C.LENGTH_UNSET) { // We have lost synchronization, so attempt to resynchronize starting at the next byte. extractorInput.skipFully(1); @@ -239,22 +292,33 @@ public final class Mp3Extractor implements Extractor { int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES; input.resetPeekPosition(); if (input.getPosition() == 0) { - peekId3Data(input); + // We need to parse enough ID3 metadata to retrieve any gapless/seeking playback information + // even if ID3 metadata parsing is disabled. + boolean parseAllId3Frames = (flags & FLAG_DISABLE_ID3_METADATA) == 0; + Id3Decoder.FramePredicate id3FramePredicate = + parseAllId3Frames ? null : REQUIRED_ID3_FRAME_PREDICATE; + metadata = id3Peeker.peekId3Data(input, id3FramePredicate); + if (metadata != null) { + gaplessInfoHolder.setFromMetadata(metadata); + } peekedId3Bytes = (int) input.getPeekPosition(); if (!sniffing) { input.skipFully(peekedId3Bytes); } } while (true) { - if (!input.peekFully(scratch.data, 0, 4, validFrameCount > 0)) { - // We reached the end of the stream but found at least one valid frame. - break; + if (peekEndOfStreamOrHeader(input)) { + if (validFrameCount > 0) { + // We reached the end of the stream but found at least one valid frame. + break; + } + throw new EOFException(); } scratch.setPosition(0); int headerData = scratch.readInt(); int frameSize; if ((candidateSynchronizedHeaderData != 0 - && (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK)) + && !headersMatch(headerData, candidateSynchronizedHeaderData)) || (frameSize = MpegAudioHeader.getFrameSize(headerData)) == C.LENGTH_UNSET) { // The header doesn't match the candidate header or is invalid. Try the next byte offset. if (searchedBytes++ == searchLimitBytes) { @@ -294,80 +358,48 @@ public final class Mp3Extractor implements Extractor { } /** - * Peeks ID3 data from the input, including gapless playback information. - * - * @param input The {@link ExtractorInput} from which data should be peeked. - * @throws IOException If an error occurred peeking from the input. - * @throws InterruptedException If the thread was interrupted. + * Returns whether the extractor input is peeking the end of the stream. If {@code false}, + * populates the scratch buffer with the next four bytes. */ - private void peekId3Data(ExtractorInput input) throws IOException, InterruptedException { - int peekedId3Bytes = 0; - while (true) { - input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH); - scratch.setPosition(0); - if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) { - // Not an ID3 tag. - break; + private boolean peekEndOfStreamOrHeader(ExtractorInput extractorInput) + throws IOException, InterruptedException { + if (seeker != null) { + long dataEndPosition = seeker.getDataEndPosition(); + if (dataEndPosition != C.POSITION_UNSET + && extractorInput.getPeekPosition() > dataEndPosition - 4) { + return true; } - scratch.skipBytes(3); // Skip major version, minor version and flags. - int framesLength = scratch.readSynchSafeInt(); - int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength; - - if (metadata == null) { - byte[] id3Data = new byte[tagLength]; - System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH); - input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength); - // We need to parse enough ID3 metadata to retrieve any gapless playback information even - // if ID3 metadata parsing is disabled. - Id3Decoder.FramePredicate id3FramePredicate = (flags & FLAG_DISABLE_ID3_METADATA) != 0 - ? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null; - metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength); - if (metadata != null) { - gaplessInfoHolder.setFromMetadata(metadata); - } - } else { - input.advancePeekPosition(framesLength); - } - - peekedId3Bytes += tagLength; } - - input.resetPeekPosition(); - input.advancePeekPosition(peekedId3Bytes); + try { + return !extractorInput.peekFully( + scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true); + } catch (EOFException e) { + return true; + } } /** - * Returns a {@link Seeker} to seek using metadata read from {@code input}, which should provide - * data from the start of the first frame in the stream. On returning, the input's position will - * be set to the start of the first frame of audio. + * Consumes the next frame from the {@code input} if it contains VBRI or Xing seeking metadata, + * returning a {@link Seeker} if the metadata was present and valid, or {@code null} otherwise. + * After this method returns, the input position is the start of the first frame of audio. * * @param input The {@link ExtractorInput} from which to read. + * @return A {@link Seeker} if seeking metadata was present and valid, or {@code null} otherwise. * @throws IOException Thrown if there was an error reading from the stream. Not expected if the * next two frames were already peeked during synchronization. * @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if * the next two frames were already peeked during synchronization. - * @return a {@link Seeker}. */ - private Seeker setupSeeker(ExtractorInput input) throws IOException, InterruptedException { - // Read the first frame which may contain a Xing or VBRI header with seeking metadata. + private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException, InterruptedException { ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize); input.peekFully(frame.data, 0, synchronizedHeader.frameSize); - - long position = input.getPosition(); - long length = input.getLength(); - int headerData = 0; - Seeker seeker = null; - - // Check if there is a Xing header. int xingBase = (synchronizedHeader.version & 1) != 0 ? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1 : (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5 - if (frame.limit() >= xingBase + 4) { - frame.setPosition(xingBase); - headerData = frame.readInt(); - } - if (headerData == XING_HEADER || headerData == INFO_HEADER) { - seeker = XingSeeker.create(synchronizedHeader, frame, position, length); + int seekHeader = getSeekFrameHeader(frame, xingBase); + Seeker seeker; + if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) { + seeker = XingSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { // If there is a Xing header, read gapless playback metadata at a fixed offset. input.resetPeekPosition(); @@ -377,44 +409,74 @@ public final class Mp3Extractor implements Extractor { gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24()); } input.skipFully(synchronizedHeader.frameSize); - } else if (frame.limit() >= 40) { - // Check if there is a VBRI header. - frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes. - headerData = frame.readInt(); - if (headerData == VBRI_HEADER) { - seeker = VbriSeeker.create(synchronizedHeader, frame, position, length); - input.skipFully(synchronizedHeader.frameSize); + if (seeker != null && !seeker.isSeekable() && seekHeader == SEEK_HEADER_INFO) { + // Fall back to constant bitrate seeking for Info headers missing a table of contents. + return getConstantBitrateSeeker(input); } - } - - if (seeker == null || (!seeker.isSeekable() - && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { - // Repopulate the synchronized header in case we had to skip an invalid seeking header, which - // would give an invalid CBR bitrate. + } else if (seekHeader == SEEK_HEADER_VBRI) { + seeker = VbriSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); + input.skipFully(synchronizedHeader.frameSize); + } else { // seekerHeader == SEEK_HEADER_UNSET + // This frame doesn't contain seeking information, so reset the peek position. + seeker = null; input.resetPeekPosition(); - input.peekFully(scratch.data, 0, 4); - scratch.setPosition(0); - MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); - seeker = new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, length); } - return seeker; } /** - * {@link SeekMap} that also allows mapping from position (byte offset) back to time, which can be - * used to work out the new sample basis timestamp after seeking and resynchronization. + * Peeks the next frame and returns a {@link ConstantBitrateSeeker} based on its bitrate. */ - /* package */ interface Seeker extends SeekMap { - - /** - * Maps a position (byte offset) to a corresponding sample timestamp. - * - * @param position A seek position (byte offset) relative to the start of the stream. - * @return The corresponding timestamp of the next sample to be read, in microseconds. - */ - long getTimeUs(long position); - + private Seeker getConstantBitrateSeeker(ExtractorInput input) + throws IOException, InterruptedException { + input.peekFully(scratch.data, 0, 4); + scratch.setPosition(0); + MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); + return new ConstantBitrateSeeker(input.getLength(), input.getPosition(), synchronizedHeader); } + /** + * Returns whether the headers match in those bits masked by {@link #MPEG_AUDIO_HEADER_MASK}. + */ + private static boolean headersMatch(int headerA, long headerB) { + return (headerA & MPEG_AUDIO_HEADER_MASK) == (headerB & MPEG_AUDIO_HEADER_MASK); + } + + /** + * Returns {@link #SEEK_HEADER_XING}, {@link #SEEK_HEADER_INFO} or {@link #SEEK_HEADER_VBRI} if + * the provided {@code frame} may have seeking metadata, or {@link #SEEK_HEADER_UNSET} otherwise. + * If seeking metadata is present, {@code frame}'s position is advanced past the header. + */ + private static int getSeekFrameHeader(ParsableByteArray frame, int xingBase) { + if (frame.limit() >= xingBase + 4) { + frame.setPosition(xingBase); + int headerData = frame.readInt(); + if (headerData == SEEK_HEADER_XING || headerData == SEEK_HEADER_INFO) { + return headerData; + } + } + if (frame.limit() >= 40) { + frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes. + if (frame.readInt() == SEEK_HEADER_VBRI) { + return SEEK_HEADER_VBRI; + } + } + return SEEK_HEADER_UNSET; + } + + @Nullable + private static MlltSeeker maybeHandleSeekMetadata(Metadata metadata, long firstFramePosition) { + if (metadata != null) { + int length = metadata.length(); + for (int i = 0; i < length; i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof MlltFrame) { + return MlltSeeker.create(firstFramePosition, (MlltFrame) entry); + } + } + } + return null; + } + + } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Seeker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Seeker.java new file mode 100644 index 000000000000..da0306cc6023 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Seeker.java @@ -0,0 +1,60 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; + +/** + * {@link SeekMap} that provides the end position of audio data and also allows mapping from + * position (byte offset) back to time, which can be used to work out the new sample basis timestamp + * after seeking and resynchronization. + */ +/* package */ interface Seeker extends SeekMap { + + /** + * Maps a position (byte offset) to a corresponding sample timestamp. + * + * @param position A seek position (byte offset) relative to the start of the stream. + * @return The corresponding timestamp of the next sample to be read, in microseconds. + */ + long getTimeUs(long position); + + /** + * Returns the position (byte offset) in the stream that is immediately after audio data, or + * {@link C#POSITION_UNSET} if not known. + */ + long getDataEndPosition(); + + /** A {@link Seeker} that does not support seeking through audio data. */ + /* package */ class UnseekableSeeker extends SeekMap.Unseekable implements Seeker { + + public UnseekableSeeker() { + super(/* durationUs= */ C.TIME_UNSET); + } + + @Override + public long getTimeUs(long position) { + return 0; + } + + @Override + public long getDataEndPosition() { + // Position unset as we do not know the data end position. Note that returning 0 doesn't work. + return C.POSITION_UNSET; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java index 1d8ec3f70594..8bb142f49666 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java @@ -15,31 +15,34 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; -/** - * MP3 seeker that uses metadata from a VBRI header. - */ -/* package */ final class VbriSeeker implements Mp3Extractor.Seeker { +/** MP3 seeker that uses metadata from a VBRI header. */ +/* package */ final class VbriSeeker implements Seeker { + + private static final String TAG = "VbriSeeker"; /** * Returns a {@link VbriSeeker} for seeking in the stream, if required information is present. * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the * caller should reset it. * + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param position The position of the start of this frame in the stream. * @param mpegAudioHeader The MPEG audio header associated with the frame. * @param frame The data in this audio frame, with its position set to immediately after the * 'VBRI' tag. - * @param position The position (byte offset) of the start of this frame in the stream. - * @param inputLength The length of the stream in bytes. * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ - public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, - long position, long inputLength) { + public static @Nullable VbriSeeker create( + long inputLength, long position, MpegAudioHeader mpegAudioHeader, ParsableByteArray frame) { frame.skipBytes(10); int numFrames = frame.readInt(); if (numFrames <= 0) { @@ -53,15 +56,15 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; int entrySize = frame.readUnsignedShort(); frame.skipBytes(2); - // Skip the frame containing the VBRI header. - position += mpegAudioHeader.frameSize; - + long minPosition = position + mpegAudioHeader.frameSize; // Read table of contents entries. - long[] timesUs = new long[entryCount + 1]; - long[] positions = new long[entryCount + 1]; - timesUs[0] = 0L; - positions[0] = position; - for (int index = 1; index < timesUs.length; index++) { + long[] timesUs = new long[entryCount]; + long[] positions = new long[entryCount]; + for (int index = 0; index < entryCount; index++) { + timesUs[index] = (index * durationUs) / entryCount; + // Ensure positions do not fall within the frame containing the VBRI header. This constraint + // will normally only apply to the first entry in the table. + positions[index] = Math.max(position, minPosition); int segmentSize; switch (entrySize) { case 1: @@ -80,21 +83,23 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; return null; } position += segmentSize * scale; - timesUs[index] = index * durationUs / entryCount; - positions[index] = - inputLength == C.LENGTH_UNSET ? position : Math.min(inputLength, position); } - return new VbriSeeker(timesUs, positions, durationUs); + if (inputLength != C.LENGTH_UNSET && inputLength != position) { + Log.w(TAG, "VBRI data size mismatch: " + inputLength + ", " + position); + } + return new VbriSeeker(timesUs, positions, durationUs, /* dataEndPosition= */ position); } private final long[] timesUs; private final long[] positions; private final long durationUs; + private final long dataEndPosition; - private VbriSeeker(long[] timesUs, long[] positions, long durationUs) { + private VbriSeeker(long[] timesUs, long[] positions, long durationUs, long dataEndPosition) { this.timesUs = timesUs; this.positions = positions; this.durationUs = durationUs; + this.dataEndPosition = dataEndPosition; } @Override @@ -103,8 +108,15 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; } @Override - public long getPosition(long timeUs) { - return positions[Util.binarySearchFloor(timesUs, timeUs, true, true)]; + public SeekPoints getSeekPoints(long timeUs) { + int tableIndex = Util.binarySearchFloor(timesUs, timeUs, true, true); + SeekPoint seekPoint = new SeekPoint(timesUs[tableIndex], positions[tableIndex]); + if (seekPoint.timeUs >= timeUs || tableIndex == timesUs.length - 1) { + return new SeekPoints(seekPoint); + } else { + SeekPoint nextSeekPoint = new SeekPoint(timesUs[tableIndex + 1], positions[tableIndex + 1]); + return new SeekPoints(seekPoint, nextSeekPoint); + } } @Override @@ -117,4 +129,8 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; return durationUs; } + @Override + public long getDataEndPosition() { + return dataEndPosition; + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java index 8f0e92ff3ff5..61568aac933b 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java @@ -15,34 +15,37 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; -/** - * MP3 seeker that uses metadata from a Xing header. - */ -/* package */ final class XingSeeker implements Mp3Extractor.Seeker { +/** MP3 seeker that uses metadata from a Xing header. */ +/* package */ final class XingSeeker implements Seeker { + + private static final String TAG = "XingSeeker"; /** * Returns a {@link XingSeeker} for seeking in the stream, if required information is present. * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the * caller should reset it. * + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param position The position of the start of this frame in the stream. * @param mpegAudioHeader The MPEG audio header associated with the frame. * @param frame The data in this audio frame, with its position set to immediately after the - * 'Xing' or 'Info' tag. - * @param position The position (byte offset) of the start of this frame in the stream. - * @param inputLength The length of the stream in bytes. + * 'Xing' or 'Info' tag. * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ - public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, - long position, long inputLength) { + public static @Nullable XingSeeker create( + long inputLength, long position, MpegAudioHeader mpegAudioHeader, ParsableByteArray frame) { int samplesPerFrame = mpegAudioHeader.samplesPerFrame; int sampleRate = mpegAudioHeader.sampleRate; - long firstFramePosition = position + mpegAudioHeader.frameSize; int flags = frame.readInt(); int frameCount; @@ -54,45 +57,60 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; sampleRate); if ((flags & 0x06) != 0x06) { // If the size in bytes or table of contents is missing, the stream is not seekable. - return new XingSeeker(firstFramePosition, durationUs, inputLength); + return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs); } - long sizeBytes = frame.readUnsignedIntToInt(); - frame.skipBytes(1); - long[] tableOfContents = new long[99]; - for (int i = 0; i < 99; i++) { + long dataSize = frame.readUnsignedIntToInt(); + long[] tableOfContents = new long[100]; + for (int i = 0; i < 100; i++) { tableOfContents[i] = frame.readUnsignedByte(); } // TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes: // delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4); // padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte(); - return new XingSeeker(firstFramePosition, durationUs, inputLength, tableOfContents, - sizeBytes, mpegAudioHeader.frameSize); + + if (inputLength != C.LENGTH_UNSET && inputLength != position + dataSize) { + Log.w(TAG, "XING data size mismatch: " + inputLength + ", " + (position + dataSize)); + } + return new XingSeeker( + position, mpegAudioHeader.frameSize, durationUs, dataSize, tableOfContents); } - private final long firstFramePosition; + private final long dataStartPosition; + private final int xingFrameSize; private final long durationUs; - private final long inputLength; - /** - * Entries are in the range [0, 255], but are stored as long integers for convenience. - */ - private final long[] tableOfContents; - private final long sizeBytes; - private final int headerSize; + /** Data size, including the XING frame. */ + private final long dataSize; - private XingSeeker(long firstFramePosition, long durationUs, long inputLength) { - this(firstFramePosition, durationUs, inputLength, null, 0, 0); + private final long dataEndPosition; + /** + * Entries are in the range [0, 255], but are stored as long integers for convenience. Null if the + * table of contents was missing from the header, in which case seeking is not be supported. + */ + @Nullable private final long[] tableOfContents; + + private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) { + this( + dataStartPosition, + xingFrameSize, + durationUs, + /* dataSize= */ C.LENGTH_UNSET, + /* tableOfContents= */ null); } - private XingSeeker(long firstFramePosition, long durationUs, long inputLength, - long[] tableOfContents, long sizeBytes, int headerSize) { - this.firstFramePosition = firstFramePosition; + private XingSeeker( + long dataStartPosition, + int xingFrameSize, + long durationUs, + long dataSize, + @Nullable long[] tableOfContents) { + this.dataStartPosition = dataStartPosition; + this.xingFrameSize = xingFrameSize; this.durationUs = durationUs; - this.inputLength = inputLength; this.tableOfContents = tableOfContents; - this.sizeBytes = sizeBytes; - this.headerSize = headerSize; + this.dataSize = dataSize; + dataEndPosition = dataSize == C.LENGTH_UNSET ? C.POSITION_UNSET : dataStartPosition + dataSize; } @Override @@ -101,55 +119,50 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; } @Override - public long getPosition(long timeUs) { + public SeekPoints getSeekPoints(long timeUs) { if (!isSeekable()) { - return firstFramePosition; + return new SeekPoints(new SeekPoint(0, dataStartPosition + xingFrameSize)); } - float percent = timeUs * 100f / durationUs; - float fx; - if (percent <= 0f) { - fx = 0f; - } else if (percent >= 100f) { - fx = 256f; + timeUs = Util.constrainValue(timeUs, 0, durationUs); + double percent = (timeUs * 100d) / durationUs; + double scaledPosition; + if (percent <= 0) { + scaledPosition = 0; + } else if (percent >= 100) { + scaledPosition = 256; } else { - int a = (int) percent; - float fa, fb; - if (a == 0) { - fa = 0f; - } else { - fa = tableOfContents[a - 1]; - } - if (a < 99) { - fb = tableOfContents[a]; - } else { - fb = 256f; - } - fx = fa + (fb - fa) * (percent - a); + int prevTableIndex = (int) percent; + long[] tableOfContents = Assertions.checkNotNull(this.tableOfContents); + double prevScaledPosition = tableOfContents[prevTableIndex]; + double nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1]; + // Linearly interpolate between the two scaled positions. + double interpolateFraction = percent - prevTableIndex; + scaledPosition = prevScaledPosition + + (interpolateFraction * (nextScaledPosition - prevScaledPosition)); } - - long position = Math.round((1.0 / 256) * fx * sizeBytes) + firstFramePosition; - long maximumPosition = inputLength != C.LENGTH_UNSET ? inputLength - 1 - : firstFramePosition - headerSize + sizeBytes - 1; - return Math.min(position, maximumPosition); + long positionOffset = Math.round((scaledPosition / 256) * dataSize); + // Ensure returned positions skip the frame containing the XING header. + positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1); + return new SeekPoints(new SeekPoint(timeUs, dataStartPosition + positionOffset)); } @Override public long getTimeUs(long position) { - if (!isSeekable() || position < firstFramePosition) { + long positionOffset = position - dataStartPosition; + if (!isSeekable() || positionOffset <= xingFrameSize) { return 0L; } - double offsetByte = 256.0 * (position - firstFramePosition) / sizeBytes; - int previousTocPosition = - Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, false) + 1; - long previousTime = getTimeUsForTocPosition(previousTocPosition); - - // Linearly interpolate the time taking into account the next entry. - long previousByte = previousTocPosition == 0 ? 0 : tableOfContents[previousTocPosition - 1]; - long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition]; - long nextTime = getTimeUsForTocPosition(previousTocPosition + 1); - long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime) - * (offsetByte - previousByte) / (nextByte - previousByte)); - return previousTime + timeOffset; + long[] tableOfContents = Assertions.checkNotNull(this.tableOfContents); + double scaledPosition = (positionOffset * 256d) / dataSize; + int prevTableIndex = Util.binarySearchFloor(tableOfContents, (long) scaledPosition, true, true); + long prevTimeUs = getTimeUsForTableIndex(prevTableIndex); + long prevScaledPosition = tableOfContents[prevTableIndex]; + long nextTimeUs = getTimeUsForTableIndex(prevTableIndex + 1); + long nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1]; + // Linearly interpolate between the two table entries. + double interpolateFraction = prevScaledPosition == nextScaledPosition ? 0 + : ((scaledPosition - prevScaledPosition) / (nextScaledPosition - prevScaledPosition)); + return prevTimeUs + Math.round(interpolateFraction * (nextTimeUs - prevTimeUs)); } @Override @@ -157,12 +170,19 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; return durationUs; } + @Override + public long getDataEndPosition() { + return dataEndPosition; + } + /** - * Returns the time in microseconds corresponding to a table of contents position, which is - * interpreted as a percentage of the stream's duration between 0 and 100. + * Returns the time in microseconds for a given table index. + * + * @param tableIndex A table index in the range [0, 100]. + * @return The corresponding time in microseconds. */ - private long getTimeUsForTocPosition(int tocPosition) { - return durationUs * tocPosition / 100; + private long getTimeUsForTableIndex(int tableIndex) { + return (durationUs * tableIndex) / 100; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java index a8941302b18f..56f0eab1cdf5 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -15,13 +15,14 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -/* package*/ abstract class Atom { +@SuppressWarnings("ConstantField") +/* package */ abstract class Atom { /** * Size of an atom header, in bytes. @@ -39,103 +40,347 @@ import java.util.List; public static final int LONG_HEADER_SIZE = 16; /** - * Value for the first 32 bits of atomSize when the atom size is actually a long value. + * Value for the size field in an atom that defines its size in the largesize field. */ - public static final int LONG_SIZE_PREFIX = 1; + public static final int DEFINES_LARGE_SIZE = 1; - public static final int TYPE_ftyp = Util.getIntegerCodeForString("ftyp"); - public static final int TYPE_avc1 = Util.getIntegerCodeForString("avc1"); - public static final int TYPE_avc3 = Util.getIntegerCodeForString("avc3"); - public static final int TYPE_hvc1 = Util.getIntegerCodeForString("hvc1"); - public static final int TYPE_hev1 = Util.getIntegerCodeForString("hev1"); - public static final int TYPE_s263 = Util.getIntegerCodeForString("s263"); - public static final int TYPE_d263 = Util.getIntegerCodeForString("d263"); - public static final int TYPE_mdat = Util.getIntegerCodeForString("mdat"); - public static final int TYPE_mp4a = Util.getIntegerCodeForString("mp4a"); - public static final int TYPE__mp3 = Util.getIntegerCodeForString(".mp3"); - public static final int TYPE_wave = Util.getIntegerCodeForString("wave"); - public static final int TYPE_lpcm = Util.getIntegerCodeForString("lpcm"); - public static final int TYPE_sowt = Util.getIntegerCodeForString("sowt"); - public static final int TYPE_ac_3 = Util.getIntegerCodeForString("ac-3"); - public static final int TYPE_dac3 = Util.getIntegerCodeForString("dac3"); - public static final int TYPE_ec_3 = Util.getIntegerCodeForString("ec-3"); - public static final int TYPE_dec3 = Util.getIntegerCodeForString("dec3"); - public static final int TYPE_dtsc = Util.getIntegerCodeForString("dtsc"); - public static final int TYPE_dtsh = Util.getIntegerCodeForString("dtsh"); - public static final int TYPE_dtsl = Util.getIntegerCodeForString("dtsl"); - public static final int TYPE_dtse = Util.getIntegerCodeForString("dtse"); - public static final int TYPE_ddts = Util.getIntegerCodeForString("ddts"); - public static final int TYPE_tfdt = Util.getIntegerCodeForString("tfdt"); - public static final int TYPE_tfhd = Util.getIntegerCodeForString("tfhd"); - public static final int TYPE_trex = Util.getIntegerCodeForString("trex"); - public static final int TYPE_trun = Util.getIntegerCodeForString("trun"); - public static final int TYPE_sidx = Util.getIntegerCodeForString("sidx"); - public static final int TYPE_moov = Util.getIntegerCodeForString("moov"); - public static final int TYPE_mvhd = Util.getIntegerCodeForString("mvhd"); - public static final int TYPE_trak = Util.getIntegerCodeForString("trak"); - public static final int TYPE_mdia = Util.getIntegerCodeForString("mdia"); - public static final int TYPE_minf = Util.getIntegerCodeForString("minf"); - public static final int TYPE_stbl = Util.getIntegerCodeForString("stbl"); - public static final int TYPE_avcC = Util.getIntegerCodeForString("avcC"); - public static final int TYPE_hvcC = Util.getIntegerCodeForString("hvcC"); - public static final int TYPE_esds = Util.getIntegerCodeForString("esds"); - public static final int TYPE_moof = Util.getIntegerCodeForString("moof"); - public static final int TYPE_traf = Util.getIntegerCodeForString("traf"); - public static final int TYPE_mvex = Util.getIntegerCodeForString("mvex"); - public static final int TYPE_mehd = Util.getIntegerCodeForString("mehd"); - public static final int TYPE_tkhd = Util.getIntegerCodeForString("tkhd"); - public static final int TYPE_edts = Util.getIntegerCodeForString("edts"); - public static final int TYPE_elst = Util.getIntegerCodeForString("elst"); - public static final int TYPE_mdhd = Util.getIntegerCodeForString("mdhd"); - public static final int TYPE_hdlr = Util.getIntegerCodeForString("hdlr"); - public static final int TYPE_stsd = Util.getIntegerCodeForString("stsd"); - public static final int TYPE_pssh = Util.getIntegerCodeForString("pssh"); - public static final int TYPE_sinf = Util.getIntegerCodeForString("sinf"); - public static final int TYPE_schm = Util.getIntegerCodeForString("schm"); - public static final int TYPE_schi = Util.getIntegerCodeForString("schi"); - public static final int TYPE_tenc = Util.getIntegerCodeForString("tenc"); - public static final int TYPE_encv = Util.getIntegerCodeForString("encv"); - public static final int TYPE_enca = Util.getIntegerCodeForString("enca"); - public static final int TYPE_frma = Util.getIntegerCodeForString("frma"); - public static final int TYPE_saiz = Util.getIntegerCodeForString("saiz"); - public static final int TYPE_saio = Util.getIntegerCodeForString("saio"); - public static final int TYPE_sbgp = Util.getIntegerCodeForString("sbgp"); - public static final int TYPE_sgpd = Util.getIntegerCodeForString("sgpd"); - public static final int TYPE_uuid = Util.getIntegerCodeForString("uuid"); - public static final int TYPE_senc = Util.getIntegerCodeForString("senc"); - public static final int TYPE_pasp = Util.getIntegerCodeForString("pasp"); - public static final int TYPE_TTML = Util.getIntegerCodeForString("TTML"); - public static final int TYPE_vmhd = Util.getIntegerCodeForString("vmhd"); - public static final int TYPE_mp4v = Util.getIntegerCodeForString("mp4v"); - public static final int TYPE_stts = Util.getIntegerCodeForString("stts"); - public static final int TYPE_stss = Util.getIntegerCodeForString("stss"); - public static final int TYPE_ctts = Util.getIntegerCodeForString("ctts"); - public static final int TYPE_stsc = Util.getIntegerCodeForString("stsc"); - public static final int TYPE_stsz = Util.getIntegerCodeForString("stsz"); - public static final int TYPE_stz2 = Util.getIntegerCodeForString("stz2"); - public static final int TYPE_stco = Util.getIntegerCodeForString("stco"); - public static final int TYPE_co64 = Util.getIntegerCodeForString("co64"); - public static final int TYPE_tx3g = Util.getIntegerCodeForString("tx3g"); - public static final int TYPE_wvtt = Util.getIntegerCodeForString("wvtt"); - public static final int TYPE_stpp = Util.getIntegerCodeForString("stpp"); - public static final int TYPE_c608 = Util.getIntegerCodeForString("c608"); - public static final int TYPE_samr = Util.getIntegerCodeForString("samr"); - public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb"); - public static final int TYPE_udta = Util.getIntegerCodeForString("udta"); - public static final int TYPE_meta = Util.getIntegerCodeForString("meta"); - public static final int TYPE_ilst = Util.getIntegerCodeForString("ilst"); - public static final int TYPE_mean = Util.getIntegerCodeForString("mean"); - public static final int TYPE_name = Util.getIntegerCodeForString("name"); - public static final int TYPE_data = Util.getIntegerCodeForString("data"); - public static final int TYPE_emsg = Util.getIntegerCodeForString("emsg"); - public static final int TYPE_st3d = Util.getIntegerCodeForString("st3d"); - public static final int TYPE_sv3d = Util.getIntegerCodeForString("sv3d"); - public static final int TYPE_proj = Util.getIntegerCodeForString("proj"); - public static final int TYPE_vp08 = Util.getIntegerCodeForString("vp08"); - public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09"); - public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC"); - public static final int TYPE_camm = Util.getIntegerCodeForString("camm"); - public static final int TYPE_alac = Util.getIntegerCodeForString("alac"); + /** + * Value for the size field in an atom that extends to the end of the file. + */ + public static final int EXTENDS_TO_END_SIZE = 0; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ftyp = 0x66747970; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_avc1 = 0x61766331; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_avc3 = 0x61766333; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_avcC = 0x61766343; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_hvc1 = 0x68766331; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_hev1 = 0x68657631; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_hvcC = 0x68766343; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_vp08 = 0x76703038; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_vp09 = 0x76703039; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_vpcC = 0x76706343; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_av01 = 0x61763031; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_av1C = 0x61763143; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dvav = 0x64766176; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dva1 = 0x64766131; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dvhe = 0x64766865; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dvh1 = 0x64766831; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dvcC = 0x64766343; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dvvC = 0x64767643; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_s263 = 0x73323633; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_d263 = 0x64323633; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mdat = 0x6d646174; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mp4a = 0x6d703461; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE__mp3 = 0x2e6d7033; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_wave = 0x77617665; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_lpcm = 0x6c70636d; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sowt = 0x736f7774; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ac_3 = 0x61632d33; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dac3 = 0x64616333; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ec_3 = 0x65632d33; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dec3 = 0x64656333; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ac_4 = 0x61632d34; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dac4 = 0x64616334; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dtsc = 0x64747363; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dtsh = 0x64747368; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dtsl = 0x6474736c; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dtse = 0x64747365; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ddts = 0x64647473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_tfdt = 0x74666474; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_tfhd = 0x74666864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_trex = 0x74726578; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_trun = 0x7472756e; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sidx = 0x73696478; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_moov = 0x6d6f6f76; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mvhd = 0x6d766864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_trak = 0x7472616b; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mdia = 0x6d646961; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_minf = 0x6d696e66; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stbl = 0x7374626c; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_esds = 0x65736473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_moof = 0x6d6f6f66; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_traf = 0x74726166; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mvex = 0x6d766578; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mehd = 0x6d656864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_tkhd = 0x746b6864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_edts = 0x65647473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_elst = 0x656c7374; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mdhd = 0x6d646864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_hdlr = 0x68646c72; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stsd = 0x73747364; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_pssh = 0x70737368; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sinf = 0x73696e66; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_schm = 0x7363686d; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_schi = 0x73636869; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_tenc = 0x74656e63; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_encv = 0x656e6376; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_enca = 0x656e6361; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_frma = 0x66726d61; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_saiz = 0x7361697a; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_saio = 0x7361696f; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sbgp = 0x73626770; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sgpd = 0x73677064; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_uuid = 0x75756964; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_senc = 0x73656e63; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_pasp = 0x70617370; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_TTML = 0x54544d4c; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_vmhd = 0x766d6864; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mp4v = 0x6d703476; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stts = 0x73747473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stss = 0x73747373; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ctts = 0x63747473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stsc = 0x73747363; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stsz = 0x7374737a; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stz2 = 0x73747a32; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stco = 0x7374636f; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_co64 = 0x636f3634; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_tx3g = 0x74783367; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_wvtt = 0x77767474; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_stpp = 0x73747070; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_c608 = 0x63363038; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_samr = 0x73616d72; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sawb = 0x73617762; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_udta = 0x75647461; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_meta = 0x6d657461; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_keys = 0x6b657973; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ilst = 0x696c7374; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mean = 0x6d65616e; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_name = 0x6e616d65; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_data = 0x64617461; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_emsg = 0x656d7367; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_st3d = 0x73743364; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_sv3d = 0x73763364; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_proj = 0x70726f6a; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_camm = 0x63616d6d; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_alac = 0x616c6163; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_alaw = 0x616c6177; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_ulaw = 0x756c6177; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_Opus = 0x4f707573; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dOps = 0x644f7073; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_fLaC = 0x664c6143; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dfLa = 0x64664c61; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_twos = 0x74776f73; public final int type; @@ -209,13 +454,14 @@ import java.util.List; /** * Returns the child leaf of the given type. - *

- * If no child exists with the given type then null is returned. If multiple children exist with - * the given type then the first one to have been added is returned. + * + *

If no child exists with the given type then null is returned. If multiple children exist + * with the given type then the first one to have been added is returned. * * @param type The leaf type. * @return The child leaf of the given type, or null if no such child exists. */ + @Nullable public LeafAtom getLeafAtomOfType(int type) { int childrenSize = leafChildren.size(); for (int i = 0; i < childrenSize; i++) { @@ -229,13 +475,14 @@ import java.util.List; /** * Returns the child container of the given type. - *

- * If no child exists with the given type then null is returned. If multiple children exist with - * the given type then the first one to have been added is returned. + * + *

If no child exists with the given type then null is returned. If multiple children exist + * with the given type then the first one to have been added is returned. * * @param type The container type. * @return The child container of the given type, or null if no such child exists. */ + @Nullable public ContainerAtom getContainerAtomOfType(int type) { int childrenSize = containerChildren.size(); for (int i = 0; i < childrenSize; i++) { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 693870d78589..93ee2d68101f 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -15,42 +15,70 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; -import android.util.Log; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes.getMimeTypeFromMp4ObjectType; + import android.util.Pair; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.GaplessInfoHolder; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import org.mozilla.thirdparty.com.google.android.exoplayer2.video.AvcConfig; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.DolbyVisionConfig; import org.mozilla.thirdparty.com.google.android.exoplayer2.video.HevcConfig; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -/** - * Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. - */ +/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */ +@SuppressWarnings({"ConstantField"}) /* package */ final class AtomParsers { private static final String TAG = "AtomParsers"; - private static final int TYPE_vide = Util.getIntegerCodeForString("vide"); - private static final int TYPE_soun = Util.getIntegerCodeForString("soun"); - private static final int TYPE_text = Util.getIntegerCodeForString("text"); - private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl"); - private static final int TYPE_subt = Util.getIntegerCodeForString("subt"); - private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp"); - private static final int TYPE_cenc = Util.getIntegerCodeForString("cenc"); - private static final int TYPE_meta = Util.getIntegerCodeForString("meta"); + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_vide = 0x76696465; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_soun = 0x736f756e; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_text = 0x74657874; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_sbtl = 0x7362746c; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_subt = 0x73756274; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_clcp = 0x636c6370; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_meta = 0x6d657461; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_mdta = 0x6d647461; + + /** + * The threshold number of samples to trim from the start/end of an audio track when applying an + * edit below which gapless info can be used (rather than removing samples from the sample table). + */ + private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 4; + + /** The magic signature for an Opus Identification header, as defined in RFC-7845. */ + private static final byte[] opusMagic = Util.getUtf8Bytes("OpusHead"); /** * Parses a trak atom (defined in 14496-12). @@ -60,13 +88,15 @@ import java.util.List; * @param duration The duration in units of the timescale declared in the mvhd atom, or * {@link C#TIME_UNSET} if the duration should be parsed from the tkhd atom. * @param drmInitData {@link DrmInitData} to be included in the format. + * @param ignoreEditLists Whether to ignore any edit lists in the trak box. * @param isQuickTime True for QuickTime media. False otherwise. * @return A {@link Track} instance, or {@code null} if the track's type isn't supported. */ public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long duration, - DrmInitData drmInitData, boolean isQuickTime) throws ParserException { + DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime) + throws ParserException { Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia); - int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data); + int trackType = getTrackTypeForHdlr(parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data)); if (trackType == C.TRACK_TYPE_UNKNOWN) { return null; } @@ -88,11 +118,17 @@ import java.util.List; Pair mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data); StsdData stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, tkhdData.id, tkhdData.rotationDegrees, mdhdData.second, drmInitData, isQuickTime); - Pair edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); + long[] editListDurations = null; + long[] editListMediaTimes = null; + if (!ignoreEditLists) { + Pair edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); + editListDurations = edtsData.first; + editListMediaTimes = edtsData.second; + } return stsdData.format == null ? null : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs, stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes, - stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second); + stsdData.nalUnitLengthFieldLength, editListDurations, editListMediaTimes); } /** @@ -102,10 +138,11 @@ import java.util.List; * @param stblAtom stbl (sample table) atom to decode. * @param gaplessInfoHolder Holder to populate with gapless playback information. * @return Sample table described by the stbl atom. - * @throws ParserException If the resulting sample sequence does not contain a sync sample. + * @throws ParserException Thrown if the stbl atom can't be parsed. */ - public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom, - GaplessInfoHolder gaplessInfoHolder) throws ParserException { + public static TrackSampleTable parseStbl( + Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder) + throws ParserException { SampleSizeBox sampleSizeBox; Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz); if (stszAtom != null) { @@ -120,7 +157,14 @@ import java.util.List; int sampleCount = sampleSizeBox.getSampleCount(); if (sampleCount == 0) { - return new TrackSampleTable(new long[0], new int[0], 0, new long[0], new int[0]); + return new TrackSampleTable( + track, + /* offsets= */ new long[0], + /* sizes= */ new int[0], + /* maximumSize= */ 0, + /* timestampsUs= */ new long[0], + /* flags= */ new int[0], + /* durationUs= */ C.TIME_UNSET); } // Entries are byte offsets of chunks. @@ -173,11 +217,13 @@ import java.util.List; } } - // True if we can rechunk fixed-sample-size data. Note that we only rechunk raw audio. - boolean isRechunkable = sampleSizeBox.isFixedSampleSize() - && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType) - && remainingTimestampDeltaChanges == 0 && remainingTimestampOffsetChanges == 0 - && remainingSynchronizationSamples == 0; + // Fixed sample size raw audio may need to be rechunked. + boolean isFixedSampleSizeRawAudio = + sampleSizeBox.isFixedSampleSize() + && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType) + && remainingTimestampDeltaChanges == 0 + && remainingTimestampOffsetChanges == 0 + && remainingSynchronizationSamples == 0; long[] offsets; int[] sizes; @@ -185,8 +231,9 @@ import java.util.List; long[] timestamps; int[] flags; long timestampTimeUnits = 0; + long duration; - if (!isRechunkable) { + if (!isFixedSampleSizeRawAudio) { offsets = new long[sampleCount]; sizes = new int[sampleCount]; timestamps = new long[sampleCount]; @@ -196,11 +243,20 @@ import java.util.List; for (int i = 0; i < sampleCount; i++) { // Advance to the next chunk if necessary. - while (remainingSamplesInChunk == 0) { - Assertions.checkState(chunkIterator.moveNext()); + boolean chunkDataComplete = true; + while (remainingSamplesInChunk == 0 && (chunkDataComplete = chunkIterator.moveNext())) { offset = chunkIterator.offset; remainingSamplesInChunk = chunkIterator.numSamples; } + if (!chunkDataComplete) { + Log.w(TAG, "Unexpected end of chunk data"); + sampleCount = i; + offsets = Arrays.copyOf(offsets, sampleCount); + sizes = Arrays.copyOf(sizes, sampleCount); + timestamps = Arrays.copyOf(timestamps, sampleCount); + flags = Arrays.copyOf(flags, sampleCount); + break; + } // Add on the timestamp offset if ctts is present. if (ctts != null) { @@ -239,31 +295,53 @@ import java.util.List; remainingSamplesAtTimestampDelta--; if (remainingSamplesAtTimestampDelta == 0 && remainingTimestampDeltaChanges > 0) { remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt(); - timestampDeltaInTimeUnits = stts.readUnsignedIntToInt(); + // The BMFF spec (ISO 14496-12) states that sample deltas should be unsigned integers + // in stts boxes, however some streams violate the spec and use signed integers instead. + // See https://github.com/google/ExoPlayer/issues/3384. It's safe to always decode sample + // deltas as signed integers here, because unsigned integers will still be parsed + // correctly (unless their top bit is set, which is never true in practice because sample + // deltas are always small). + timestampDeltaInTimeUnits = stts.readInt(); remainingTimestampDeltaChanges--; } offset += sizes[i]; remainingSamplesInChunk--; } - - Assertions.checkArgument(remainingSamplesAtTimestampOffset == 0); - // Remove trailing ctts entries with 0-valued sample counts. - while (remainingTimestampOffsetChanges > 0) { - Assertions.checkArgument(ctts.readUnsignedIntToInt() == 0); - ctts.readInt(); // Ignore offset. - remainingTimestampOffsetChanges--; - } + duration = timestampTimeUnits + timestampOffset; // If the stbl's child boxes are not consistent the container is malformed, but the stream may // still be playable. - if (remainingSynchronizationSamples != 0 || remainingSamplesAtTimestampDelta != 0 - || remainingSamplesInChunk != 0 || remainingTimestampDeltaChanges != 0) { - Log.w(TAG, "Inconsistent stbl box for track " + track.id - + ": remainingSynchronizationSamples " + remainingSynchronizationSamples - + ", remainingSamplesAtTimestampDelta " + remainingSamplesAtTimestampDelta - + ", remainingSamplesInChunk " + remainingSamplesInChunk - + ", remainingTimestampDeltaChanges " + remainingTimestampDeltaChanges); + boolean isCttsValid = true; + while (remainingTimestampOffsetChanges > 0) { + if (ctts.readUnsignedIntToInt() != 0) { + isCttsValid = false; + break; + } + ctts.readInt(); // Ignore offset. + remainingTimestampOffsetChanges--; + } + if (remainingSynchronizationSamples != 0 + || remainingSamplesAtTimestampDelta != 0 + || remainingSamplesInChunk != 0 + || remainingTimestampDeltaChanges != 0 + || remainingSamplesAtTimestampOffset != 0 + || !isCttsValid) { + Log.w( + TAG, + "Inconsistent stbl box for track " + + track.id + + ": remainingSynchronizationSamples " + + remainingSynchronizationSamples + + ", remainingSamplesAtTimestampDelta " + + remainingSamplesAtTimestampDelta + + ", remainingSamplesInChunk " + + remainingSamplesInChunk + + ", remainingTimestampDeltaChanges " + + remainingTimestampDeltaChanges + + ", remainingSamplesAtTimestampOffset " + + remainingSamplesAtTimestampOffset + + (!isCttsValid ? ", ctts invalid" : "")); } } else { long[] chunkOffsetsBytes = new long[chunkIterator.length]; @@ -272,7 +350,8 @@ import java.util.List; chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset; chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples; } - int fixedSampleSize = sampleSizeBox.readNextSampleSize(); + int fixedSampleSize = + Util.getPcmFrameSize(track.format.pcmEncoding, track.format.channelCount); FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk( fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits); offsets = rechunkedResults.offsets; @@ -280,33 +359,31 @@ import java.util.List; maximumSize = rechunkedResults.maximumSize; timestamps = rechunkedResults.timestamps; flags = rechunkedResults.flags; + duration = rechunkedResults.duration; } + long durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, track.timescale); - if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) { - // There is no edit list, or we are ignoring it as we already have gapless metadata to apply. - // This implementation does not support applying both gapless metadata and an edit list. + if (track.editListDurations == null) { Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); - return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); + return new TrackSampleTable( + track, offsets, sizes, maximumSize, timestamps, flags, durationUs); } // See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a // sync sample after reordering are not supported. Partial audio sample truncation is only - // supported in edit lists with one edit that removes less than one sample from the start/end of - // the track, for gapless audio playback. This implementation handles simple discarding/delaying - // of samples. The extractor may place further restrictions on what edited streams are playable. + // supported in edit lists with one edit that removes less than MAX_GAPLESS_TRIM_SIZE_SAMPLES + // samples from the start/end of the track. This implementation handles simple + // discarding/delaying of samples. The extractor may place further restrictions on what edited + // streams are playable. - if (track.editListDurations.length == 1 && track.type == C.TRACK_TYPE_AUDIO + if (track.editListDurations.length == 1 + && track.type == C.TRACK_TYPE_AUDIO && timestamps.length >= 2) { - // Handle the edit by setting gapless playback metadata, if possible. This implementation - // assumes that only one "roll" sample is needed, which is the case for AAC, so the start/end - // points of the edit must lie within the first/last samples respectively. long editStartTime = track.editListMediaTimes[0]; long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0], track.timescale, track.movieTimescale); - long lastSampleEndTime = timestampTimeUnits; - if (timestamps[0] <= editStartTime && editStartTime < timestamps[1] - && timestamps[timestamps.length - 1] < editEndTime && editEndTime <= lastSampleEndTime) { - long paddingTimeUnits = lastSampleEndTime - editEndTime; + if (canApplyEditWithGaplessInfo(timestamps, duration, editStartTime, editEndTime)) { + long paddingTimeUnits = duration - editEndTime; long encoderDelay = Util.scaleLargeTimestamp(editStartTime - timestamps[0], track.format.sampleRate, track.timescale); long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits, @@ -316,7 +393,11 @@ import java.util.List; gaplessInfoHolder.encoderDelay = (int) encoderDelay; gaplessInfoHolder.encoderPadding = (int) encoderPadding; Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); - return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); + long editedDurationUs = + Util.scaleLargeTimestamp( + track.editListDurations[0], C.MICROS_PER_SECOND, track.movieTimescale); + return new TrackSampleTable( + track, offsets, sizes, maximumSize, timestamps, flags, editedDurationUs); } } } @@ -325,11 +406,16 @@ import java.util.List; // The current version of the spec leaves handling of an edit with zero segment_duration in // unfragmented files open to interpretation. We handle this as a special case and include all // samples in the edit. + long editStartTime = track.editListMediaTimes[0]; for (int i = 0; i < timestamps.length; i++) { - timestamps[i] = Util.scaleLargeTimestamp(timestamps[i] - track.editListMediaTimes[0], - C.MICROS_PER_SECOND, track.timescale); + timestamps[i] = + Util.scaleLargeTimestamp( + timestamps[i] - editStartTime, C.MICROS_PER_SECOND, track.timescale); } - return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); + durationUs = + Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale); + return new TrackSampleTable( + track, offsets, sizes, maximumSize, timestamps, flags, durationUs); } // Omit any sample at the end point of an edit for audio tracks. @@ -339,17 +425,34 @@ import java.util.List; int editedSampleCount = 0; int nextSampleIndex = 0; boolean copyMetadata = false; + int[] startIndices = new int[track.editListDurations.length]; + int[] endIndices = new int[track.editListDurations.length]; for (int i = 0; i < track.editListDurations.length; i++) { - long mediaTime = track.editListMediaTimes[i]; - if (mediaTime != -1) { - long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale, - track.movieTimescale); - int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); - int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, omitClippedSample, - false); - editedSampleCount += endIndex - startIndex; - copyMetadata |= nextSampleIndex != startIndex; - nextSampleIndex = endIndex; + long editMediaTime = track.editListMediaTimes[i]; + if (editMediaTime != -1) { + long editDuration = + Util.scaleLargeTimestamp( + track.editListDurations[i], track.timescale, track.movieTimescale); + startIndices[i] = + Util.binarySearchFloor( + timestamps, editMediaTime, /* inclusive= */ true, /* stayInBounds= */ true); + endIndices[i] = + Util.binarySearchCeil( + timestamps, + editMediaTime + editDuration, + /* inclusive= */ omitClippedSample, + /* stayInBounds= */ false); + while (startIndices[i] < endIndices[i] + && (flags[startIndices[i]] & C.BUFFER_FLAG_KEY_FRAME) == 0) { + // Applying the edit correctly would require prerolling from the previous sync sample. In + // the current implementation we advance to the next sync sample instead. Only other + // tracks (i.e. audio) will be rendered until the time of the first sync sample. + // See https://github.com/google/ExoPlayer/issues/1659. + startIndices[i]++; + } + editedSampleCount += endIndices[i] - startIndices[i]; + copyMetadata |= nextSampleIndex != startIndices[i]; + nextSampleIndex = endIndices[i]; } } copyMetadata |= editedSampleCount != sampleCount; @@ -363,43 +466,38 @@ import java.util.List; long pts = 0; int sampleIndex = 0; for (int i = 0; i < track.editListDurations.length; i++) { - long mediaTime = track.editListMediaTimes[i]; - long duration = track.editListDurations[i]; - if (mediaTime != -1) { - long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale, - track.movieTimescale); - int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); - int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, omitClippedSample, false); - if (copyMetadata) { - int count = endIndex - startIndex; - System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count); - System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count); - System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count); - } - for (int j = startIndex; j < endIndex; j++) { - long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale); - long timeInSegmentUs = Util.scaleLargeTimestamp(timestamps[j] - mediaTime, - C.MICROS_PER_SECOND, track.timescale); - editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs; - if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) { - editedMaximumSize = sizes[j]; - } - sampleIndex++; - } + long editMediaTime = track.editListMediaTimes[i]; + int startIndex = startIndices[i]; + int endIndex = endIndices[i]; + if (copyMetadata) { + int count = endIndex - startIndex; + System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count); + System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count); + System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count); } - pts += duration; + for (int j = startIndex; j < endIndex; j++) { + long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale); + long timeInSegmentUs = + Util.scaleLargeTimestamp( + Math.max(0, timestamps[j] - editMediaTime), C.MICROS_PER_SECOND, track.timescale); + editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs; + if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) { + editedMaximumSize = sizes[j]; + } + sampleIndex++; + } + pts += track.editListDurations[i]; } - - boolean hasSyncSample = false; - for (int i = 0; i < editedFlags.length && !hasSyncSample; i++) { - hasSyncSample |= (editedFlags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0; - } - if (!hasSyncSample) { - throw new ParserException("The edited sample sequence does not contain a sync sample."); - } - - return new TrackSampleTable(editedOffsets, editedSizes, editedMaximumSize, editedTimestamps, - editedFlags); + long editedDurationUs = + Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale); + return new TrackSampleTable( + track, + editedOffsets, + editedSizes, + editedMaximumSize, + editedTimestamps, + editedFlags, + editedDurationUs); } /** @@ -409,6 +507,7 @@ import java.util.List; * @param isQuickTime True for QuickTime media. False otherwise. * @return Parsed metadata, or null. */ + @Nullable public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { if (isQuickTime) { // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and @@ -423,14 +522,69 @@ import java.util.List; int atomType = udtaData.readInt(); if (atomType == Atom.TYPE_meta) { udtaData.setPosition(atomPosition); - return parseMetaAtom(udtaData, atomPosition + atomSize); + return parseUdtaMeta(udtaData, atomPosition + atomSize); } - udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); + udtaData.setPosition(atomPosition + atomSize); } return null; } - private static Metadata parseMetaAtom(ParsableByteArray meta, int limit) { + /** + * Parses a metadata meta atom if it contains metadata with handler 'mdta'. + * + * @param meta The metadata atom to decode. + * @return Parsed metadata, or null. + */ + @Nullable + public static Metadata parseMdtaFromMeta(Atom.ContainerAtom meta) { + Atom.LeafAtom hdlrAtom = meta.getLeafAtomOfType(Atom.TYPE_hdlr); + Atom.LeafAtom keysAtom = meta.getLeafAtomOfType(Atom.TYPE_keys); + Atom.LeafAtom ilstAtom = meta.getLeafAtomOfType(Atom.TYPE_ilst); + if (hdlrAtom == null + || keysAtom == null + || ilstAtom == null + || AtomParsers.parseHdlr(hdlrAtom.data) != TYPE_mdta) { + // There isn't enough information to parse the metadata, or the handler type is unexpected. + return null; + } + + // Parse metadata keys. + ParsableByteArray keys = keysAtom.data; + keys.setPosition(Atom.FULL_HEADER_SIZE); + int entryCount = keys.readInt(); + String[] keyNames = new String[entryCount]; + for (int i = 0; i < entryCount; i++) { + int entrySize = keys.readInt(); + keys.skipBytes(4); // keyNamespace + int keySize = entrySize - 8; + keyNames[i] = keys.readString(keySize); + } + + // Parse metadata items. + ParsableByteArray ilst = ilstAtom.data; + ilst.setPosition(Atom.HEADER_SIZE); + ArrayList entries = new ArrayList<>(); + while (ilst.bytesLeft() > Atom.HEADER_SIZE) { + int atomPosition = ilst.getPosition(); + int atomSize = ilst.readInt(); + int keyIndex = ilst.readInt() - 1; + if (keyIndex >= 0 && keyIndex < keyNames.length) { + String key = keyNames[keyIndex]; + Metadata.Entry entry = + MetadataUtil.parseMdtaMetadataEntryFromIlst(ilst, atomPosition + atomSize, key); + if (entry != null) { + entries.add(entry); + } + } else { + Log.w(TAG, "Skipped metadata with unknown key index: " + keyIndex); + } + ilst.setPosition(atomPosition + atomSize); + } + return entries.isEmpty() ? null : new Metadata(entries); + } + + @Nullable + private static Metadata parseUdtaMeta(ParsableByteArray meta, int limit) { meta.skipBytes(Atom.FULL_HEADER_SIZE); while (meta.getPosition() < limit) { int atomPosition = meta.getPosition(); @@ -440,11 +594,12 @@ import java.util.List; meta.setPosition(atomPosition); return parseIlst(meta, atomPosition + atomSize); } - meta.skipBytes(atomSize - Atom.HEADER_SIZE); + meta.setPosition(atomPosition + atomSize); } return null; } + @Nullable private static Metadata parseIlst(ParsableByteArray ilst, int limit) { ilst.skipBytes(Atom.HEADER_SIZE); ArrayList entries = new ArrayList<>(); @@ -534,19 +689,22 @@ import java.util.List; * Parses an hdlr atom. * * @param hdlr The hdlr atom to decode. - * @return The track type. + * @return The handler value. */ private static int parseHdlr(ParsableByteArray hdlr) { hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4); - int trackType = hdlr.readInt(); - if (trackType == TYPE_soun) { + return hdlr.readInt(); + } + + /** Returns the track type for a given handler value. */ + private static int getTrackTypeForHdlr(int hdlr) { + if (hdlr == TYPE_soun) { return C.TRACK_TYPE_AUDIO; - } else if (trackType == TYPE_vide) { + } else if (hdlr == TYPE_vide) { return C.TRACK_TYPE_VIDEO; - } else if (trackType == TYPE_text || trackType == TYPE_sbtl || trackType == TYPE_subt - || trackType == TYPE_clcp) { + } else if (hdlr == TYPE_text || hdlr == TYPE_sbtl || hdlr == TYPE_subt || hdlr == TYPE_clcp) { return C.TRACK_TYPE_TEXT; - } else if (trackType == TYPE_meta) { + } else if (hdlr == TYPE_meta) { return C.TRACK_TYPE_METADATA; } else { return C.TRACK_TYPE_UNKNOWN; @@ -568,9 +726,11 @@ import java.util.List; long timescale = mdhd.readUnsignedInt(); mdhd.skipBytes(version == 0 ? 4 : 8); int languageCode = mdhd.readUnsignedShort(); - String language = "" + (char) (((languageCode >> 10) & 0x1F) + 0x60) - + (char) (((languageCode >> 5) & 0x1F) + 0x60) - + (char) (((languageCode) & 0x1F) + 0x60); + String language = + "" + + (char) (((languageCode >> 10) & 0x1F) + 0x60) + + (char) (((languageCode >> 5) & 0x1F) + 0x60) + + (char) ((languageCode & 0x1F) + 0x60); return Pair.create(timescale, language); } @@ -595,30 +755,52 @@ import java.util.List; int childAtomSize = stsd.readInt(); Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = stsd.readInt(); - if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3 - || childAtomType == Atom.TYPE_encv || childAtomType == Atom.TYPE_mp4v - || childAtomType == Atom.TYPE_hvc1 || childAtomType == Atom.TYPE_hev1 - || childAtomType == Atom.TYPE_s263 || childAtomType == Atom.TYPE_vp08 - || childAtomType == Atom.TYPE_vp09) { + if (childAtomType == Atom.TYPE_avc1 + || childAtomType == Atom.TYPE_avc3 + || childAtomType == Atom.TYPE_encv + || childAtomType == Atom.TYPE_mp4v + || childAtomType == Atom.TYPE_hvc1 + || childAtomType == Atom.TYPE_hev1 + || childAtomType == Atom.TYPE_s263 + || childAtomType == Atom.TYPE_vp08 + || childAtomType == Atom.TYPE_vp09 + || childAtomType == Atom.TYPE_av01 + || childAtomType == Atom.TYPE_dvav + || childAtomType == Atom.TYPE_dva1 + || childAtomType == Atom.TYPE_dvhe + || childAtomType == Atom.TYPE_dvh1) { parseVideoSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, rotationDegrees, drmInitData, out, i); - } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca - || childAtomType == Atom.TYPE_ac_3 || childAtomType == Atom.TYPE_ec_3 - || childAtomType == Atom.TYPE_dtsc || childAtomType == Atom.TYPE_dtse - || childAtomType == Atom.TYPE_dtsh || childAtomType == Atom.TYPE_dtsl - || childAtomType == Atom.TYPE_samr || childAtomType == Atom.TYPE_sawb - || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt - || childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac) { + } else if (childAtomType == Atom.TYPE_mp4a + || childAtomType == Atom.TYPE_enca + || childAtomType == Atom.TYPE_ac_3 + || childAtomType == Atom.TYPE_ec_3 + || childAtomType == Atom.TYPE_ac_4 + || childAtomType == Atom.TYPE_dtsc + || childAtomType == Atom.TYPE_dtse + || childAtomType == Atom.TYPE_dtsh + || childAtomType == Atom.TYPE_dtsl + || childAtomType == Atom.TYPE_samr + || childAtomType == Atom.TYPE_sawb + || childAtomType == Atom.TYPE_lpcm + || childAtomType == Atom.TYPE_sowt + || childAtomType == Atom.TYPE_twos + || childAtomType == Atom.TYPE__mp3 + || childAtomType == Atom.TYPE_alac + || childAtomType == Atom.TYPE_alaw + || childAtomType == Atom.TYPE_ulaw + || childAtomType == Atom.TYPE_Opus + || childAtomType == Atom.TYPE_fLaC) { parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, language, isQuickTime, drmInitData, out, i); } else if (childAtomType == Atom.TYPE_TTML || childAtomType == Atom.TYPE_tx3g || childAtomType == Atom.TYPE_wvtt || childAtomType == Atom.TYPE_stpp || childAtomType == Atom.TYPE_c608) { parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, - language, drmInitData, out); + language, out); } else if (childAtomType == Atom.TYPE_camm) { out.format = Format.createSampleFormat(Integer.toString(trackId), - MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, drmInitData); + MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, null); } stsd.setPosition(childStartPosition + childAtomSize); } @@ -626,8 +808,7 @@ import java.util.List; } private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position, - int atomSize, int trackId, String language, DrmInitData drmInitData, StsdData out) - throws ParserException { + int atomSize, int trackId, String language, StsdData out) throws ParserException { parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); // Default values. @@ -657,9 +838,18 @@ import java.util.List; throw new IllegalStateException(); } - out.format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, - Format.NO_VALUE, 0, language, Format.NO_VALUE, drmInitData, subsampleOffsetUs, - initializationData); + out.format = + Format.createTextSampleFormat( + Integer.toString(trackId), + mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* selectionFlags= */ 0, + language, + /* accessibilityChannel= */ Format.NO_VALUE, + /* drmInitData= */ null, + subsampleOffsetUs, + initializationData); } private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position, @@ -676,12 +866,24 @@ import java.util.List; int childPosition = parent.getPosition(); if (atomType == Atom.TYPE_encv) { - atomType = parseSampleEntryEncryptionData(parent, position, size, out, entryIndex); + Pair sampleEntryEncryptionData = parseSampleEntryEncryptionData( + parent, position, size); + if (sampleEntryEncryptionData != null) { + atomType = sampleEntryEncryptionData.first; + drmInitData = drmInitData == null ? null + : drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType); + out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second; + } parent.setPosition(childPosition); } + // TODO: Uncomment when [Internal: b/63092960] is fixed. + // else { + // drmInitData = null; + // } List initializationData = null; String mimeType = null; + String codecs = null; byte[] projectionData = null; @C.StereoMode int stereoMode = Format.NO_VALUE; @@ -712,9 +914,18 @@ import java.util.List; HevcConfig hevcConfig = HevcConfig.parse(parent); initializationData = hevcConfig.initializationData; out.nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; + } else if (childAtomType == Atom.TYPE_dvcC || childAtomType == Atom.TYPE_dvvC) { + DolbyVisionConfig dolbyVisionConfig = DolbyVisionConfig.parse(parent); + if (dolbyVisionConfig != null) { + codecs = dolbyVisionConfig.codecs; + mimeType = MimeTypes.VIDEO_DOLBY_VISION; + } } else if (childAtomType == Atom.TYPE_vpcC) { Assertions.checkState(mimeType == null); mimeType = (atomType == Atom.TYPE_vp08) ? MimeTypes.VIDEO_VP8 : MimeTypes.VIDEO_VP9; + } else if (childAtomType == Atom.TYPE_av1C) { + Assertions.checkState(mimeType == null); + mimeType = MimeTypes.VIDEO_AV1; } else if (childAtomType == Atom.TYPE_d263) { Assertions.checkState(mimeType == null); mimeType = MimeTypes.VIDEO_H263; @@ -760,9 +971,23 @@ import java.util.List; return; } - out.format = Format.createVideoSampleFormat(Integer.toString(trackId), mimeType, null, - Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE, initializationData, - rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, null, drmInitData); + out.format = + Format.createVideoSampleFormat( + Integer.toString(trackId), + mimeType, + codecs, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + width, + height, + /* frameRate= */ Format.NO_VALUE, + initializationData, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + /* colorInfo= */ null, + drmInitData); } /** @@ -770,7 +995,7 @@ import java.util.List; * * @param edtsAtom edts (edit box) atom to decode. * @return Pair of edit list durations and edit list media times, or a pair of nulls if they are - * not present. + * not present. */ private static Pair parseEdts(Atom.ContainerAtom edtsAtom) { Atom.LeafAtom elst; @@ -807,7 +1032,7 @@ import java.util.List; private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position, int size, int trackId, String language, boolean isQuickTime, DrmInitData drmInitData, - StsdData out, int entryIndex) { + StsdData out, int entryIndex) throws ParserException { parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); int quickTimeSoundDescriptionVersion = 0; @@ -820,6 +1045,7 @@ import java.util.List; int channelCount; int sampleRate; + @C.PcmEncoding int pcmEncoding = Format.NO_VALUE; if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) { channelCount = parent.readUnsignedShort(); @@ -845,9 +1071,20 @@ import java.util.List; int childPosition = parent.getPosition(); if (atomType == Atom.TYPE_enca) { - atomType = parseSampleEntryEncryptionData(parent, position, size, out, entryIndex); + Pair sampleEntryEncryptionData = parseSampleEntryEncryptionData( + parent, position, size); + if (sampleEntryEncryptionData != null) { + atomType = sampleEntryEncryptionData.first; + drmInitData = drmInitData == null ? null + : drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType); + out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second; + } parent.setPosition(childPosition); } + // TODO: Uncomment when [Internal: b/63092960] is fixed. + // else { + // drmInitData = null; + // } // If the atom type determines a MIME type, set it immediately. String mimeType = null; @@ -855,6 +1092,8 @@ import java.util.List; mimeType = MimeTypes.AUDIO_AC3; } else if (atomType == Atom.TYPE_ec_3) { mimeType = MimeTypes.AUDIO_E_AC3; + } else if (atomType == Atom.TYPE_ac_4) { + mimeType = MimeTypes.AUDIO_AC4; } else if (atomType == Atom.TYPE_dtsc) { mimeType = MimeTypes.AUDIO_DTS; } else if (atomType == Atom.TYPE_dtsh || atomType == Atom.TYPE_dtsl) { @@ -867,10 +1106,22 @@ import java.util.List; mimeType = MimeTypes.AUDIO_AMR_WB; } else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) { mimeType = MimeTypes.AUDIO_RAW; + pcmEncoding = C.ENCODING_PCM_16BIT; + } else if (atomType == Atom.TYPE_twos) { + mimeType = MimeTypes.AUDIO_RAW; + pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN; } else if (atomType == Atom.TYPE__mp3) { mimeType = MimeTypes.AUDIO_MPEG; } else if (atomType == Atom.TYPE_alac) { mimeType = MimeTypes.AUDIO_ALAC; + } else if (atomType == Atom.TYPE_alaw) { + mimeType = MimeTypes.AUDIO_ALAW; + } else if (atomType == Atom.TYPE_ulaw) { + mimeType = MimeTypes.AUDIO_MLAW; + } else if (atomType == Atom.TYPE_Opus) { + mimeType = MimeTypes.AUDIO_OPUS; + } else if (atomType == Atom.TYPE_fLaC) { + mimeType = MimeTypes.AUDIO_FLAC; } byte[] initializationData = null; @@ -888,8 +1139,8 @@ import java.util.List; mimeType = mimeTypeAndInitializationData.first; initializationData = mimeTypeAndInitializationData.second; if (MimeTypes.AUDIO_AAC.equals(mimeType)) { - // TODO: Do we really need to do this? See [Internal: b/10903778] - // Update sampleRate and channelCount from the AudioSpecificConfig initialization data. + // Update sampleRate and channelCount from the AudioSpecificConfig initialization data, + // which is more reliable. See [Internal: b/10903778]. Pair audioSpecificConfig = CodecSpecificDataUtil.parseAacAudioSpecificConfig(initializationData); sampleRate = audioSpecificConfig.first; @@ -904,22 +1155,47 @@ import java.util.List; parent.setPosition(Atom.HEADER_SIZE + childPosition); out.format = Ac3Util.parseEAc3AnnexFFormat(parent, Integer.toString(trackId), language, drmInitData); + } else if (childAtomType == Atom.TYPE_dac4) { + parent.setPosition(Atom.HEADER_SIZE + childPosition); + out.format = + Ac4Util.parseAc4AnnexEFormat(parent, Integer.toString(trackId), language, drmInitData); } else if (childAtomType == Atom.TYPE_ddts) { out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); + } else if (childAtomType == Atom.TYPE_dOps) { + // Build an Opus Identification Header (defined in RFC-7845) by concatenating the Opus Magic + // Signature and the body of the dOps atom. + int childAtomBodySize = childAtomSize - Atom.HEADER_SIZE; + initializationData = new byte[opusMagic.length + childAtomBodySize]; + System.arraycopy(opusMagic, 0, initializationData, 0, opusMagic.length); + parent.setPosition(childPosition + Atom.HEADER_SIZE); + parent.readBytes(initializationData, opusMagic.length, childAtomBodySize); + } else if (childAtomType == Atom.TYPE_dfLa) { + int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE; + initializationData = new byte[4 + childAtomBodySize]; + initializationData[0] = 0x66; // f + initializationData[1] = 0x4C; // L + initializationData[2] = 0x61; // a + initializationData[3] = 0x43; // C + parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE); + parent.readBytes(initializationData, /* offset= */ 4, childAtomBodySize); } else if (childAtomType == Atom.TYPE_alac) { - initializationData = new byte[childAtomSize]; - parent.setPosition(childPosition); - parent.readBytes(initializationData, 0, childAtomSize); + int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE; + initializationData = new byte[childAtomBodySize]; + parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE); + parent.readBytes(initializationData, /* offset= */ 0, childAtomBodySize); + // Update sampleRate and channelCount from the AudioSpecificConfig initialization data, + // which is more reliable. See https://github.com/google/ExoPlayer/pull/6629. + Pair audioSpecificConfig = + CodecSpecificDataUtil.parseAlacAudioSpecificConfig(initializationData); + sampleRate = audioSpecificConfig.first; + channelCount = audioSpecificConfig.second; } childPosition += childAtomSize; } if (out.format == null && mimeType != null) { - // TODO: Determine the correct PCM encoding. - @C.PcmEncoding int pcmEncoding = - MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT : Format.NO_VALUE; out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding, initializationData == null ? null : Collections.singletonList(initializationData), @@ -973,49 +1249,17 @@ import java.util.List; // Set the MIME type based on the object type indication (14496-1 table 5). int objectTypeIndication = parent.readUnsignedByte(); - String mimeType; - switch (objectTypeIndication) { - case 0x6B: - mimeType = MimeTypes.AUDIO_MPEG; - return Pair.create(mimeType, null); - case 0x20: - mimeType = MimeTypes.VIDEO_MP4V; - break; - case 0x21: - mimeType = MimeTypes.VIDEO_H264; - break; - case 0x23: - mimeType = MimeTypes.VIDEO_H265; - break; - case 0x40: - case 0x66: - case 0x67: - case 0x68: - mimeType = MimeTypes.AUDIO_AAC; - break; - case 0xA5: - mimeType = MimeTypes.AUDIO_AC3; - break; - case 0xA6: - mimeType = MimeTypes.AUDIO_E_AC3; - break; - case 0xA9: - case 0xAC: - mimeType = MimeTypes.AUDIO_DTS; - return Pair.create(mimeType, null); - case 0xAA: - case 0xAB: - mimeType = MimeTypes.AUDIO_DTS_HD; - return Pair.create(mimeType, null); - default: - mimeType = null; - break; + String mimeType = getMimeTypeFromMp4ObjectType(objectTypeIndication); + if (MimeTypes.AUDIO_MPEG.equals(mimeType) + || MimeTypes.AUDIO_DTS.equals(mimeType) + || MimeTypes.AUDIO_DTS_HD.equals(mimeType)) { + return Pair.create(mimeType, null); } parent.skipBytes(12); - // Start of the AudioSpecificConfig. - parent.skipBytes(1); // AudioSpecificConfig tag + // Start of the DecoderSpecificInfo. + parent.skipBytes(1); // DecoderSpecificInfo tag int initializationDataSize = parseExpandableClassSize(parent); byte[] initializationData = new byte[initializationDataSize]; parent.readBytes(initializationData, 0, initializationDataSize); @@ -1023,11 +1267,12 @@ import java.util.List; } /** - * Parses encryption data from an audio/video sample entry, populating {@code out} and returning - * the unencrypted atom type, or 0 if no common encryption sinf atom was present. + * Parses encryption data from an audio/video sample entry, returning a pair consisting of the + * unencrypted atom type and a {@link TrackEncryptionBox}. Null is returned if no common + * encryption sinf atom was present. */ - private static int parseSampleEntryEncryptionData(ParsableByteArray parent, int position, - int size, StsdData out, int entryIndex) { + private static Pair parseSampleEntryEncryptionData( + ParsableByteArray parent, int position, int size) { int childPosition = parent.getPosition(); while (childPosition - position < size) { parent.setPosition(childPosition); @@ -1035,25 +1280,23 @@ import java.util.List; Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_sinf) { - Pair result = parseSinfFromParent(parent, childPosition, - childAtomSize); + Pair result = parseCommonEncryptionSinfFromParent(parent, + childPosition, childAtomSize); if (result != null) { - out.trackEncryptionBoxes[entryIndex] = result.second; - return result.first; + return result; } } childPosition += childAtomSize; } - // This enca/encv box does not have a data format so return an invalid atom type. - return 0; + return null; } - private static Pair parseSinfFromParent(ParsableByteArray parent, - int position, int size) { + /* package */ static Pair parseCommonEncryptionSinfFromParent( + ParsableByteArray parent, int position, int size) { int childPosition = position + Atom.HEADER_SIZE; - - boolean isCencScheme = false; - TrackEncryptionBox trackEncryptionBox = null; + int schemeInformationBoxPosition = C.POSITION_UNSET; + int schemeInformationBoxSize = 0; + String schemeType = null; Integer dataFormat = null; while (childPosition - position < size) { parent.setPosition(childPosition); @@ -1063,36 +1306,61 @@ import java.util.List; dataFormat = parent.readInt(); } else if (childAtomType == Atom.TYPE_schm) { parent.skipBytes(4); - isCencScheme = parent.readInt() == TYPE_cenc; + // Common encryption scheme_type values are defined in ISO/IEC 23001-7:2016, section 4.1. + schemeType = parent.readString(4); } else if (childAtomType == Atom.TYPE_schi) { - trackEncryptionBox = parseSchiFromParent(parent, childPosition, childAtomSize); + schemeInformationBoxPosition = childPosition; + schemeInformationBoxSize = childAtomSize; } childPosition += childAtomSize; } - if (isCencScheme) { + if (C.CENC_TYPE_cenc.equals(schemeType) || C.CENC_TYPE_cbc1.equals(schemeType) + || C.CENC_TYPE_cens.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType)) { Assertions.checkArgument(dataFormat != null, "frma atom is mandatory"); - Assertions.checkArgument(trackEncryptionBox != null, "schi->tenc atom is mandatory"); - return Pair.create(dataFormat, trackEncryptionBox); + Assertions.checkArgument(schemeInformationBoxPosition != C.POSITION_UNSET, + "schi atom is mandatory"); + TrackEncryptionBox encryptionBox = parseSchiFromParent(parent, schemeInformationBoxPosition, + schemeInformationBoxSize, schemeType); + Assertions.checkArgument(encryptionBox != null, "tenc atom is mandatory"); + return Pair.create(dataFormat, encryptionBox); } else { return null; } } private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position, - int size) { + int size, String schemeType) { int childPosition = position + Atom.HEADER_SIZE; while (childPosition - position < size) { parent.setPosition(childPosition); int childAtomSize = parent.readInt(); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_tenc) { - parent.skipBytes(6); - boolean defaultIsEncrypted = parent.readUnsignedByte() == 1; - int defaultInitVectorSize = parent.readUnsignedByte(); + int fullAtom = parent.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + parent.skipBytes(1); // reserved = 0. + int defaultCryptByteBlock = 0; + int defaultSkipByteBlock = 0; + if (version == 0) { + parent.skipBytes(1); // reserved = 0. + } else /* version 1 or greater */ { + int patternByte = parent.readUnsignedByte(); + defaultCryptByteBlock = (patternByte & 0xF0) >> 4; + defaultSkipByteBlock = patternByte & 0x0F; + } + boolean defaultIsProtected = parent.readUnsignedByte() == 1; + int defaultPerSampleIvSize = parent.readUnsignedByte(); byte[] defaultKeyId = new byte[16]; parent.readBytes(defaultKeyId, 0, defaultKeyId.length); - return new TrackEncryptionBox(defaultIsEncrypted, defaultInitVectorSize, defaultKeyId); + byte[] constantIv = null; + if (defaultIsProtected && defaultPerSampleIvSize == 0) { + int constantIvSize = parent.readUnsignedByte(); + constantIv = new byte[constantIvSize]; + parent.readBytes(constantIv, 0, constantIvSize); + } + return new TrackEncryptionBox(defaultIsProtected, schemeType, defaultPerSampleIvSize, + defaultKeyId, defaultCryptByteBlock, defaultSkipByteBlock, constantIv); } childPosition += childAtomSize; } @@ -1100,7 +1368,7 @@ import java.util.List; } /** - * Parses the proj box from sv3d box, as specified by https://github.com/google/spatial-media + * Parses the proj box from sv3d box, as specified by https://github.com/google/spatial-media. */ private static byte[] parseProjFromParent(ParsableByteArray parent, int position, int size) { int childPosition = position + Atom.HEADER_SIZE; @@ -1129,6 +1397,19 @@ import java.util.List; return size; } + /** Returns whether it's possible to apply the specified edit using gapless playback info. */ + private static boolean canApplyEditWithGaplessInfo( + long[] timestamps, long duration, long editStartTime, long editEndTime) { + int lastIndex = timestamps.length - 1; + int latestDelayIndex = Util.constrainValue(MAX_GAPLESS_TRIM_SIZE_SAMPLES, 0, lastIndex); + int earliestPaddingIndex = + Util.constrainValue(timestamps.length - MAX_GAPLESS_TRIM_SIZE_SAMPLES, 0, lastIndex); + return timestamps[0] <= editStartTime + && editStartTime < timestamps[latestDelayIndex] + && timestamps[earliestPaddingIndex] < editEndTime + && editEndTime <= duration; + } + private AtomParsers() { // Prevent instantiation. } @@ -1158,7 +1439,7 @@ import java.util.List; stsc.setPosition(Atom.FULL_HEADER_SIZE); remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt(); Assertions.checkState(stsc.readInt() == 1, "first_chunk must be 1"); - index = C.INDEX_UNSET; + index = -1; } public boolean moveNext() { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java index b5294d74f704..78d30ba5824c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java @@ -33,13 +33,21 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; public final int maximumSize; public final long[] timestamps; public final int[] flags; + public final long duration; - private Results(long[] offsets, int[] sizes, int maximumSize, long[] timestamps, int[] flags) { + private Results( + long[] offsets, + int[] sizes, + int maximumSize, + long[] timestamps, + int[] flags, + long duration) { this.offsets = offsets; this.sizes = sizes; this.maximumSize = maximumSize; this.timestamps = timestamps; this.flags = flags; + this.duration = duration; } } @@ -95,8 +103,12 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; newSampleIndex++; } } + long duration = timestampDeltaInTimeUnits * originalSampleIndex; - return new Results(offsets, sizes, maximumSize, timestamps, flags); + return new Results(offsets, sizes, maximumSize, timestamps, flags, duration); } + private FixedSampleSizeRechunker() { + // Prevent instantiation. + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index ba979032b0ae..291a9ade2765 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -15,13 +15,14 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; -import android.support.annotation.IntDef; -import android.util.Log; import android.util.Pair; import android.util.SparseArray; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ChunkIndex; @@ -34,47 +35,52 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg.EventMessage; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea.CeaUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedList; +import java.util.Collections; import java.util.List; -import java.util.Stack; import java.util.UUID; -/** - * Facilitates the extraction of data from the fragmented mp4 container format. - */ -public final class FragmentedMp4Extractor implements Extractor { +/** Extracts data from the FMP4 container format. */ +@SuppressWarnings("ConstantField") +public class FragmentedMp4Extractor implements Extractor { + + /** Factory for {@link FragmentedMp4Extractor} instances. */ + public static final ExtractorsFactory FACTORY = + () -> new Extractor[] {new FragmentedMp4Extractor()}; /** - * Factory for {@link FragmentedMp4Extractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new FragmentedMp4Extractor()}; - } - - }; - - /** - * Flags controlling the behavior of the extractor. + * Flags controlling the behavior of the extractor. Possible flag values are {@link + * #FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME}, {@link #FLAG_WORKAROUND_IGNORE_TFDT_BOX}, + * {@link #FLAG_ENABLE_EMSG_TRACK}, {@link #FLAG_SIDELOADED} and {@link + * #FLAG_WORKAROUND_IGNORE_EDIT_LISTS}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, - FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_ENABLE_CEA608_TRACK, - FLAG_SIDELOADED}) + @IntDef( + flag = true, + value = { + FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, + FLAG_WORKAROUND_IGNORE_TFDT_BOX, + FLAG_ENABLE_EMSG_TRACK, + FLAG_SIDELOADED, + FLAG_WORKAROUND_IGNORE_EDIT_LISTS + }) public @interface Flags {} /** * Flag to work around an issue in some video streams where every frame is marked as a sync frame. @@ -84,30 +90,30 @@ public final class FragmentedMp4Extractor implements Extractor { * This flag does nothing if the stream is not a video stream. */ public static final int FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1; - /** - * Flag to ignore any tfdt boxes in the stream. - */ - public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 2; + /** Flag to ignore any tfdt boxes in the stream. */ + public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 1 << 1; // 2 /** * Flag to indicate that the extractor should output an event message metadata track. Any event * messages in the stream will be delivered as samples to this track. */ - public static final int FLAG_ENABLE_EMSG_TRACK = 4; - /** - * Flag to indicate that the extractor should output a CEA-608 text track. Any CEA-608 messages - * contained within SEI NAL units in the stream will be delivered as samples to this track. - */ - public static final int FLAG_ENABLE_CEA608_TRACK = 8; + public static final int FLAG_ENABLE_EMSG_TRACK = 1 << 2; // 4 /** * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4 * container. */ - private static final int FLAG_SIDELOADED = 16; + private static final int FLAG_SIDELOADED = 1 << 3; // 8 + /** Flag to ignore any edit lists in the stream. */ + public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1 << 4; // 16 private static final String TAG = "FragmentedMp4Extractor"; - private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); + + @SuppressWarnings("ConstantCaseForConstants") + private static final int SAMPLE_GROUP_TYPE_seig = 0x73656967; + private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; + private static final Format EMSG_FORMAT = + Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE); // Parser states. private static final int STATE_READING_ATOM_HEADER = 0; @@ -118,7 +124,10 @@ public final class FragmentedMp4Extractor implements Extractor { // Workarounds. @Flags private final int flags; - private final Track sideloadedTrack; + @Nullable private final Track sideloadedTrack; + + // Sideloaded data. + private final List closedCaptionFormats; // Track-linked data bundle, accessible as a whole through trackID. private final SparseArray trackBundles; @@ -127,16 +136,19 @@ public final class FragmentedMp4Extractor implements Extractor { private final ParsableByteArray nalStartCode; private final ParsableByteArray nalPrefix; private final ParsableByteArray nalBuffer; - private final ParsableByteArray encryptionSignalByte; + private final byte[] scratchBytes; + private final ParsableByteArray scratch; // Adjusts sample timestamps. - private final TimestampAdjuster timestampAdjuster; + @Nullable private final TimestampAdjuster timestampAdjuster; + + private final EventMessageEncoder eventMessageEncoder; // Parser state. private final ParsableByteArray atomHeader; - private final byte[] extendedTypeScratch; - private final Stack containerAtoms; - private final LinkedList pendingMetadataSampleInfos; + private final ArrayDeque containerAtoms; + private final ArrayDeque pendingMetadataSampleInfos; + @Nullable private final TrackOutput additionalEmsgTrackOutput; private int parserState; private int atomType; @@ -145,6 +157,7 @@ public final class FragmentedMp4Extractor implements Extractor { private ParsableByteArray atomData; private long endOfMdatPosition; private int pendingMetadataSampleBytes; + private long pendingSeekTimeUs; private long durationUs; private long segmentIndexEarliestPresentationTimeUs; @@ -156,7 +169,7 @@ public final class FragmentedMp4Extractor implements Extractor { // Extractor output. private ExtractorOutput extractorOutput; - private TrackOutput eventMessageTrackOutput; + private TrackOutput[] emsgTrackOutputs; private TrackOutput[] cea608TrackOutputs; // Whether extractorOutput.seekMap has been called. @@ -170,38 +183,85 @@ public final class FragmentedMp4Extractor implements Extractor { * @param flags Flags that control the extractor's behavior. */ public FragmentedMp4Extractor(@Flags int flags) { - this(flags, null); + this(flags, /* timestampAdjuster= */ null); } /** * @param flags Flags that control the extractor's behavior. * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ - public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) { - this(flags, timestampAdjuster, null); + public FragmentedMp4Extractor(@Flags int flags, @Nullable TimestampAdjuster timestampAdjuster) { + this(flags, timestampAdjuster, /* sideloadedTrack= */ null, Collections.emptyList()); } /** * @param flags Flags that control the extractor's behavior. * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. - * @param sideloadedTrack Sideloaded track information, in the case that the extractor - * will not receive a moov box in the input data. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not + * receive a moov box in the input data. Null if a moov box is expected. */ - public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, - Track sideloadedTrack) { + public FragmentedMp4Extractor( + @Flags int flags, + @Nullable TimestampAdjuster timestampAdjuster, + @Nullable Track sideloadedTrack) { + this(flags, timestampAdjuster, sideloadedTrack, Collections.emptyList()); + } + + /** + * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not + * receive a moov box in the input data. Null if a moov box is expected. + * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed + * caption channels to expose. + */ + public FragmentedMp4Extractor( + @Flags int flags, + @Nullable TimestampAdjuster timestampAdjuster, + @Nullable Track sideloadedTrack, + List closedCaptionFormats) { + this( + flags, + timestampAdjuster, + sideloadedTrack, + closedCaptionFormats, + /* additionalEmsgTrackOutput= */ null); + } + + /** + * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not + * receive a moov box in the input data. Null if a moov box is expected. + * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed + * caption channels to expose. + * @param additionalEmsgTrackOutput An extra track output that will receive all emsg messages + * targeting the player, even if {@link #FLAG_ENABLE_EMSG_TRACK} is not set. Null if special + * handling of emsg messages for players is not required. + */ + public FragmentedMp4Extractor( + @Flags int flags, + @Nullable TimestampAdjuster timestampAdjuster, + @Nullable Track sideloadedTrack, + List closedCaptionFormats, + @Nullable TrackOutput additionalEmsgTrackOutput) { this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.timestampAdjuster = timestampAdjuster; this.sideloadedTrack = sideloadedTrack; + this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats); + this.additionalEmsgTrackOutput = additionalEmsgTrackOutput; + eventMessageEncoder = new EventMessageEncoder(); atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalPrefix = new ParsableByteArray(5); nalBuffer = new ParsableByteArray(); - encryptionSignalByte = new ParsableByteArray(1); - extendedTypeScratch = new byte[16]; - containerAtoms = new Stack<>(); - pendingMetadataSampleInfos = new LinkedList<>(); + scratchBytes = new byte[16]; + scratch = new ParsableByteArray(scratchBytes); + containerAtoms = new ArrayDeque<>(); + pendingMetadataSampleInfos = new ArrayDeque<>(); trackBundles = new SparseArray<>(); durationUs = C.TIME_UNSET; + pendingSeekTimeUs = C.TIME_UNSET; segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET; enterReadingAtomHeaderState(); } @@ -231,6 +291,7 @@ public final class FragmentedMp4Extractor implements Extractor { } pendingMetadataSampleInfos.clear(); pendingMetadataSampleBytes = 0; + pendingSeekTimeUs = timeUs; containerAtoms.clear(); enterReadingAtomHeaderState(); } @@ -281,12 +342,22 @@ public final class FragmentedMp4Extractor implements Extractor { atomType = atomHeader.readInt(); } - if (atomSize == Atom.LONG_SIZE_PREFIX) { - // Read the extended atom size. + if (atomSize == Atom.DEFINES_LARGE_SIZE) { + // Read the large size. int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE; input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining); atomHeaderBytesRead += headerBytesRemaining; atomSize = atomHeader.readUnsignedLongToLong(); + } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) { + // The atom extends to the end of the file. Note that if the atom is within a container we can + // work out its size even if the input length is unknown. + long endPosition = input.getLength(); + if (endPosition == C.LENGTH_UNSET && !containerAtoms.isEmpty()) { + endPosition = containerAtoms.peek().endPosition; + } + if (endPosition != C.LENGTH_UNSET) { + atomSize = endPosition - input.getPosition() + atomHeaderBytesRead; + } } if (atomSize < atomHeaderBytesRead) { @@ -309,7 +380,8 @@ public final class FragmentedMp4Extractor implements Extractor { currentTrackBundle = null; endOfMdatPosition = atomPosition + atomSize; if (!haveOutputSeekMap) { - extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); + // This must be the first mdat in the stream. + extractorOutput.seekMap(new SeekMap.Unseekable(durationUs, atomPosition)); haveOutputSeekMap = true; } parserState = STATE_READING_ENCRYPTION_DATA; @@ -318,7 +390,7 @@ public final class FragmentedMp4Extractor implements Extractor { if (shouldParseContainerAtom(atomType)) { long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE; - containerAtoms.add(new ContainerAtom(atomType, endPosition)); + containerAtoms.push(new ContainerAtom(atomType, endPosition)); if (atomSize == atomHeaderBytesRead) { processAtomEnded(endPosition); } else { @@ -390,7 +462,7 @@ public final class FragmentedMp4Extractor implements Extractor { private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException { Assertions.checkState(sideloadedTrack == null, "Unexpected moov box."); - DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren); + @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren); // Read declaration of track fragments in the Moov box. ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex); @@ -413,8 +485,15 @@ public final class FragmentedMp4Extractor implements Extractor { for (int i = 0; i < moovContainerChildrenSize; i++) { Atom.ContainerAtom atom = moov.containerChildren.get(i); if (atom.type == Atom.TYPE_trak) { - Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), duration, - drmInitData, false); + Track track = + modifyTrack( + AtomParsers.parseTrak( + atom, + moov.getLeafAtomOfType(Atom.TYPE_mvhd), + duration, + drmInitData, + (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, + false)); if (track != null) { tracks.put(track.id, track); } @@ -427,7 +506,7 @@ public final class FragmentedMp4Extractor implements Extractor { for (int i = 0; i < trackCount; i++) { Track track = tracks.valueAt(i); TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type)); - trackBundle.init(track, defaultSampleValuesArray.get(track.id)); + trackBundle.init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id)); trackBundles.put(track.id, trackBundle); durationUs = Math.max(durationUs, track.durationUs); } @@ -437,72 +516,151 @@ public final class FragmentedMp4Extractor implements Extractor { Assertions.checkState(trackBundles.size() == trackCount); for (int i = 0; i < trackCount; i++) { Track track = tracks.valueAt(i); - trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id)); + trackBundles + .get(track.id) + .init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id)); } } } + @Nullable + protected Track modifyTrack(@Nullable Track track) { + return track; + } + + private DefaultSampleValues getDefaultSampleValues( + SparseArray defaultSampleValuesArray, int trackId) { + if (defaultSampleValuesArray.size() == 1) { + // Ignore track id if there is only one track to cope with non-matching track indices. + // See https://github.com/google/ExoPlayer/issues/4477. + return defaultSampleValuesArray.valueAt(/* index= */ 0); + } + return Assertions.checkNotNull(defaultSampleValuesArray.get(trackId)); + } + private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { - parseMoof(moof, trackBundles, flags, extendedTypeScratch); - DrmInitData drmInitData = getDrmInitDataFromAtoms(moof.leafChildren); + parseMoof(moof, trackBundles, flags, scratchBytes); + + @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moof.leafChildren); if (drmInitData != null) { int trackCount = trackBundles.size(); for (int i = 0; i < trackCount; i++) { trackBundles.valueAt(i).updateDrmInitData(drmInitData); } } + // If we have a pending seek, advance tracks to their preceding sync frames. + if (pendingSeekTimeUs != C.TIME_UNSET) { + int trackCount = trackBundles.size(); + for (int i = 0; i < trackCount; i++) { + trackBundles.valueAt(i).seek(pendingSeekTimeUs); + } + pendingSeekTimeUs = C.TIME_UNSET; + } } private void maybeInitExtraTracks() { - if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0 && eventMessageTrackOutput == null) { - eventMessageTrackOutput = extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA); - eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, - Format.OFFSET_SAMPLE_RELATIVE)); + if (emsgTrackOutputs == null) { + emsgTrackOutputs = new TrackOutput[2]; + int emsgTrackOutputCount = 0; + if (additionalEmsgTrackOutput != null) { + emsgTrackOutputs[emsgTrackOutputCount++] = additionalEmsgTrackOutput; + } + if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0) { + emsgTrackOutputs[emsgTrackOutputCount++] = + extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA); + } + emsgTrackOutputs = Arrays.copyOf(emsgTrackOutputs, emsgTrackOutputCount); + + for (TrackOutput eventMessageTrackOutput : emsgTrackOutputs) { + eventMessageTrackOutput.format(EMSG_FORMAT); + } } - if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutputs == null) { - TrackOutput cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1, - C.TRACK_TYPE_TEXT); - cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, - null, Format.NO_VALUE, 0, null, null)); - cea608TrackOutputs = new TrackOutput[] {cea608TrackOutput}; + if (cea608TrackOutputs == null) { + cea608TrackOutputs = new TrackOutput[closedCaptionFormats.size()]; + for (int i = 0; i < cea608TrackOutputs.length; i++) { + TrackOutput output = extractorOutput.track(trackBundles.size() + 1 + i, C.TRACK_TYPE_TEXT); + output.format(closedCaptionFormats.get(i)); + cea608TrackOutputs[i] = output; + } } } - /** - * Handles an emsg atom (defined in 23009-1). - */ + /** Handles an emsg atom (defined in 23009-1). */ private void onEmsgLeafAtomRead(ParsableByteArray atom) { - if (eventMessageTrackOutput == null) { + if (emsgTrackOutputs == null || emsgTrackOutputs.length == 0) { return; } - // Parse the event's presentation time delta. - atom.setPosition(Atom.FULL_HEADER_SIZE); - atom.readNullTerminatedString(); // schemeIdUri - atom.readNullTerminatedString(); // value - long timescale = atom.readUnsignedInt(); - long presentationTimeDeltaUs = - Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale); + atom.setPosition(Atom.HEADER_SIZE); + int fullAtom = atom.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + String schemeIdUri; + String value; + long timescale; + long presentationTimeDeltaUs = C.TIME_UNSET; // Only set if version == 0 + long sampleTimeUs = C.TIME_UNSET; + long durationMs; + long id; + switch (version) { + case 0: + schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString()); + value = Assertions.checkNotNull(atom.readNullTerminatedString()); + timescale = atom.readUnsignedInt(); + presentationTimeDeltaUs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale); + if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) { + sampleTimeUs = segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs; + } + durationMs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); + id = atom.readUnsignedInt(); + break; + case 1: + timescale = atom.readUnsignedInt(); + sampleTimeUs = + Util.scaleLargeTimestamp(atom.readUnsignedLongToLong(), C.MICROS_PER_SECOND, timescale); + durationMs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); + id = atom.readUnsignedInt(); + schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString()); + value = Assertions.checkNotNull(atom.readNullTerminatedString()); + break; + default: + Log.w(TAG, "Skipping unsupported emsg version: " + version); + return; + } + + byte[] messageData = new byte[atom.bytesLeft()]; + atom.readBytes(messageData, /*offset=*/ 0, atom.bytesLeft()); + EventMessage eventMessage = new EventMessage(schemeIdUri, value, durationMs, id, messageData); + ParsableByteArray encodedEventMessage = + new ParsableByteArray(eventMessageEncoder.encode(eventMessage)); + int sampleSize = encodedEventMessage.bytesLeft(); + // Output the sample data. - atom.setPosition(Atom.FULL_HEADER_SIZE); - int sampleSize = atom.bytesLeft(); - eventMessageTrackOutput.sampleData(atom, sampleSize); - // Output the sample metadata. - if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) { - // We can output the sample metadata immediately. - eventMessageTrackOutput.sampleMetadata( - segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs, - C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0 /* offset */, null); - } else { + for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { + encodedEventMessage.setPosition(0); + emsgTrackOutput.sampleData(encodedEventMessage, sampleSize); + } + + // Output the sample metadata. This is made a little complicated because emsg-v0 atoms + // have presentation time *delta* while v1 atoms have absolute presentation time. + if (sampleTimeUs == C.TIME_UNSET) { // We need the first sample timestamp in the segment before we can output the metadata. pendingMetadataSampleInfos.addLast( new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize)); pendingMetadataSampleBytes += sampleSize; + } else { + if (timestampAdjuster != null) { + sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); + } + for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { + emsgTrackOutput.sampleMetadata( + sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, /* offset= */ 0, null); + } } } - /** - * Parses a trex atom (defined in 14496-12). - */ + /** Parses a trex atom (defined in 14496-12). */ private static Pair parseTrex(ParsableByteArray trex) { trex.setPosition(Atom.FULL_HEADER_SIZE); int trackId = trex.readInt(); @@ -543,7 +701,7 @@ public final class FragmentedMp4Extractor implements Extractor { private static void parseTraf(ContainerAtom traf, SparseArray trackBundleArray, @Flags int flags, byte[] extendedTypeScratch) throws ParserException { LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); - TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray, flags); + TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray); if (trackBundle == null) { return; } @@ -559,11 +717,12 @@ public final class FragmentedMp4Extractor implements Extractor { parseTruns(traf, trackBundle, decodeTime, flags); + TrackEncryptionBox encryptionBox = trackBundle.track + .getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); + LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); if (saiz != null) { - TrackEncryptionBox trackEncryptionBox = trackBundle.track - .sampleDescriptionEncryptionBoxes[fragment.header.sampleDescriptionIndex]; - parseSaiz(trackEncryptionBox, saiz.data, fragment); + parseSaiz(encryptionBox, saiz.data, fragment); } LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio); @@ -579,7 +738,8 @@ public final class FragmentedMp4Extractor implements Extractor { LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp); LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd); if (sbgp != null && sgpd != null) { - parseSgpd(sbgp.data, sgpd.data, fragment); + parseSgpd(sbgp.data, sgpd.data, encryptionBox != null ? encryptionBox.schemeType : null, + fragment); } int leafChildrenSize = traf.leafChildren.size(); @@ -627,7 +787,7 @@ public final class FragmentedMp4Extractor implements Extractor { private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz, TrackFragment out) throws ParserException { - int vectorSize = encryptionBox.initializationVectorSize; + int vectorSize = encryptionBox.perSampleIvSize; saiz.setPosition(Atom.HEADER_SIZE); int fullAtom = saiz.readInt(); int flags = Atom.parseFullAtomFlags(fullAtom); @@ -692,13 +852,13 @@ public final class FragmentedMp4Extractor implements Extractor { * @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd * does not refer to any {@link TrackBundle}. */ - private static TrackBundle parseTfhd(ParsableByteArray tfhd, - SparseArray trackBundles, int flags) { + private static TrackBundle parseTfhd( + ParsableByteArray tfhd, SparseArray trackBundles) { tfhd.setPosition(Atom.HEADER_SIZE); int fullAtom = tfhd.readInt(); int atomFlags = Atom.parseFullAtomFlags(fullAtom); int trackId = tfhd.readInt(); - TrackBundle trackBundle = trackBundles.get((flags & FLAG_SIDELOADED) == 0 ? trackId : 0); + TrackBundle trackBundle = getTrackBundle(trackBundles, trackId); if (trackBundle == null) { return null; } @@ -723,6 +883,17 @@ public final class FragmentedMp4Extractor implements Extractor { return trackBundle; } + private static @Nullable TrackBundle getTrackBundle( + SparseArray trackBundles, int trackId) { + if (trackBundles.size() == 1) { + // Ignore track id if there is only one track. This is either because we have a side-loaded + // track (flag FLAG_SIDELOADED) or to cope with non-matching track indices (see + // https://github.com/google/ExoPlayer/issues/4083). + return trackBundles.valueAt(/* index= */ 0); + } + return trackBundles.get(trackId); + } + /** * Parses a tfdt atom (defined in 14496-12). * @@ -783,7 +954,9 @@ public final class FragmentedMp4Extractor implements Extractor { // duration == 0). Other uses of edit lists are uncommon and unsupported. if (track.editListDurations != null && track.editListDurations.length == 1 && track.editListDurations[0] == 0) { - edtsOffset = Util.scaleLargeTimestamp(track.editListMediaTimes[0], 1000, track.timescale); + edtsOffset = + Util.scaleLargeTimestamp( + track.editListMediaTimes[0], C.MILLIS_PER_SECOND, track.timescale); } int[] sampleSizeTable = fragment.sampleSizeTable; @@ -811,12 +984,13 @@ public final class FragmentedMp4Extractor implements Extractor { // here, because unsigned integers will still be parsed correctly (unless their top bit is // set, which is never true in practice because sample offsets are always small). int sampleOffset = trun.readInt(); - sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale); + sampleCompositionTimeOffsetTable[i] = + (int) ((sampleOffset * C.MILLIS_PER_SECOND) / timescale); } else { sampleCompositionTimeOffsetTable[i] = 0; } sampleDecodingTimeTable[i] = - Util.scaleLargeTimestamp(cumulativeTime, 1000, timescale) - edtsOffset; + Util.scaleLargeTimestamp(cumulativeTime, C.MILLIS_PER_SECOND, timescale) - edtsOffset; sampleSizeTable[i] = sampleSize; sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0 && (!workaroundEveryVideoFrameIsSyncFrame || i == 0); @@ -868,8 +1042,8 @@ public final class FragmentedMp4Extractor implements Extractor { out.fillEncryptionData(senc); } - private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, TrackFragment out) - throws ParserException { + private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, String schemeType, + TrackFragment out) throws ParserException { sbgp.setPosition(Atom.HEADER_SIZE); int sbgpFullAtom = sbgp.readInt(); if (sbgp.readInt() != SAMPLE_GROUP_TYPE_seig) { @@ -877,9 +1051,9 @@ public final class FragmentedMp4Extractor implements Extractor { return; } if (Atom.parseFullAtomVersion(sbgpFullAtom) == 1) { - sbgp.skipBytes(4); + sbgp.skipBytes(4); // default_length. } - if (sbgp.readInt() != 1) { + if (sbgp.readInt() != 1) { // entry_count. throw new ParserException("Entry count in sbgp != 1 (unsupported)."); } @@ -892,25 +1066,35 @@ public final class FragmentedMp4Extractor implements Extractor { int sgpdVersion = Atom.parseFullAtomVersion(sgpdFullAtom); if (sgpdVersion == 1) { if (sgpd.readUnsignedInt() == 0) { - throw new ParserException("Variable length decription in sgpd found (unsupported)"); + throw new ParserException("Variable length description in sgpd found (unsupported)"); } } else if (sgpdVersion >= 2) { - sgpd.skipBytes(4); + sgpd.skipBytes(4); // default_sample_description_index. } - if (sgpd.readUnsignedInt() != 1) { + if (sgpd.readUnsignedInt() != 1) { // entry_count. throw new ParserException("Entry count in sgpd != 1 (unsupported)."); } // CencSampleEncryptionInformationGroupEntry - sgpd.skipBytes(2); + sgpd.skipBytes(1); // reserved = 0. + int patternByte = sgpd.readUnsignedByte(); + int cryptByteBlock = (patternByte & 0xF0) >> 4; + int skipByteBlock = patternByte & 0x0F; boolean isProtected = sgpd.readUnsignedByte() == 1; if (!isProtected) { return; } - int initVectorSize = sgpd.readUnsignedByte(); + int perSampleIvSize = sgpd.readUnsignedByte(); byte[] keyId = new byte[16]; sgpd.readBytes(keyId, 0, keyId.length); + byte[] constantIv = null; + if (perSampleIvSize == 0) { + int constantIvSize = sgpd.readUnsignedByte(); + constantIv = new byte[constantIvSize]; + sgpd.readBytes(constantIv, 0, constantIvSize); + } out.definesEncryptionData = true; - out.trackEncryptionBox = new TrackEncryptionBox(isProtected, initVectorSize, keyId); + out.trackEncryptionBox = new TrackEncryptionBox(isProtected, schemeType, perSampleIvSize, keyId, + cryptByteBlock, skipByteBlock, constantIv); } /** @@ -1003,16 +1187,18 @@ public final class FragmentedMp4Extractor implements Extractor { } /** - * Attempts to extract the next sample in the current mdat atom. - *

- * If there are no more samples in the current mdat atom then the parser state is transitioned + * Attempts to read the next sample in the current mdat atom. The read sample may be output or + * skipped. + * + *

If there are no more samples in the current mdat atom then the parser state is transitioned * to {@link #STATE_READING_ATOM_HEADER} and {@code false} is returned. - *

- * It is possible for a sample to be extracted in part in the case that an exception is thrown. In - * this case the method can be called again to extract the remainder of the sample. + * + *

It is possible for a sample to be partially read in the case that an exception is thrown. In + * this case the method can be called again to read the remainder of the sample. * * @param input The {@link ExtractorInput} from which to read data. - * @return Whether a sample was extracted. + * @return Whether a sample was read. The read sample may have been output or skipped. False + * indicates that there are no samples left to read in the current mdat. * @throws IOException If an error occurs reading from the input. * @throws InterruptedException If the thread is interrupted. */ @@ -1044,18 +1230,37 @@ public final class FragmentedMp4Extractor implements Extractor { input.skipFully(bytesToSkip); this.currentTrackBundle = currentTrackBundle; } + sampleSize = currentTrackBundle.fragment .sampleSizeTable[currentTrackBundle.currentSampleIndex]; - if (currentTrackBundle.fragment.definesEncryptionData) { - sampleBytesWritten = appendSampleEncryptionData(currentTrackBundle); - sampleSize += sampleBytesWritten; - } else { - sampleBytesWritten = 0; + + if (currentTrackBundle.currentSampleIndex < currentTrackBundle.firstSampleToOutputIndex) { + input.skipFully(sampleSize); + currentTrackBundle.skipSampleEncryptionData(); + if (!currentTrackBundle.next()) { + currentTrackBundle = null; + } + parserState = STATE_READING_SAMPLE_START; + return true; } + if (currentTrackBundle.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { sampleSize -= Atom.HEADER_SIZE; input.skipFully(Atom.HEADER_SIZE); } + + if (MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType)) { + // AC4 samples need to be prefixed with a clear sample header. + sampleBytesWritten = + currentTrackBundle.outputSampleEncryptionData(sampleSize, Ac4Util.SAMPLE_HEADER_SIZE); + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + currentTrackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); + sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; + } else { + sampleBytesWritten = + currentTrackBundle.outputSampleEncryptionData(sampleSize, /* clearHeaderSize= */ 0); + } + sampleSize += sampleBytesWritten; parserState = STATE_READING_SAMPLE_CONTINUE; sampleCurrentNalBytesRemaining = 0; } @@ -1064,6 +1269,10 @@ public final class FragmentedMp4Extractor implements Extractor { Track track = currentTrackBundle.track; TrackOutput output = currentTrackBundle.output; int sampleIndex = currentTrackBundle.currentSampleIndex; + long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L; + if (timestampAdjuster != null) { + sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); + } if (track.nalUnitLengthFieldLength != 0) { // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // they're only 1 or 2 bytes long. @@ -1081,13 +1290,17 @@ public final class FragmentedMp4Extractor implements Extractor { // Read the NAL length so that we know where we find the next one, and its type. input.readFully(nalPrefixData, nalUnitLengthFieldLengthDiff, nalUnitPrefixLength); nalPrefix.setPosition(0); - sampleCurrentNalBytesRemaining = nalPrefix.readUnsignedIntToInt() - 1; + int nalLengthInt = nalPrefix.readInt(); + if (nalLengthInt < 1) { + throw new ParserException("Invalid NAL length"); + } + sampleCurrentNalBytesRemaining = nalLengthInt - 1; // Write a start code for the current NAL unit. nalStartCode.setPosition(0); output.sampleData(nalStartCode, 4); // Write the NAL unit type byte. output.sampleData(nalPrefix, 1); - processSeiNalUnitPayload = cea608TrackOutputs != null + processSeiNalUnitPayload = cea608TrackOutputs.length > 0 && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); sampleBytesWritten += 5; sampleSize += nalUnitLengthFieldLengthDiff; @@ -1104,8 +1317,7 @@ public final class FragmentedMp4Extractor implements Extractor { // If the format is H.265/HEVC the NAL unit header has two bytes so skip one more byte. nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0); nalBuffer.setLimit(unescapedLength); - CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer, - cea608TrackOutputs); + CeaUtil.consume(sampleTimeUs, nalBuffer, cea608TrackOutputs); } else { // Write the payload of the NAL unit. writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); @@ -1121,41 +1333,47 @@ public final class FragmentedMp4Extractor implements Extractor { } } - long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L; - @C.BufferFlags int sampleFlags = (fragment.definesEncryptionData ? C.BUFFER_FLAG_ENCRYPTED : 0) - | (fragment.sampleIsSyncFrameTable[sampleIndex] ? C.BUFFER_FLAG_KEY_FRAME : 0); - int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex; - byte[] encryptionKey = null; - if (fragment.definesEncryptionData) { - encryptionKey = fragment.trackEncryptionBox != null - ? fragment.trackEncryptionBox.keyId - : track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex].keyId; - } - if (timestampAdjuster != null) { - sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); - } - output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey); + @C.BufferFlags int sampleFlags = fragment.sampleIsSyncFrameTable[sampleIndex] + ? C.BUFFER_FLAG_KEY_FRAME : 0; - while (!pendingMetadataSampleInfos.isEmpty()) { - MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst(); - pendingMetadataSampleBytes -= sampleInfo.size; - eventMessageTrackOutput.sampleMetadata( - sampleTimeUs + sampleInfo.presentationTimeDeltaUs, - C.BUFFER_FLAG_KEY_FRAME, sampleInfo.size, pendingMetadataSampleBytes, null); + // Encryption data. + TrackOutput.CryptoData cryptoData = null; + TrackEncryptionBox encryptionBox = currentTrackBundle.getEncryptionBoxIfEncrypted(); + if (encryptionBox != null) { + sampleFlags |= C.BUFFER_FLAG_ENCRYPTED; + cryptoData = encryptionBox.cryptoData; } - currentTrackBundle.currentSampleIndex++; - currentTrackBundle.currentSampleInTrackRun++; - if (currentTrackBundle.currentSampleInTrackRun - == fragment.trunLength[currentTrackBundle.currentTrackRunIndex]) { - currentTrackBundle.currentTrackRunIndex++; - currentTrackBundle.currentSampleInTrackRun = 0; + output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, cryptoData); + + // After we have the sampleTimeUs, we can commit all the pending metadata samples + outputPendingMetadataSamples(sampleTimeUs); + if (!currentTrackBundle.next()) { currentTrackBundle = null; } parserState = STATE_READING_SAMPLE_START; return true; } + private void outputPendingMetadataSamples(long sampleTimeUs) { + while (!pendingMetadataSampleInfos.isEmpty()) { + MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst(); + pendingMetadataSampleBytes -= sampleInfo.size; + long metadataTimeUs = sampleTimeUs + sampleInfo.presentationTimeDeltaUs; + if (timestampAdjuster != null) { + metadataTimeUs = timestampAdjuster.adjustSampleTimestamp(metadataTimeUs); + } + for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { + emsgTrackOutput.sampleMetadata( + metadataTimeUs, + C.BUFFER_FLAG_KEY_FRAME, + sampleInfo.size, + pendingMetadataSampleBytes, + null); + } + } + } + /** * Returns the {@link TrackBundle} whose fragment run has the earliest file position out of those * yet to be consumed, or null if all have been consumed. @@ -1180,46 +1398,8 @@ public final class FragmentedMp4Extractor implements Extractor { return nextTrackBundle; } - /** - * Appends the corresponding encryption data to the {@link TrackOutput} contained in the given - * {@link TrackBundle}. - * - * @param trackBundle The {@link TrackBundle} that contains the {@link Track} for which the - * Sample encryption data must be output. - * @return The number of written bytes. - */ - private int appendSampleEncryptionData(TrackBundle trackBundle) { - TrackFragment trackFragment = trackBundle.fragment; - ParsableByteArray sampleEncryptionData = trackFragment.sampleEncryptionData; - int sampleDescriptionIndex = trackFragment.header.sampleDescriptionIndex; - TrackEncryptionBox encryptionBox = trackFragment.trackEncryptionBox != null - ? trackFragment.trackEncryptionBox - : trackBundle.track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; - int vectorSize = encryptionBox.initializationVectorSize; - boolean subsampleEncryption = trackFragment - .sampleHasSubsampleEncryptionTable[trackBundle.currentSampleIndex]; - - // Write the signal byte, containing the vector size and the subsample encryption flag. - encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0)); - encryptionSignalByte.setPosition(0); - TrackOutput output = trackBundle.output; - output.sampleData(encryptionSignalByte, 1); - // Write the vector. - output.sampleData(sampleEncryptionData, vectorSize); - // If we don't have subsample encryption data, we're done. - if (!subsampleEncryption) { - return 1 + vectorSize; - } - // Write the subsample encryption data. - int subsampleCount = sampleEncryptionData.readUnsignedShort(); - sampleEncryptionData.skipBytes(-2); - int subsampleDataLength = 2 + 6 * subsampleCount; - output.sampleData(sampleEncryptionData, subsampleDataLength); - return 1 + vectorSize + subsampleDataLength; - } - - /** Returns DrmInitData from leaf atoms. */ + @Nullable private static DrmInitData getDrmInitDataFromAtoms(List leafChildren) { ArrayList schemeDatas = null; int leafChildrenSize = leafChildren.size(); @@ -1279,18 +1459,28 @@ public final class FragmentedMp4Extractor implements Extractor { */ private static final class TrackBundle { - public final TrackFragment fragment; + private static final int SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH = 8; + public final TrackOutput output; + public final TrackFragment fragment; + public final ParsableByteArray scratch; public Track track; public DefaultSampleValues defaultSampleValues; public int currentSampleIndex; public int currentSampleInTrackRun; public int currentTrackRunIndex; + public int firstSampleToOutputIndex; + + private final ParsableByteArray encryptionSignalByte; + private final ParsableByteArray defaultInitializationVector; public TrackBundle(TrackOutput output) { - fragment = new TrackFragment(); this.output = output; + fragment = new TrackFragment(); + scratch = new ParsableByteArray(); + encryptionSignalByte = new ParsableByteArray(1); + defaultInitializationVector = new ParsableByteArray(); } public void init(Track track, DefaultSampleValues defaultSampleValues) { @@ -1300,16 +1490,171 @@ public final class FragmentedMp4Extractor implements Extractor { reset(); } + public void updateDrmInitData(DrmInitData drmInitData) { + TrackEncryptionBox encryptionBox = + track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); + String schemeType = encryptionBox != null ? encryptionBox.schemeType : null; + output.format(track.format.copyWithDrmInitData(drmInitData.copyWithSchemeType(schemeType))); + } + + /** Resets the current fragment and sample indices. */ public void reset() { fragment.reset(); currentSampleIndex = 0; currentTrackRunIndex = 0; currentSampleInTrackRun = 0; + firstSampleToOutputIndex = 0; } - public void updateDrmInitData(DrmInitData drmInitData) { - output.format(track.format.copyWithDrmInitData(drmInitData)); + /** + * Advances {@link #firstSampleToOutputIndex} to point to the sync sample before the specified + * seek time in the current fragment. + * + * @param timeUs The seek time, in microseconds. + */ + public void seek(long timeUs) { + long timeMs = C.usToMs(timeUs); + int searchIndex = currentSampleIndex; + while (searchIndex < fragment.sampleCount + && fragment.getSamplePresentationTime(searchIndex) < timeMs) { + if (fragment.sampleIsSyncFrameTable[searchIndex]) { + firstSampleToOutputIndex = searchIndex; + } + searchIndex++; + } } + + /** + * Advances the indices in the bundle to point to the next sample in the current fragment. If + * the current sample is the last one in the current fragment, then the advanced state will be + * {@code currentSampleIndex == fragment.sampleCount}, {@code currentTrackRunIndex == + * fragment.trunCount} and {@code #currentSampleInTrackRun == 0}. + * + * @return Whether the next sample is in the same track run as the previous one. + */ + public boolean next() { + currentSampleIndex++; + currentSampleInTrackRun++; + if (currentSampleInTrackRun == fragment.trunLength[currentTrackRunIndex]) { + currentTrackRunIndex++; + currentSampleInTrackRun = 0; + return false; + } + return true; + } + + /** + * Outputs the encryption data for the current sample. + * + * @param sampleSize The size of the current sample in bytes, excluding any additional clear + * header that will be prefixed to the sample by the extractor. + * @param clearHeaderSize The size of a clear header that will be prefixed to the sample by the + * extractor, or 0. + * @return The number of written bytes. + */ + public int outputSampleEncryptionData(int sampleSize, int clearHeaderSize) { + TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); + if (encryptionBox == null) { + return 0; + } + + ParsableByteArray initializationVectorData; + int vectorSize; + if (encryptionBox.perSampleIvSize != 0) { + initializationVectorData = fragment.sampleEncryptionData; + vectorSize = encryptionBox.perSampleIvSize; + } else { + // The default initialization vector should be used. + byte[] initVectorData = encryptionBox.defaultInitializationVector; + defaultInitializationVector.reset(initVectorData, initVectorData.length); + initializationVectorData = defaultInitializationVector; + vectorSize = initVectorData.length; + } + + boolean haveSubsampleEncryptionTable = + fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex); + boolean writeSubsampleEncryptionData = haveSubsampleEncryptionTable || clearHeaderSize != 0; + + // Write the signal byte, containing the vector size and the subsample encryption flag. + encryptionSignalByte.data[0] = + (byte) (vectorSize | (writeSubsampleEncryptionData ? 0x80 : 0)); + encryptionSignalByte.setPosition(0); + output.sampleData(encryptionSignalByte, 1); + // Write the vector. + output.sampleData(initializationVectorData, vectorSize); + + if (!writeSubsampleEncryptionData) { + return 1 + vectorSize; + } + + if (!haveSubsampleEncryptionTable) { + // The sample is fully encrypted, except for the additional clear header that the extractor + // is going to prefix. We need to synthesize subsample encryption data that takes the header + // into account. + scratch.reset(SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH); + // subsampleCount = 1 (unsigned short) + scratch.data[0] = (byte) 0; + scratch.data[1] = (byte) 1; + // clearDataSize = clearHeaderSize (unsigned short) + scratch.data[2] = (byte) ((clearHeaderSize >> 8) & 0xFF); + scratch.data[3] = (byte) (clearHeaderSize & 0xFF); + // encryptedDataSize = sampleSize (unsigned short) + scratch.data[4] = (byte) ((sampleSize >> 24) & 0xFF); + scratch.data[5] = (byte) ((sampleSize >> 16) & 0xFF); + scratch.data[6] = (byte) ((sampleSize >> 8) & 0xFF); + scratch.data[7] = (byte) (sampleSize & 0xFF); + output.sampleData(scratch, SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH); + return 1 + vectorSize + SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH; + } + + ParsableByteArray subsampleEncryptionData = fragment.sampleEncryptionData; + int subsampleCount = subsampleEncryptionData.readUnsignedShort(); + subsampleEncryptionData.skipBytes(-2); + int subsampleDataLength = 2 + 6 * subsampleCount; + + if (clearHeaderSize != 0) { + // We need to account for the additional clear header by adding clearHeaderSize to + // clearDataSize for the first subsample specified in the subsample encryption data. + scratch.reset(subsampleDataLength); + scratch.readBytes(subsampleEncryptionData.data, /* offset= */ 0, subsampleDataLength); + subsampleEncryptionData.skipBytes(subsampleDataLength); + + int clearDataSize = (scratch.data[2] & 0xFF) << 8 | (scratch.data[3] & 0xFF); + int adjustedClearDataSize = clearDataSize + clearHeaderSize; + scratch.data[2] = (byte) ((adjustedClearDataSize >> 8) & 0xFF); + scratch.data[3] = (byte) (adjustedClearDataSize & 0xFF); + subsampleEncryptionData = scratch; + } + + output.sampleData(subsampleEncryptionData, subsampleDataLength); + return 1 + vectorSize + subsampleDataLength; + } + + /** Skips the encryption data for the current sample. */ + private void skipSampleEncryptionData() { + TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); + if (encryptionBox == null) { + return; + } + + ParsableByteArray sampleEncryptionData = fragment.sampleEncryptionData; + if (encryptionBox.perSampleIvSize != 0) { + sampleEncryptionData.skipBytes(encryptionBox.perSampleIvSize); + } + if (fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex)) { + sampleEncryptionData.skipBytes(6 * sampleEncryptionData.readUnsignedShort()); + } + } + + private TrackEncryptionBox getEncryptionBoxIfEncrypted() { + int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex; + TrackEncryptionBox encryptionBox = + fragment.trackEncryptionBox != null + ? fragment.trackEncryptionBox + : track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex); + return encryptionBox != null && encryptionBox.isEncrypted ? encryptionBox : null; + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java new file mode 100644 index 000000000000..7040df64254d --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java @@ -0,0 +1,115 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.Arrays; + +/** + * Stores extensible metadata with handler type 'mdta'. See also the QuickTime File Format + * Specification. + */ +public final class MdtaMetadataEntry implements Metadata.Entry { + + /** The metadata key name. */ + public final String key; + /** The payload. The interpretation of the value depends on {@link #typeIndicator}. */ + public final byte[] value; + /** The four byte locale indicator. */ + public final int localeIndicator; + /** The four byte type indicator. */ + public final int typeIndicator; + + /** Creates a new metadata entry for the specified metadata key/value. */ + public MdtaMetadataEntry(String key, byte[] value, int localeIndicator, int typeIndicator) { + this.key = key; + this.value = value; + this.localeIndicator = localeIndicator; + this.typeIndicator = typeIndicator; + } + + private MdtaMetadataEntry(Parcel in) { + key = Util.castNonNull(in.readString()); + value = new byte[in.readInt()]; + in.readByteArray(value); + localeIndicator = in.readInt(); + typeIndicator = in.readInt(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + MdtaMetadataEntry other = (MdtaMetadataEntry) obj; + return key.equals(other.key) + && Arrays.equals(value, other.value) + && localeIndicator == other.localeIndicator + && typeIndicator == other.typeIndicator; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + key.hashCode(); + result = 31 * result + Arrays.hashCode(value); + result = 31 * result + localeIndicator; + result = 31 * result + typeIndicator; + return result; + } + + @Override + public String toString() { + return "mdta: key=" + key; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(key); + dest.writeInt(value.length); + dest.writeByteArray(value); + dest.writeInt(localeIndicator); + dest.writeInt(typeIndicator); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public MdtaMetadataEntry createFromParcel(Parcel in) { + return new MdtaMetadataEntry(in); + } + + @Override + public MdtaMetadataEntry[] newArray(int size) { + return new MdtaMetadataEntry[size]; + } + }; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java index 0d7eb7b83565..7d4de0e49841 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -15,108 +15,336 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; -import android.util.Log; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.GaplessInfoHolder; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.ApicFrame; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.CommentFrame; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Frame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.InternalFrame; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.TextInformationFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.nio.ByteBuffer; -/** - * Parses metadata items stored in ilst atoms. - */ +/** Utilities for handling metadata in MP4. */ /* package */ final class MetadataUtil { private static final String TAG = "MetadataUtil"; // Codes that start with the copyright character (omitted) and have equivalent ID3 frames. - private static final int SHORT_TYPE_NAME_1 = Util.getIntegerCodeForString("nam"); - private static final int SHORT_TYPE_NAME_2 = Util.getIntegerCodeForString("trk"); - private static final int SHORT_TYPE_COMMENT = Util.getIntegerCodeForString("cmt"); - private static final int SHORT_TYPE_YEAR = Util.getIntegerCodeForString("day"); - private static final int SHORT_TYPE_ARTIST = Util.getIntegerCodeForString("ART"); - private static final int SHORT_TYPE_ENCODER = Util.getIntegerCodeForString("too"); - private static final int SHORT_TYPE_ALBUM = Util.getIntegerCodeForString("alb"); - private static final int SHORT_TYPE_COMPOSER_1 = Util.getIntegerCodeForString("com"); - private static final int SHORT_TYPE_COMPOSER_2 = Util.getIntegerCodeForString("wrt"); - private static final int SHORT_TYPE_LYRICS = Util.getIntegerCodeForString("lyr"); - private static final int SHORT_TYPE_GENRE = Util.getIntegerCodeForString("gen"); + private static final int SHORT_TYPE_NAME_1 = 0x006e616d; + private static final int SHORT_TYPE_NAME_2 = 0x0074726b; + private static final int SHORT_TYPE_COMMENT = 0x00636d74; + private static final int SHORT_TYPE_YEAR = 0x00646179; + private static final int SHORT_TYPE_ARTIST = 0x00415254; + private static final int SHORT_TYPE_ENCODER = 0x00746f6f; + private static final int SHORT_TYPE_ALBUM = 0x00616c62; + private static final int SHORT_TYPE_COMPOSER_1 = 0x00636f6d; + private static final int SHORT_TYPE_COMPOSER_2 = 0x00777274; + private static final int SHORT_TYPE_LYRICS = 0x006c7972; + private static final int SHORT_TYPE_GENRE = 0x0067656e; // Codes that have equivalent ID3 frames. - private static final int TYPE_COVER_ART = Util.getIntegerCodeForString("covr"); - private static final int TYPE_GENRE = Util.getIntegerCodeForString("gnre"); - private static final int TYPE_GROUPING = Util.getIntegerCodeForString("grp"); - private static final int TYPE_DISK_NUMBER = Util.getIntegerCodeForString("disk"); - private static final int TYPE_TRACK_NUMBER = Util.getIntegerCodeForString("trkn"); - private static final int TYPE_TEMPO = Util.getIntegerCodeForString("tmpo"); - private static final int TYPE_COMPILATION = Util.getIntegerCodeForString("cpil"); - private static final int TYPE_ALBUM_ARTIST = Util.getIntegerCodeForString("aART"); - private static final int TYPE_SORT_TRACK_NAME = Util.getIntegerCodeForString("sonm"); - private static final int TYPE_SORT_ALBUM = Util.getIntegerCodeForString("soal"); - private static final int TYPE_SORT_ARTIST = Util.getIntegerCodeForString("soar"); - private static final int TYPE_SORT_ALBUM_ARTIST = Util.getIntegerCodeForString("soaa"); - private static final int TYPE_SORT_COMPOSER = Util.getIntegerCodeForString("soco"); + private static final int TYPE_COVER_ART = 0x636f7672; + private static final int TYPE_GENRE = 0x676e7265; + private static final int TYPE_GROUPING = 0x00677270; + private static final int TYPE_DISK_NUMBER = 0x6469736b; + private static final int TYPE_TRACK_NUMBER = 0x74726b6e; + private static final int TYPE_TEMPO = 0x746d706f; + private static final int TYPE_COMPILATION = 0x6370696c; + private static final int TYPE_ALBUM_ARTIST = 0x61415254; + private static final int TYPE_SORT_TRACK_NAME = 0x736f6e6d; + private static final int TYPE_SORT_ALBUM = 0x736f616c; + private static final int TYPE_SORT_ARTIST = 0x736f6172; + private static final int TYPE_SORT_ALBUM_ARTIST = 0x736f6161; + private static final int TYPE_SORT_COMPOSER = 0x736f636f; // Types that do not have equivalent ID3 frames. - private static final int TYPE_RATING = Util.getIntegerCodeForString("rtng"); - private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString("pgap"); - private static final int TYPE_TV_SORT_SHOW = Util.getIntegerCodeForString("sosn"); - private static final int TYPE_TV_SHOW = Util.getIntegerCodeForString("tvsh"); + private static final int TYPE_RATING = 0x72746e67; + private static final int TYPE_GAPLESS_ALBUM = 0x70676170; + private static final int TYPE_TV_SORT_SHOW = 0x736f736e; + private static final int TYPE_TV_SHOW = 0x74767368; // Type for items that are intended for internal use by the player. - private static final int TYPE_INTERNAL = Util.getIntegerCodeForString("----"); + private static final int TYPE_INTERNAL = 0x2d2d2d2d; + + private static final int PICTURE_TYPE_FRONT_COVER = 3; // Standard genres. - private static final String[] STANDARD_GENRES = new String[] { - // These are the official ID3v1 genres. - "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", - "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno", - "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", - "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental", - "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass", "Soul", - "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", - "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", - "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle", - "Native American", "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", - "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", - "Hard Rock", - // These were made up by the authors of Winamp and later added to the ID3 spec. - "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival", - "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock", - "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", - "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus", - "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad", - "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", - "Euro-House", "Dance Hall", - // These were med up by the authors of Winamp but have not been added to the ID3 spec. - "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "BritPop", "Negerpunk", - "Polsk Punk", "Beat", "Christian Gangsta Rap", "Heavy Metal", "Black Metal", "Crossover", - "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", - "Jpop", "Synthpop" - }; + @VisibleForTesting + /* package */ static final String[] STANDARD_GENRES = + new String[] { + // These are the official ID3v1 genres. + "Blues", + "Classic Rock", + "Country", + "Dance", + "Disco", + "Funk", + "Grunge", + "Hip-Hop", + "Jazz", + "Metal", + "New Age", + "Oldies", + "Other", + "Pop", + "R&B", + "Rap", + "Reggae", + "Rock", + "Techno", + "Industrial", + "Alternative", + "Ska", + "Death Metal", + "Pranks", + "Soundtrack", + "Euro-Techno", + "Ambient", + "Trip-Hop", + "Vocal", + "Jazz+Funk", + "Fusion", + "Trance", + "Classical", + "Instrumental", + "Acid", + "House", + "Game", + "Sound Clip", + "Gospel", + "Noise", + "AlternRock", + "Bass", + "Soul", + "Punk", + "Space", + "Meditative", + "Instrumental Pop", + "Instrumental Rock", + "Ethnic", + "Gothic", + "Darkwave", + "Techno-Industrial", + "Electronic", + "Pop-Folk", + "Eurodance", + "Dream", + "Southern Rock", + "Comedy", + "Cult", + "Gangsta", + "Top 40", + "Christian Rap", + "Pop/Funk", + "Jungle", + "Native American", + "Cabaret", + "New Wave", + "Psychadelic", + "Rave", + "Showtunes", + "Trailer", + "Lo-Fi", + "Tribal", + "Acid Punk", + "Acid Jazz", + "Polka", + "Retro", + "Musical", + "Rock & Roll", + "Hard Rock", + // Genres made up by the authors of Winamp (v1.91) and later added to the ID3 spec. + "Folk", + "Folk-Rock", + "National Folk", + "Swing", + "Fast Fusion", + "Bebob", + "Latin", + "Revival", + "Celtic", + "Bluegrass", + "Avantgarde", + "Gothic Rock", + "Progressive Rock", + "Psychedelic Rock", + "Symphonic Rock", + "Slow Rock", + "Big Band", + "Chorus", + "Easy Listening", + "Acoustic", + "Humour", + "Speech", + "Chanson", + "Opera", + "Chamber Music", + "Sonata", + "Symphony", + "Booty Bass", + "Primus", + "Porn Groove", + "Satire", + "Slow Jam", + "Club", + "Tango", + "Samba", + "Folklore", + "Ballad", + "Power Ballad", + "Rhythmic Soul", + "Freestyle", + "Duet", + "Punk Rock", + "Drum Solo", + "A capella", + "Euro-House", + "Dance Hall", + // Genres made up by the authors of Winamp (v1.91) but have not been added to the ID3 spec. + "Goa", + "Drum & Bass", + "Club-House", + "Hardcore", + "Terror", + "Indie", + "BritPop", + "Afro-Punk", + "Polsk Punk", + "Beat", + "Christian Gangsta Rap", + "Heavy Metal", + "Black Metal", + "Crossover", + "Contemporary Christian", + "Christian Rock", + "Merengue", + "Salsa", + "Thrash Metal", + "Anime", + "Jpop", + "Synthpop", + // Genres made up by the authors of Winamp (v5.6) but have not been added to the ID3 spec. + "Abstract", + "Art Rock", + "Baroque", + "Bhangra", + "Big beat", + "Breakbeat", + "Chillout", + "Downtempo", + "Dub", + "EBM", + "Eclectic", + "Electro", + "Electroclash", + "Emo", + "Experimental", + "Garage", + "Global", + "IDM", + "Illbient", + "Industro-Goth", + "Jam Band", + "Krautrock", + "Leftfield", + "Lounge", + "Math Rock", + "New Romantic", + "Nu-Breakz", + "Post-Punk", + "Post-Rock", + "Psytrance", + "Shoegaze", + "Space Rock", + "Trop Rock", + "World Music", + "Neoclassical", + "Audiobook", + "Audio theatre", + "Neue Deutsche Welle", + "Podcast", + "Indie-Rock", + "G-Funk", + "Dubstep", + "Garage Rock", + "Psybient" + }; private static final String LANGUAGE_UNDEFINED = "und"; + private static final int TYPE_TOP_BYTE_COPYRIGHT = 0xA9; + private static final int TYPE_TOP_BYTE_REPLACEMENT = 0xFD; // Truncated value of \uFFFD. + + private static final String MDTA_KEY_ANDROID_CAPTURE_FPS = "com.android.capture.fps"; + private static final int MDTA_TYPE_INDICATOR_FLOAT = 23; + private MetadataUtil() {} /** - * Parses a single ilst element from a {@link ParsableByteArray}. The element is read starting - * from the current position of the {@link ParsableByteArray}, and the position is advanced by - * the size of the element. The position is advanced even if the element's type is unrecognized. + * Returns a {@link Format} that is the same as the input format but includes information from the + * specified sources of metadata. + */ + public static Format getFormatWithMetadata( + int trackType, + Format format, + @Nullable Metadata udtaMetadata, + @Nullable Metadata mdtaMetadata, + GaplessInfoHolder gaplessInfoHolder) { + if (trackType == C.TRACK_TYPE_AUDIO) { + if (gaplessInfoHolder.hasGaplessInfo()) { + format = + format.copyWithGaplessInfo( + gaplessInfoHolder.encoderDelay, gaplessInfoHolder.encoderPadding); + } + // We assume all udta metadata is associated with the audio track. + if (udtaMetadata != null) { + format = format.copyWithMetadata(udtaMetadata); + } + } else if (trackType == C.TRACK_TYPE_VIDEO && mdtaMetadata != null) { + // Populate only metadata keys that are known to be specific to video. + for (int i = 0; i < mdtaMetadata.length(); i++) { + Metadata.Entry entry = mdtaMetadata.get(i); + if (entry instanceof MdtaMetadataEntry) { + MdtaMetadataEntry mdtaMetadataEntry = (MdtaMetadataEntry) entry; + if (MDTA_KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key) + && mdtaMetadataEntry.typeIndicator == MDTA_TYPE_INDICATOR_FLOAT) { + try { + float fps = ByteBuffer.wrap(mdtaMetadataEntry.value).asFloatBuffer().get(); + format = format.copyWithFrameRate(fps); + format = format.copyWithMetadata(new Metadata(mdtaMetadataEntry)); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring invalid framerate"); + } + } + } + } + } + return format; + } + + /** + * Parses a single userdata ilst element from a {@link ParsableByteArray}. The element is read + * starting from the current position of the {@link ParsableByteArray}, and the position is + * advanced by the size of the element. The position is advanced even if the element's type is + * unrecognized. * * @param ilst Holds the data to be parsed. * @return The parsed element, or null if the element's type was not recognized. */ + @Nullable public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) { int position = ilst.getPosition(); int endPosition = position + ilst.readInt(); int type = ilst.readInt(); int typeTopByte = (type >> 24) & 0xFF; try { - if (typeTopByte == '\u00A9' /* Copyright char */ - || typeTopByte == '\uFFFD' /* Replacement char */) { + if (typeTopByte == TYPE_TOP_BYTE_COPYRIGHT || typeTopByte == TYPE_TOP_BYTE_REPLACEMENT) { int shortType = type & 0x00FFFFFF; if (shortType == SHORT_TYPE_COMMENT) { return parseCommentAttribute(type, ilst); @@ -181,19 +409,49 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; } } - private static TextInformationFrame parseTextAttribute(int type, String id, - ParsableByteArray data) { + /** + * Parses an 'mdta' metadata entry starting at the current position in an ilst box. + * + * @param ilst The ilst box. + * @param endPosition The end position of the entry in the ilst box. + * @param key The mdta metadata entry key for the entry. + * @return The parsed element, or null if the entry wasn't recognized. + */ + @Nullable + public static MdtaMetadataEntry parseMdtaMetadataEntryFromIlst( + ParsableByteArray ilst, int endPosition, String key) { + int atomPosition; + while ((atomPosition = ilst.getPosition()) < endPosition) { + int atomSize = ilst.readInt(); + int atomType = ilst.readInt(); + if (atomType == Atom.TYPE_data) { + int typeIndicator = ilst.readInt(); + int localeIndicator = ilst.readInt(); + int dataSize = atomSize - 16; + byte[] value = new byte[dataSize]; + ilst.readBytes(value, 0, dataSize); + return new MdtaMetadataEntry(key, value, localeIndicator, typeIndicator); + } + ilst.setPosition(atomPosition + atomSize); + } + return null; + } + + @Nullable + private static TextInformationFrame parseTextAttribute( + int type, String id, ParsableByteArray data) { int atomSize = data.readInt(); int atomType = data.readInt(); if (atomType == Atom.TYPE_data) { data.skipBytes(8); // version (1), flags (3), empty (4) String value = data.readNullTerminatedString(atomSize - 16); - return new TextInformationFrame(id, null, value); + return new TextInformationFrame(id, /* description= */ null, value); } Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type)); return null; } + @Nullable private static CommentFrame parseCommentAttribute(int type, ParsableByteArray data) { int atomSize = data.readInt(); int atomType = data.readInt(); @@ -206,22 +464,29 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; return null; } - private static Id3Frame parseUint8Attribute(int type, String id, ParsableByteArray data, - boolean isTextInformationFrame, boolean isBoolean) { + @Nullable + private static Id3Frame parseUint8Attribute( + int type, + String id, + ParsableByteArray data, + boolean isTextInformationFrame, + boolean isBoolean) { int value = parseUint8AttributeValue(data); if (isBoolean) { value = Math.min(1, value); } if (value >= 0) { - return isTextInformationFrame ? new TextInformationFrame(id, null, Integer.toString(value)) + return isTextInformationFrame + ? new TextInformationFrame(id, /* description= */ null, Integer.toString(value)) : new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value)); } Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type)); return null; } - private static TextInformationFrame parseIndexAndCountAttribute(int type, String attributeName, - ParsableByteArray data) { + @Nullable + private static TextInformationFrame parseIndexAndCountAttribute( + int type, String attributeName, ParsableByteArray data) { int atomSize = data.readInt(); int atomType = data.readInt(); if (atomType == Atom.TYPE_data && atomSize >= 22) { @@ -233,24 +498,26 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; if (count > 0) { value += "/" + count; } - return new TextInformationFrame(attributeName, null, value); + return new TextInformationFrame(attributeName, /* description= */ null, value); } } Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type)); return null; } + @Nullable private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) { int genreCode = parseUint8AttributeValue(data); String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length) ? STANDARD_GENRES[genreCode - 1] : null; if (genreString != null) { - return new TextInformationFrame("TCON", null, genreString); + return new TextInformationFrame("TCON", /* description= */ null, genreString); } Log.w(TAG, "Failed to parse standard genre code"); return null; } + @Nullable private static ApicFrame parseCoverArt(ParsableByteArray data) { int atomSize = data.readInt(); int atomType = data.readInt(); @@ -265,12 +532,17 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; data.skipBytes(4); // empty (4) byte[] pictureData = new byte[atomSize - 16]; data.readBytes(pictureData, 0, pictureData.length); - return new ApicFrame(mimeType, null, 3 /* Cover (front) */, pictureData); + return new ApicFrame( + mimeType, + /* description= */ null, + /* pictureType= */ PICTURE_TYPE_FRONT_COVER, + pictureData); } Log.w(TAG, "Failed to parse cover art attribute"); return null; } + @Nullable private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) { String domain = null; String name = null; @@ -293,14 +565,13 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; data.skipBytes(atomSize - 12); } } - if (!"com.apple.iTunes".equals(domain) || !"iTunSMPB".equals(name) || dataAtomPosition == -1) { - // We're only interested in iTunSMPB. + if (domain == null || name == null || dataAtomPosition == -1) { return null; } data.setPosition(dataAtomPosition); data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4) String value = data.readNullTerminatedString(dataAtomSize - 16); - return new CommentFrame(LANGUAGE_UNDEFINED, name, value); + return new InternalFrame(domain, name, value); } private static int parseUint8AttributeValue(ParsableByteArray data) { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 96d52df61e39..254cad1eb163 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -15,10 +15,11 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; -import android.support.annotation.IntDef; +import androidx.annotation.IntDef; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -26,49 +27,57 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractors import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.GaplessInfoHolder; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; -import java.util.Stack; /** - * Extracts data from an unfragmented MP4 file. + * Extracts data from the MP4 container format. */ public final class Mp4Extractor implements Extractor, SeekMap { - /** - * Factory for {@link Mp4Extractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new Mp4Extractor()}; - } - - }; + /** Factory for {@link Mp4Extractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp4Extractor()}; /** - * Parser states. + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_WORKAROUND_IGNORE_EDIT_LISTS}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) + public @interface Flags {} + /** + * Flag to ignore any edit lists in the stream. + */ + public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1; + + /** Parser states. */ + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({STATE_READING_ATOM_HEADER, STATE_READING_ATOM_PAYLOAD, STATE_READING_SAMPLE}) private @interface State {} + private static final int STATE_READING_ATOM_HEADER = 0; private static final int STATE_READING_ATOM_PAYLOAD = 1; private static final int STATE_READING_SAMPLE = 2; - // Brand stored in the ftyp atom for QuickTime media. - private static final int BRAND_QUICKTIME = Util.getIntegerCodeForString("qt "); + /** Brand stored in the ftyp atom for QuickTime media. */ + private static final int BRAND_QUICKTIME = 0x71742020; /** * When seeking within the source, if the offset is greater than or equal to this value (or the @@ -76,12 +85,21 @@ public final class Mp4Extractor implements Extractor, SeekMap { */ private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024; + /** + * For poorly interleaved streams, the maximum byte difference one track is allowed to be read + * ahead before the source will be reloaded at a new position to read another track. + */ + private static final long MAXIMUM_READ_AHEAD_BYTES_STREAM = 10 * 1024 * 1024; + + private final @Flags int flags; + // Temporary arrays. private final ParsableByteArray nalStartCode; private final ParsableByteArray nalLength; + private final ParsableByteArray scratch; private final ParsableByteArray atomHeader; - private final Stack containerAtoms; + private final ArrayDeque containerAtoms; @State private int parserState; private int atomType; @@ -89,20 +107,40 @@ public final class Mp4Extractor implements Extractor, SeekMap { private int atomHeaderBytesRead; private ParsableByteArray atomData; + private int sampleTrackIndex; + private int sampleBytesRead; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; // Extractor outputs. private ExtractorOutput extractorOutput; private Mp4Track[] tracks; + private long[][] accumulatedSampleSizes; + private int firstVideoTrackIndex; private long durationUs; private boolean isQuickTime; + /** + * Creates a new extractor for unfragmented MP4 streams. + */ public Mp4Extractor() { + this(0); + } + + /** + * Creates a new extractor for unfragmented MP4 streams, using the specified flags to control the + * extractor's behavior. + * + * @param flags Flags that control the extractor's behavior. + */ + public Mp4Extractor(@Flags int flags) { + this.flags = flags; atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); - containerAtoms = new Stack<>(); + containerAtoms = new ArrayDeque<>(); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalLength = new ParsableByteArray(4); + scratch = new ParsableByteArray(); + sampleTrackIndex = C.INDEX_UNSET; } @Override @@ -119,6 +157,8 @@ public final class Mp4Extractor implements Extractor, SeekMap { public void seek(long position, long timeUs) { containerAtoms.clear(); atomHeaderBytesRead = 0; + sampleTrackIndex = C.INDEX_UNSET; + sampleBytesRead = 0; sampleBytesWritten = 0; sampleCurrentNalBytesRemaining = 0; if (position == 0) { @@ -169,21 +209,56 @@ public final class Mp4Extractor implements Extractor, SeekMap { } @Override - public long getPosition(long timeUs) { - long earliestSamplePosition = Long.MAX_VALUE; - for (Mp4Track track : tracks) { - TrackSampleTable sampleTable = track.sampleTable; - int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs); + public SeekPoints getSeekPoints(long timeUs) { + if (tracks.length == 0) { + return new SeekPoints(SeekPoint.START); + } + + long firstTimeUs; + long firstOffset; + long secondTimeUs = C.TIME_UNSET; + long secondOffset = C.POSITION_UNSET; + + // If we have a video track, use it to establish one or two seek points. + if (firstVideoTrackIndex != C.INDEX_UNSET) { + TrackSampleTable sampleTable = tracks[firstVideoTrackIndex].sampleTable; + int sampleIndex = getSynchronizationSampleIndex(sampleTable, timeUs); if (sampleIndex == C.INDEX_UNSET) { - // Handle the case where the requested time is before the first synchronization sample. - sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + return new SeekPoints(SeekPoint.START); } - long offset = sampleTable.offsets[sampleIndex]; - if (offset < earliestSamplePosition) { - earliestSamplePosition = offset; + long sampleTimeUs = sampleTable.timestampsUs[sampleIndex]; + firstTimeUs = sampleTimeUs; + firstOffset = sampleTable.offsets[sampleIndex]; + if (sampleTimeUs < timeUs && sampleIndex < sampleTable.sampleCount - 1) { + int secondSampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + if (secondSampleIndex != C.INDEX_UNSET && secondSampleIndex != sampleIndex) { + secondTimeUs = sampleTable.timestampsUs[secondSampleIndex]; + secondOffset = sampleTable.offsets[secondSampleIndex]; + } + } + } else { + firstTimeUs = timeUs; + firstOffset = Long.MAX_VALUE; + } + + // Take into account other tracks. + for (int i = 0; i < tracks.length; i++) { + if (i != firstVideoTrackIndex) { + TrackSampleTable sampleTable = tracks[i].sampleTable; + firstOffset = maybeAdjustSeekOffset(sampleTable, firstTimeUs, firstOffset); + if (secondTimeUs != C.TIME_UNSET) { + secondOffset = maybeAdjustSeekOffset(sampleTable, secondTimeUs, secondOffset); + } } } - return earliestSamplePosition; + + SeekPoint firstSeekPoint = new SeekPoint(firstTimeUs, firstOffset); + if (secondTimeUs == C.TIME_UNSET) { + return new SeekPoints(firstSeekPoint); + } else { + SeekPoint secondSeekPoint = new SeekPoint(secondTimeUs, secondOffset); + return new SeekPoints(firstSeekPoint, secondSeekPoint); + } } // Private methods. @@ -205,17 +280,34 @@ public final class Mp4Extractor implements Extractor, SeekMap { atomType = atomHeader.readInt(); } - if (atomSize == Atom.LONG_SIZE_PREFIX) { - // Read the extended atom size. + if (atomSize == Atom.DEFINES_LARGE_SIZE) { + // Read the large size. int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE; input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining); atomHeaderBytesRead += headerBytesRemaining; atomSize = atomHeader.readUnsignedLongToLong(); + } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) { + // The atom extends to the end of the file. Note that if the atom is within a container we can + // work out its size even if the input length is unknown. + long endPosition = input.getLength(); + if (endPosition == C.LENGTH_UNSET && !containerAtoms.isEmpty()) { + endPosition = containerAtoms.peek().endPosition; + } + if (endPosition != C.LENGTH_UNSET) { + atomSize = endPosition - input.getPosition() + atomHeaderBytesRead; + } + } + + if (atomSize < atomHeaderBytesRead) { + throw new ParserException("Atom size less than header length (unsupported)."); } if (shouldParseContainerAtom(atomType)) { long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead; - containerAtoms.add(new ContainerAtom(atomType, endPosition)); + if (atomSize != atomHeaderBytesRead && atomType == Atom.TYPE_meta) { + maybeSkipRemainingMetaAtomHeaderBytes(input); + } + containerAtoms.push(new ContainerAtom(atomType, endPosition)); if (atomSize == atomHeaderBytesRead) { processAtomEnded(endPosition); } else { @@ -285,93 +377,104 @@ public final class Mp4Extractor implements Extractor, SeekMap { } } - /** - * Process an ftyp atom to determine whether the media is QuickTime. - * - * @param atomData The ftyp atom data. - * @return Whether the media is QuickTime. - */ - private static boolean processFtypAtom(ParsableByteArray atomData) { - atomData.setPosition(Atom.HEADER_SIZE); - int majorBrand = atomData.readInt(); - if (majorBrand == BRAND_QUICKTIME) { - return true; - } - atomData.skipBytes(4); // minor_version - while (atomData.bytesLeft() > 0) { - if (atomData.readInt() == BRAND_QUICKTIME) { - return true; - } - } - return false; - } - /** * Updates the stored track metadata to reflect the contents of the specified moov atom. */ private void processMoovAtom(ContainerAtom moov) throws ParserException { + int firstVideoTrackIndex = C.INDEX_UNSET; long durationUs = C.TIME_UNSET; List tracks = new ArrayList<>(); - long earliestSampleOffset = Long.MAX_VALUE; - Metadata metadata = null; + // Process metadata. + Metadata udtaMetadata = null; GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); if (udta != null) { - metadata = AtomParsers.parseUdta(udta, isQuickTime); - if (metadata != null) { - gaplessInfoHolder.setFromMetadata(metadata); + udtaMetadata = AtomParsers.parseUdta(udta, isQuickTime); + if (udtaMetadata != null) { + gaplessInfoHolder.setFromMetadata(udtaMetadata); } } + Metadata mdtaMetadata = null; + Atom.ContainerAtom meta = moov.getContainerAtomOfType(Atom.TYPE_meta); + if (meta != null) { + mdtaMetadata = AtomParsers.parseMdtaFromMeta(meta); + } + boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0; + ArrayList trackSampleTables = + getTrackSampleTables(moov, gaplessInfoHolder, ignoreEditLists); + + int trackCount = trackSampleTables.size(); + for (int i = 0; i < trackCount; i++) { + TrackSampleTable trackSampleTable = trackSampleTables.get(i); + Track track = trackSampleTable.track; + long trackDurationUs = + track.durationUs != C.TIME_UNSET ? track.durationUs : trackSampleTable.durationUs; + durationUs = Math.max(durationUs, trackDurationUs); + Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, + extractorOutput.track(i, track.type)); + + // Each sample has up to three bytes of overhead for the start code that replaces its length. + // Allow ten source samples per output sample, like the platform extractor. + int maxInputSize = trackSampleTable.maximumSize + 3 * 10; + Format format = track.format.copyWithMaxInputSize(maxInputSize); + if (track.type == C.TRACK_TYPE_VIDEO + && trackDurationUs > 0 + && trackSampleTable.sampleCount > 1) { + float frameRate = trackSampleTable.sampleCount / (trackDurationUs / 1000000f); + format = format.copyWithFrameRate(frameRate); + } + format = + MetadataUtil.getFormatWithMetadata( + track.type, format, udtaMetadata, mdtaMetadata, gaplessInfoHolder); + mp4Track.trackOutput.format(format); + + if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) { + firstVideoTrackIndex = tracks.size(); + } + tracks.add(mp4Track); + } + this.firstVideoTrackIndex = firstVideoTrackIndex; + this.durationUs = durationUs; + this.tracks = tracks.toArray(new Mp4Track[0]); + accumulatedSampleSizes = calculateAccumulatedSampleSizes(this.tracks); + + extractorOutput.endTracks(); + extractorOutput.seekMap(this); + } + + private ArrayList getTrackSampleTables( + ContainerAtom moov, GaplessInfoHolder gaplessInfoHolder, boolean ignoreEditLists) + throws ParserException { + ArrayList trackSampleTables = new ArrayList<>(); for (int i = 0; i < moov.containerChildren.size(); i++) { Atom.ContainerAtom atom = moov.containerChildren.get(i); if (atom.type != Atom.TYPE_trak) { continue; } - - Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), - C.TIME_UNSET, null, isQuickTime); + Track track = + AtomParsers.parseTrak( + atom, + moov.getLeafAtomOfType(Atom.TYPE_mvhd), + /* duration= */ C.TIME_UNSET, + /* drmInitData= */ null, + ignoreEditLists, + isQuickTime); if (track == null) { continue; } - - Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia) - .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl); + Atom.ContainerAtom stblAtom = + atom.getContainerAtomOfType(Atom.TYPE_mdia) + .getContainerAtomOfType(Atom.TYPE_minf) + .getContainerAtomOfType(Atom.TYPE_stbl); TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); if (trackSampleTable.sampleCount == 0) { continue; } - - Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, - extractorOutput.track(i, track.type)); - // Each sample has up to three bytes of overhead for the start code that replaces its length. - // Allow ten source samples per output sample, like the platform extractor. - int maxInputSize = trackSampleTable.maximumSize + 3 * 10; - Format format = track.format.copyWithMaxInputSize(maxInputSize); - if (track.type == C.TRACK_TYPE_AUDIO) { - if (gaplessInfoHolder.hasGaplessInfo()) { - format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding); - } - if (metadata != null) { - format = format.copyWithMetadata(metadata); - } - } - mp4Track.trackOutput.format(format); - - durationUs = Math.max(durationUs, track.durationUs); - tracks.add(mp4Track); - - long firstSampleOffset = trackSampleTable.offsets[0]; - if (firstSampleOffset < earliestSampleOffset) { - earliestSampleOffset = firstSampleOffset; - } + trackSampleTables.add(trackSampleTable); } - this.durationUs = durationUs; - this.tracks = tracks.toArray(new Mp4Track[tracks.size()]); - extractorOutput.endTracks(); - extractorOutput.seekMap(this); + return trackSampleTables; } /** @@ -392,26 +495,29 @@ public final class Mp4Extractor implements Extractor, SeekMap { */ private int readSample(ExtractorInput input, PositionHolder positionHolder) throws IOException, InterruptedException { - int trackIndex = getTrackIndexOfEarliestCurrentSample(); - if (trackIndex == C.INDEX_UNSET) { - return RESULT_END_OF_INPUT; + long inputPosition = input.getPosition(); + if (sampleTrackIndex == C.INDEX_UNSET) { + sampleTrackIndex = getTrackIndexOfNextReadSample(inputPosition); + if (sampleTrackIndex == C.INDEX_UNSET) { + return RESULT_END_OF_INPUT; + } } - Mp4Track track = tracks[trackIndex]; + Mp4Track track = tracks[sampleTrackIndex]; TrackOutput trackOutput = track.trackOutput; int sampleIndex = track.sampleIndex; long position = track.sampleTable.offsets[sampleIndex]; int sampleSize = track.sampleTable.sizes[sampleIndex]; - if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { - // The sample information is contained in a cdat atom. The header must be discarded for - // committing. - position += Atom.HEADER_SIZE; - sampleSize -= Atom.HEADER_SIZE; - } - long skipAmount = position - input.getPosition() + sampleBytesWritten; + long skipAmount = position - inputPosition + sampleBytesRead; if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) { positionHolder.position = position; return RESULT_SEEK; } + if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { + // The sample information is contained in a cdat atom. The header must be discarded for + // committing. + skipAmount += Atom.HEADER_SIZE; + sampleSize -= Atom.HEADER_SIZE; + } input.skipFully((int) skipAmount); if (track.track.nalUnitLengthFieldLength != 0) { // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case @@ -428,9 +534,14 @@ public final class Mp4Extractor implements Extractor, SeekMap { while (sampleBytesWritten < sampleSize) { if (sampleCurrentNalBytesRemaining == 0) { // Read the NAL length so that we know where we find the next one. - input.readFully(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + sampleBytesRead += nalUnitLengthFieldLength; nalLength.setPosition(0); - sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt(); + int nalLengthInt = nalLength.readInt(); + if (nalLengthInt < 0) { + throw new ParserException("Invalid NAL length"); + } + sampleCurrentNalBytesRemaining = nalLengthInt; // Write a start code for the current NAL unit. nalStartCode.setPosition(0); trackOutput.sampleData(nalStartCode, 4); @@ -439,13 +550,23 @@ public final class Mp4Extractor implements Extractor, SeekMap { } else { // Write the payload of the NAL unit. int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false); + sampleBytesRead += writtenBytes; sampleBytesWritten += writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes; } } } else { + if (MimeTypes.AUDIO_AC4.equals(track.track.format.sampleMimeType)) { + if (sampleBytesWritten == 0) { + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + trackOutput.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); + sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; + } + sampleSize += Ac4Util.SAMPLE_HEADER_SIZE; + } while (sampleBytesWritten < sampleSize) { int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false); + sampleBytesRead += writtenBytes; sampleBytesWritten += writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes; } @@ -453,33 +574,62 @@ public final class Mp4Extractor implements Extractor, SeekMap { trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex], track.sampleTable.flags[sampleIndex], sampleSize, 0, null); track.sampleIndex++; + sampleTrackIndex = C.INDEX_UNSET; + sampleBytesRead = 0; sampleBytesWritten = 0; sampleCurrentNalBytesRemaining = 0; return RESULT_CONTINUE; } /** - * Returns the index of the track that contains the earliest current sample, or - * {@link C#INDEX_UNSET} if no samples remain. + * Returns the index of the track that contains the next sample to be read, or {@link + * C#INDEX_UNSET} if no samples remain. + * + *

The preferred choice is the sample with the smallest offset not requiring a source reload, + * or if not available the sample with the smallest overall offset to avoid subsequent source + * reloads. + * + *

To deal with poor sample interleaving, we also check whether the required memory to catch up + * with the next logical sample (based on sample time) exceeds {@link + * #MAXIMUM_READ_AHEAD_BYTES_STREAM}. If this is the case, we continue with this sample even + * though it may require a source reload. */ - private int getTrackIndexOfEarliestCurrentSample() { - int earliestSampleTrackIndex = C.INDEX_UNSET; - long earliestSampleOffset = Long.MAX_VALUE; + private int getTrackIndexOfNextReadSample(long inputPosition) { + long preferredSkipAmount = Long.MAX_VALUE; + boolean preferredRequiresReload = true; + int preferredTrackIndex = C.INDEX_UNSET; + long preferredAccumulatedBytes = Long.MAX_VALUE; + long minAccumulatedBytes = Long.MAX_VALUE; + boolean minAccumulatedBytesRequiresReload = true; + int minAccumulatedBytesTrackIndex = C.INDEX_UNSET; for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) { Mp4Track track = tracks[trackIndex]; int sampleIndex = track.sampleIndex; if (sampleIndex == track.sampleTable.sampleCount) { continue; } - - long trackSampleOffset = track.sampleTable.offsets[sampleIndex]; - if (trackSampleOffset < earliestSampleOffset) { - earliestSampleOffset = trackSampleOffset; - earliestSampleTrackIndex = trackIndex; + long sampleOffset = track.sampleTable.offsets[sampleIndex]; + long sampleAccumulatedBytes = accumulatedSampleSizes[trackIndex][sampleIndex]; + long skipAmount = sampleOffset - inputPosition; + boolean requiresReload = skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE; + if ((!requiresReload && preferredRequiresReload) + || (requiresReload == preferredRequiresReload && skipAmount < preferredSkipAmount)) { + preferredRequiresReload = requiresReload; + preferredSkipAmount = skipAmount; + preferredTrackIndex = trackIndex; + preferredAccumulatedBytes = sampleAccumulatedBytes; + } + if (sampleAccumulatedBytes < minAccumulatedBytes) { + minAccumulatedBytes = sampleAccumulatedBytes; + minAccumulatedBytesRequiresReload = requiresReload; + minAccumulatedBytesTrackIndex = trackIndex; } } - - return earliestSampleTrackIndex; + return minAccumulatedBytes == Long.MAX_VALUE + || !minAccumulatedBytesRequiresReload + || preferredAccumulatedBytes < minAccumulatedBytes + MAXIMUM_READ_AHEAD_BYTES_STREAM + ? preferredTrackIndex + : minAccumulatedBytesTrackIndex; } /** @@ -498,23 +648,161 @@ public final class Mp4Extractor implements Extractor, SeekMap { } /** - * Returns whether the extractor should decode a leaf atom with type {@code atom}. + * Possibly skips the version and flags fields (1+3 byte) of a full meta atom of the {@code + * input}. + * + *

Atoms of type {@link Atom#TYPE_meta} are defined to be full atoms which have four additional + * bytes for a version and a flags field (see 4.2 'Object Structure' in ISO/IEC 14496-12:2005). + * QuickTime do not have such a full box structure. Since some of these files are encoded wrongly, + * we can't rely on the file type though. Instead we must check the 8 bytes after the common + * header bytes ourselves. */ - private static boolean shouldParseLeafAtom(int atom) { - return atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd || atom == Atom.TYPE_hdlr - || atom == Atom.TYPE_stsd || atom == Atom.TYPE_stts || atom == Atom.TYPE_stss - || atom == Atom.TYPE_ctts || atom == Atom.TYPE_elst || atom == Atom.TYPE_stsc - || atom == Atom.TYPE_stsz || atom == Atom.TYPE_stz2 || atom == Atom.TYPE_stco - || atom == Atom.TYPE_co64 || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_ftyp - || atom == Atom.TYPE_udta; + private void maybeSkipRemainingMetaAtomHeaderBytes(ExtractorInput input) + throws IOException, InterruptedException { + scratch.reset(8); + // Peek the next 8 bytes which can be either + // (iso) [1 byte version + 3 bytes flags][4 byte size of next atom] + // (qt) [4 byte size of next atom ][4 byte hdlr atom type ] + // In case of (iso) we need to skip the next 4 bytes. + input.peekFully(scratch.data, 0, 8); + scratch.skipBytes(4); + if (scratch.readInt() == Atom.TYPE_hdlr) { + input.resetPeekPosition(); + } else { + input.skipFully(4); + } } /** - * Returns whether the extractor should decode a container atom with type {@code atom}. + * For each sample of each track, calculates accumulated size of all samples which need to be read + * before this sample can be used. */ + private static long[][] calculateAccumulatedSampleSizes(Mp4Track[] tracks) { + long[][] accumulatedSampleSizes = new long[tracks.length][]; + int[] nextSampleIndex = new int[tracks.length]; + long[] nextSampleTimesUs = new long[tracks.length]; + boolean[] tracksFinished = new boolean[tracks.length]; + for (int i = 0; i < tracks.length; i++) { + accumulatedSampleSizes[i] = new long[tracks[i].sampleTable.sampleCount]; + nextSampleTimesUs[i] = tracks[i].sampleTable.timestampsUs[0]; + } + long accumulatedSampleSize = 0; + int finishedTracks = 0; + while (finishedTracks < tracks.length) { + long minTimeUs = Long.MAX_VALUE; + int minTimeTrackIndex = -1; + for (int i = 0; i < tracks.length; i++) { + if (!tracksFinished[i] && nextSampleTimesUs[i] <= minTimeUs) { + minTimeTrackIndex = i; + minTimeUs = nextSampleTimesUs[i]; + } + } + int trackSampleIndex = nextSampleIndex[minTimeTrackIndex]; + accumulatedSampleSizes[minTimeTrackIndex][trackSampleIndex] = accumulatedSampleSize; + accumulatedSampleSize += tracks[minTimeTrackIndex].sampleTable.sizes[trackSampleIndex]; + nextSampleIndex[minTimeTrackIndex] = ++trackSampleIndex; + if (trackSampleIndex < accumulatedSampleSizes[minTimeTrackIndex].length) { + nextSampleTimesUs[minTimeTrackIndex] = + tracks[minTimeTrackIndex].sampleTable.timestampsUs[trackSampleIndex]; + } else { + tracksFinished[minTimeTrackIndex] = true; + finishedTracks++; + } + } + return accumulatedSampleSizes; + } + + /** + * Adjusts a seek point offset to take into account the track with the given {@code sampleTable}, + * for a given {@code seekTimeUs}. + * + * @param sampleTable The sample table to use. + * @param seekTimeUs The seek time in microseconds. + * @param offset The current offset. + * @return The adjusted offset. + */ + private static long maybeAdjustSeekOffset( + TrackSampleTable sampleTable, long seekTimeUs, long offset) { + int sampleIndex = getSynchronizationSampleIndex(sampleTable, seekTimeUs); + if (sampleIndex == C.INDEX_UNSET) { + return offset; + } + long sampleOffset = sampleTable.offsets[sampleIndex]; + return Math.min(sampleOffset, offset); + } + + /** + * Returns the index of the synchronization sample before or at {@code timeUs}, or the index of + * the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET} if + * there are no synchronization samples in the table. + * + * @param sampleTable The sample table in which to locate a synchronization sample. + * @param timeUs A time in microseconds. + * @return The index of the synchronization sample before or at {@code timeUs}, or the index of + * the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET} + * if there are no synchronization samples in the table. + */ + private static int getSynchronizationSampleIndex(TrackSampleTable sampleTable, long timeUs) { + int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs); + if (sampleIndex == C.INDEX_UNSET) { + // Handle the case where the requested time is before the first synchronization sample. + sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + } + return sampleIndex; + } + + /** + * Process an ftyp atom to determine whether the media is QuickTime. + * + * @param atomData The ftyp atom data. + * @return Whether the media is QuickTime. + */ + private static boolean processFtypAtom(ParsableByteArray atomData) { + atomData.setPosition(Atom.HEADER_SIZE); + int majorBrand = atomData.readInt(); + if (majorBrand == BRAND_QUICKTIME) { + return true; + } + atomData.skipBytes(4); // minor_version + while (atomData.bytesLeft() > 0) { + if (atomData.readInt() == BRAND_QUICKTIME) { + return true; + } + } + return false; + } + + /** Returns whether the extractor should decode a leaf atom with type {@code atom}. */ + private static boolean shouldParseLeafAtom(int atom) { + return atom == Atom.TYPE_mdhd + || atom == Atom.TYPE_mvhd + || atom == Atom.TYPE_hdlr + || atom == Atom.TYPE_stsd + || atom == Atom.TYPE_stts + || atom == Atom.TYPE_stss + || atom == Atom.TYPE_ctts + || atom == Atom.TYPE_elst + || atom == Atom.TYPE_stsc + || atom == Atom.TYPE_stsz + || atom == Atom.TYPE_stz2 + || atom == Atom.TYPE_stco + || atom == Atom.TYPE_co64 + || atom == Atom.TYPE_tkhd + || atom == Atom.TYPE_ftyp + || atom == Atom.TYPE_udta + || atom == Atom.TYPE_keys + || atom == Atom.TYPE_ilst; + } + + /** Returns whether the extractor should decode a container atom with type {@code atom}. */ private static boolean shouldParseContainerAtom(int atom) { - return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia - || atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_edts; + return atom == Atom.TYPE_moov + || atom == Atom.TYPE_trak + || atom == Atom.TYPE_mdia + || atom == Atom.TYPE_minf + || atom == Atom.TYPE_stbl + || atom == Atom.TYPE_edts + || atom == Atom.TYPE_meta; } private static final class Mp4Track { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java index 6cf9c7a3a216..ddb13aeb9c73 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java @@ -15,8 +15,8 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; -import android.util.Log; -import android.util.Pair; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import java.nio.ByteBuffer; import java.util.UUID; @@ -31,46 +31,101 @@ public final class PsshAtomUtil { private PsshAtomUtil() {} /** - * Builds a PSSH atom for a given {@link UUID} containing the given scheme specific data. + * Builds a version 0 PSSH atom for a given system id, containing the given data. * - * @param uuid The UUID of the scheme. + * @param systemId The system id of the scheme. * @param data The scheme specific data. * @return The PSSH atom. */ - public static byte[] buildPsshAtom(UUID uuid, byte[] data) { - int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */ + data.length; + public static byte[] buildPsshAtom(UUID systemId, @Nullable byte[] data) { + return buildPsshAtom(systemId, null, data); + } + + /** + * Builds a PSSH atom for the given system id, containing the given key ids and data. + * + * @param systemId The system id of the scheme. + * @param keyIds The key ids for a version 1 PSSH atom, or null for a version 0 PSSH atom. + * @param data The scheme specific data. + * @return The PSSH atom. + */ + // dereference of possibly-null reference keyId + @SuppressWarnings({"ParameterNotNullable", "nullness:dereference.of.nullable"}) + public static byte[] buildPsshAtom( + UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) { + int dataLength = data != null ? data.length : 0; + int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* SystemId */ + 4 /* DataSize */ + dataLength; + if (keyIds != null) { + psshBoxLength += 4 /* KID_count */ + (keyIds.length * 16) /* KIDs */; + } ByteBuffer psshBox = ByteBuffer.allocate(psshBoxLength); psshBox.putInt(psshBoxLength); psshBox.putInt(Atom.TYPE_pssh); - psshBox.putInt(0 /* version=0, flags=0 */); - psshBox.putLong(uuid.getMostSignificantBits()); - psshBox.putLong(uuid.getLeastSignificantBits()); - psshBox.putInt(data.length); - psshBox.put(data); + psshBox.putInt(keyIds != null ? 0x01000000 : 0 /* version=(buildV1Atom ? 1 : 0), flags=0 */); + psshBox.putLong(systemId.getMostSignificantBits()); + psshBox.putLong(systemId.getLeastSignificantBits()); + if (keyIds != null) { + psshBox.putInt(keyIds.length); + for (UUID keyId : keyIds) { + psshBox.putLong(keyId.getMostSignificantBits()); + psshBox.putLong(keyId.getLeastSignificantBits()); + } + } + if (data != null && data.length != 0) { + psshBox.putInt(data.length); + psshBox.put(data); + } // Else the last 4 bytes are a 0 DataSize. return psshBox.array(); } + /** + * Returns whether the data is a valid PSSH atom. + * + * @param data The data to parse. + * @return Whether the data is a valid PSSH atom. + */ + public static boolean isPsshAtom(byte[] data) { + return parsePsshAtom(data) != null; + } + /** * Parses the UUID from a PSSH atom. Version 0 and 1 PSSH atoms are supported. - *

- * The UUID is only parsed if the data is a valid PSSH atom. + * + *

The UUID is only parsed if the data is a valid PSSH atom. * * @param atom The atom to parse. - * @return The parsed UUID. Null if the input is not a valid PSSH atom, or if the PSSH atom has - * an unsupported version. + * @return The parsed UUID. Null if the input is not a valid PSSH atom, or if the PSSH atom has an + * unsupported version. */ - public static UUID parseUuid(byte[] atom) { - Pair parsedAtom = parsePsshAtom(atom); + public static @Nullable UUID parseUuid(byte[] atom) { + PsshAtom parsedAtom = parsePsshAtom(atom); if (parsedAtom == null) { return null; } - return parsedAtom.first; + return parsedAtom.uuid; + } + + /** + * Parses the version from a PSSH atom. Version 0 and 1 PSSH atoms are supported. + *

+ * The version is only parsed if the data is a valid PSSH atom. + * + * @param atom The atom to parse. + * @return The parsed version. -1 if the input is not a valid PSSH atom, or if the PSSH atom has + * an unsupported version. + */ + public static int parseVersion(byte[] atom) { + PsshAtom parsedAtom = parsePsshAtom(atom); + if (parsedAtom == null) { + return -1; + } + return parsedAtom.version; } /** * Parses the scheme specific data from a PSSH atom. Version 0 and 1 PSSH atoms are supported. - *

- * The scheme specific data is only parsed if the data is a valid PSSH atom matching the given + * + *

The scheme specific data is only parsed if the data is a valid PSSH atom matching the given * UUID, or if the data is a valid PSSH atom of any type in the case that the passed UUID is null. * * @param atom The atom to parse. @@ -78,27 +133,27 @@ public final class PsshAtomUtil { * @return The parsed scheme specific data. Null if the input is not a valid PSSH atom, or if the * PSSH atom has an unsupported version, or if the PSSH atom does not match the passed UUID. */ - public static byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) { - Pair parsedAtom = parsePsshAtom(atom); + public static @Nullable byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) { + PsshAtom parsedAtom = parsePsshAtom(atom); if (parsedAtom == null) { return null; } - if (uuid != null && !uuid.equals(parsedAtom.first)) { - Log.w(TAG, "UUID mismatch. Expected: " + uuid + ", got: " + parsedAtom.first + "."); + if (uuid != null && !uuid.equals(parsedAtom.uuid)) { + Log.w(TAG, "UUID mismatch. Expected: " + uuid + ", got: " + parsedAtom.uuid + "."); return null; } - return parsedAtom.second; + return parsedAtom.schemeData; } /** - * Parses the UUID and scheme specific data from a PSSH atom. Version 0 and 1 PSSH atoms are - * supported. + * Parses a PSSH atom. Version 0 and 1 PSSH atoms are supported. * * @param atom The atom to parse. - * @return A pair consisting of the parsed UUID and scheme specific data. Null if the input is - * not a valid PSSH atom, or if the PSSH atom has an unsupported version. + * @return The parsed PSSH atom. Null if the input is not a valid PSSH atom, or if the PSSH atom + * has an unsupported version. */ - private static Pair parsePsshAtom(byte[] atom) { + // TODO: Support parsing of the key ids for version 1 PSSH atoms. + private static @Nullable PsshAtom parsePsshAtom(byte[] atom) { ParsableByteArray atomData = new ParsableByteArray(atom); if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) { // Data too short. @@ -132,7 +187,22 @@ public final class PsshAtomUtil { } byte[] data = new byte[dataSize]; atomData.readBytes(data, 0, dataSize); - return Pair.create(uuid, data); + return new PsshAtom(uuid, atomVersion, data); + } + + // TODO: Consider exposing this and making parsePsshAtom public. + private static class PsshAtom { + + private final UUID uuid; + private final int version; + private final byte[] schemeData; + + public PsshAtom(UUID uuid, int version, byte[] schemeData) { + this.uuid = uuid; + this.version = version; + this.schemeData = schemeData; + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java index 6514baca80ef..d58c2f06ebd6 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java @@ -18,7 +18,6 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; /** @@ -27,37 +26,38 @@ import java.io.IOException; */ /* package */ final class Sniffer { - /** - * The maximum number of bytes to peek when sniffing. - */ + /** The maximum number of bytes to peek when sniffing. */ private static final int SEARCH_LENGTH = 4 * 1024; - private static final int[] COMPATIBLE_BRANDS = new int[] { - Util.getIntegerCodeForString("isom"), - Util.getIntegerCodeForString("iso2"), - Util.getIntegerCodeForString("iso3"), - Util.getIntegerCodeForString("iso4"), - Util.getIntegerCodeForString("iso5"), - Util.getIntegerCodeForString("iso6"), - Util.getIntegerCodeForString("avc1"), - Util.getIntegerCodeForString("hvc1"), - Util.getIntegerCodeForString("hev1"), - Util.getIntegerCodeForString("mp41"), - Util.getIntegerCodeForString("mp42"), - Util.getIntegerCodeForString("3g2a"), - Util.getIntegerCodeForString("3g2b"), - Util.getIntegerCodeForString("3gr6"), - Util.getIntegerCodeForString("3gs6"), - Util.getIntegerCodeForString("3ge6"), - Util.getIntegerCodeForString("3gg6"), - Util.getIntegerCodeForString("M4V "), - Util.getIntegerCodeForString("M4A "), - Util.getIntegerCodeForString("f4v "), - Util.getIntegerCodeForString("kddi"), - Util.getIntegerCodeForString("M4VP"), - Util.getIntegerCodeForString("qt "), // Apple QuickTime - Util.getIntegerCodeForString("MSNV"), // Sony PSP - }; + private static final int[] COMPATIBLE_BRANDS = + new int[] { + 0x69736f6d, // isom + 0x69736f32, // iso2 + 0x69736f33, // iso3 + 0x69736f34, // iso4 + 0x69736f35, // iso5 + 0x69736f36, // iso6 + 0x61766331, // avc1 + 0x68766331, // hvc1 + 0x68657631, // hev1 + 0x61763031, // av01 + 0x6d703431, // mp41 + 0x6d703432, // mp42 + 0x33673261, // 3g2a + 0x33673262, // 3g2b + 0x33677236, // 3gr6 + 0x33677336, // 3gs6 + 0x33676536, // 3ge6 + 0x33676736, // 3gg6 + 0x4d345620, // M4V[space] + 0x4d344120, // M4A[space] + 0x66347620, // f4v[space] + 0x6b646469, // kddi + 0x4d345650, // M4VP + 0x71742020, // qt[space][space], Apple QuickTime + 0x4d534e56, // MSNV, Sony PSP + 0x64627931, // dby1, Dolby Vision + }; /** * Returns whether data peeked from the current position in {@code input} is consistent with the @@ -104,11 +104,18 @@ import java.io.IOException; input.peekFully(buffer.data, 0, headerSize); long atomSize = buffer.readUnsignedInt(); int atomType = buffer.readInt(); - if (atomSize == Atom.LONG_SIZE_PREFIX) { + if (atomSize == Atom.DEFINES_LARGE_SIZE) { + // Read the large atom size. headerSize = Atom.LONG_HEADER_SIZE; input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE); buffer.setLimit(Atom.LONG_HEADER_SIZE); - atomSize = buffer.readUnsignedLongToLong(); + atomSize = buffer.readLong(); + } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) { + // The atom extends to the end of the file. + long fileEndPosition = input.getLength(); + if (fileEndPosition != C.LENGTH_UNSET) { + atomSize = fileEndPosition - input.getPeekPosition() + headerSize; + } } if (atomSize < headerSize) { @@ -118,6 +125,13 @@ import java.io.IOException; bytesSearched += headerSize; if (atomType == Atom.TYPE_moov) { + // We have seen the moov atom. We increase the search size to make sure we don't miss an + // mvex atom because the moov's size exceeds the search length. + bytesToSearch += (int) atomSize; + if (inputLength != C.LENGTH_UNSET && bytesToSearch > inputLength) { + // Make sure we don't exceed the file size. + bytesToSearch = (int) inputLength; + } // Check for an mvex atom inside the moov atom to identify whether the file is fragmented. continue; } @@ -169,7 +183,7 @@ import java.io.IOException; */ private static boolean isCompatibleBrand(int brand) { // Accept all brands starting '3gp'. - if (brand >>> 8 == Util.getIntegerCodeForString("3gp")) { + if (brand >>> 8 == 0x00336770) { return true; } for (int compatibleBrand : COMPATIBLE_BRANDS) { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java index a179c7d8dded..b7a1555a7675 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java @@ -15,9 +15,11 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; -import android.support.annotation.IntDef; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -27,8 +29,10 @@ import java.lang.annotation.RetentionPolicy; public final class Track { /** - * The transformation to apply to samples in the track, if any. + * The transformation to apply to samples in the track, if any. One of {@link + * #TRANSFORMATION_NONE} or {@link #TRANSFORMATION_CEA608_CDAT}. */ + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({TRANSFORMATION_NONE, TRANSFORMATION_CEA608_CDAT}) public @interface Transformation {} @@ -77,20 +81,15 @@ public final class Track { */ @Transformation public final int sampleTransformation; - /** - * Track encryption boxes for the different track sample descriptions. Entries may be null. - */ - public final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes; - /** * Durations of edit list segments in the movie timescale. Null if there is no edit list. */ - public final long[] editListDurations; + @Nullable public final long[] editListDurations; /** * Media times for edit list segments in the track timescale. Null if there is no edit list. */ - public final long[] editListMediaTimes; + @Nullable public final long[] editListMediaTimes; /** * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. 0 for @@ -98,10 +97,12 @@ public final class Track { */ public final int nalUnitLengthFieldLength; + @Nullable private final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes; + public Track(int id, int type, long timescale, long movieTimescale, long durationUs, Format format, @Transformation int sampleTransformation, - TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength, - long[] editListDurations, long[] editListMediaTimes) { + @Nullable TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength, + @Nullable long[] editListDurations, @Nullable long[] editListMediaTimes) { this.id = id; this.type = type; this.timescale = timescale; @@ -115,4 +116,33 @@ public final class Track { this.editListMediaTimes = editListMediaTimes; } + /** + * Returns the {@link TrackEncryptionBox} for the given sample description index. + * + * @param sampleDescriptionIndex The given sample description index + * @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no + * such entry exists. + */ + @Nullable + public TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptionIndex) { + return sampleDescriptionEncryptionBoxes == null ? null + : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; + } + + // incompatible types in argument. + @SuppressWarnings("nullness:argument.type.incompatible") + public Track copyWithFormat(Format format) { + return new Track( + id, + type, + timescale, + movieTimescale, + durationUs, + format, + sampleTransformation, + sampleDescriptionEncryptionBoxes, + nalUnitLengthFieldLength, + editListDurations, + editListMediaTimes); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java index 395b50425fb9..04bfb82210f6 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java @@ -15,37 +15,89 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; + /** - * Encapsulates information parsed from a track encryption (tenc) box or sample group description + * Encapsulates information parsed from a track encryption (tenc) box or sample group description * (sgpd) box in an MP4 stream. */ public final class TrackEncryptionBox { + private static final String TAG = "TrackEncryptionBox"; + /** * Indicates the encryption state of the samples in the sample group. */ public final boolean isEncrypted; /** - * The initialization vector size in bytes for the samples in the corresponding sample group. + * The protection scheme type, as defined by the 'schm' box, or null if unknown. */ - public final int initializationVectorSize; + @Nullable public final String schemeType; /** - * The key identifier for the samples in the corresponding sample group. + * A {@link TrackOutput.CryptoData} instance containing the encryption information from this + * {@link TrackEncryptionBox}. */ - public final byte[] keyId; + public final TrackOutput.CryptoData cryptoData; + + /** The initialization vector size in bytes for the samples in the corresponding sample group. */ + public final int perSampleIvSize; /** - * @param isEncrypted Indicates the encryption state of the samples in the sample group. - * @param initializationVectorSize The initialization vector size in bytes for the samples in the - * corresponding sample group. - * @param keyId The key identifier for the samples in the corresponding sample group. + * If {@link #perSampleIvSize} is 0, holds the default initialization vector as defined in the + * track encryption box or sample group description box. Null otherwise. */ - public TrackEncryptionBox(boolean isEncrypted, int initializationVectorSize, byte[] keyId) { + @Nullable public final byte[] defaultInitializationVector; + + /** + * @param isEncrypted See {@link #isEncrypted}. + * @param schemeType See {@link #schemeType}. + * @param perSampleIvSize See {@link #perSampleIvSize}. + * @param keyId See {@link TrackOutput.CryptoData#encryptionKey}. + * @param defaultEncryptedBlocks See {@link TrackOutput.CryptoData#encryptedBlocks}. + * @param defaultClearBlocks See {@link TrackOutput.CryptoData#clearBlocks}. + * @param defaultInitializationVector See {@link #defaultInitializationVector}. + */ + public TrackEncryptionBox( + boolean isEncrypted, + @Nullable String schemeType, + int perSampleIvSize, + byte[] keyId, + int defaultEncryptedBlocks, + int defaultClearBlocks, + @Nullable byte[] defaultInitializationVector) { + Assertions.checkArgument(perSampleIvSize == 0 ^ defaultInitializationVector == null); this.isEncrypted = isEncrypted; - this.initializationVectorSize = initializationVectorSize; - this.keyId = keyId; + this.schemeType = schemeType; + this.perSampleIvSize = perSampleIvSize; + this.defaultInitializationVector = defaultInitializationVector; + cryptoData = new TrackOutput.CryptoData(schemeToCryptoMode(schemeType), keyId, + defaultEncryptedBlocks, defaultClearBlocks); + } + + @C.CryptoMode + private static int schemeToCryptoMode(@Nullable String schemeType) { + if (schemeType == null) { + // If unknown, assume cenc. + return C.CRYPTO_MODE_AES_CTR; + } + switch (schemeType) { + case C.CENC_TYPE_cenc: + case C.CENC_TYPE_cens: + return C.CRYPTO_MODE_AES_CTR; + case C.CENC_TYPE_cbc1: + case C.CENC_TYPE_cbcs: + return C.CRYPTO_MODE_AES_CBC; + default: + Log.w(TAG, "Unsupported protection scheme type '" + schemeType + "'. Assuming AES-CTR " + + "crypto mode."); + return C.CRYPTO_MODE_AES_CTR; + } } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java index 4546e4463f47..e027d6ed76f5 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java @@ -190,4 +190,8 @@ import java.io.IOException; return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index]; } + /** Returns whether the sample at the given index has a subsample encryption table. */ + public boolean sampleHasSubsampleEncryptionTable(int index) { + return definesEncryptionData && sampleHasSubsampleEncryptionTable[index]; + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java index 44e1b8b2f1ca..bb9891b302d0 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java @@ -24,43 +24,49 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; */ /* package */ final class TrackSampleTable { - /** - * Number of samples. - */ + /** The track corresponding to this sample table. */ + public final Track track; + /** Number of samples. */ public final int sampleCount; - /** - * Sample offsets in bytes. - */ + /** Sample offsets in bytes. */ public final long[] offsets; - /** - * Sample sizes in bytes. - */ + /** Sample sizes in bytes. */ public final int[] sizes; - /** - * Maximum sample size in {@link #sizes}. - */ + /** Maximum sample size in {@link #sizes}. */ public final int maximumSize; - /** - * Sample timestamps in microseconds. - */ + /** Sample timestamps in microseconds. */ public final long[] timestampsUs; - /** - * Sample flags. - */ + /** Sample flags. */ public final int[] flags; + /** + * The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample + * table is empty. + */ + public final long durationUs; - public TrackSampleTable(long[] offsets, int[] sizes, int maximumSize, long[] timestampsUs, - int[] flags) { + public TrackSampleTable( + Track track, + long[] offsets, + int[] sizes, + int maximumSize, + long[] timestampsUs, + int[] flags, + long durationUs) { Assertions.checkArgument(sizes.length == timestampsUs.length); Assertions.checkArgument(offsets.length == timestampsUs.length); Assertions.checkArgument(flags.length == timestampsUs.length); + this.track = track; this.offsets = offsets; this.sizes = sizes; this.maximumSize = maximumSize; this.timestampsUs = timestampsUs; this.flags = flags; + this.durationUs = durationUs; sampleCount = offsets.length; + if (flags.length > 0) { + flags[flags.length - 1] |= C.BUFFER_FLAG_LAST_SAMPLE; + } } /** diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index ae98bd7b3e72..5d3b27e29460 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -15,32 +15,33 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg; +import androidx.annotation.VisibleForTesting; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; -/** - * Used to seek in an Ogg stream. - */ +/** Seeks in an Ogg stream. */ /* package */ final class DefaultOggSeeker implements OggSeeker { - //@VisibleForTesting - public static final int MATCH_RANGE = 72000; - //@VisibleForTesting - public static final int MATCH_BYTE_RANGE = 100000; + private static final int MATCH_RANGE = 72000; + private static final int MATCH_BYTE_RANGE = 100000; private static final int DEFAULT_OFFSET = 30000; private static final int STATE_SEEK_TO_END = 0; private static final int STATE_READ_LAST_PAGE = 1; private static final int STATE_SEEK = 2; - private static final int STATE_IDLE = 3; + private static final int STATE_SKIP = 3; + private static final int STATE_IDLE = 4; private final OggPageHeader pageHeader = new OggPageHeader(); - private final long startPosition; - private final long endPosition; + private final long payloadStartPosition; + private final long payloadEndPosition; private final StreamReader streamReader; private int state; @@ -55,19 +56,28 @@ import java.io.IOException; /** * Constructs an OggSeeker. - * @param startPosition Start position of the payload (inclusive). - * @param endPosition End position of the payload (exclusive). - * @param streamReader StreamReader instance which owns this OggSeeker + * + * @param streamReader The {@link StreamReader} that owns this seeker. + * @param payloadStartPosition Start position of the payload (inclusive). + * @param payloadEndPosition End position of the payload (exclusive). * @param firstPayloadPageSize The total size of the first payload page, in bytes. * @param firstPayloadPageGranulePosition The granule position of the first payload page. + * @param firstPayloadPageIsLastPage Whether the first payload page is also the last page. */ - public DefaultOggSeeker(long startPosition, long endPosition, StreamReader streamReader, - int firstPayloadPageSize, long firstPayloadPageGranulePosition) { - Assertions.checkArgument(startPosition >= 0 && endPosition > startPosition); + public DefaultOggSeeker( + StreamReader streamReader, + long payloadStartPosition, + long payloadEndPosition, + long firstPayloadPageSize, + long firstPayloadPageGranulePosition, + boolean firstPayloadPageIsLastPage) { + Assertions.checkArgument( + payloadStartPosition >= 0 && payloadEndPosition > payloadStartPosition); this.streamReader = streamReader; - this.startPosition = startPosition; - this.endPosition = endPosition; - if (firstPayloadPageSize == endPosition - startPosition) { + this.payloadStartPosition = payloadStartPosition; + this.payloadEndPosition = payloadEndPosition; + if (firstPayloadPageSize == payloadEndPosition - payloadStartPosition + || firstPayloadPageIsLastPage) { totalGranules = firstPayloadPageGranulePosition; state = STATE_IDLE; } else { @@ -85,7 +95,7 @@ import java.io.IOException; positionBeforeSeekToEnd = input.getPosition(); state = STATE_READ_LAST_PAGE; // Seek to the end just before the last page of stream to get the duration. - long lastPageSearchPosition = endPosition - OggPageHeader.MAX_PAGE_SIZE; + long lastPageSearchPosition = payloadEndPosition - OggPageHeader.MAX_PAGE_SIZE; if (lastPageSearchPosition > positionBeforeSeekToEnd) { return lastPageSearchPosition; } @@ -95,157 +105,123 @@ import java.io.IOException; state = STATE_IDLE; return positionBeforeSeekToEnd; case STATE_SEEK: - long currentGranule; - if (targetGranule == 0) { - currentGranule = 0; - } else { - long position = getNextSeekPosition(targetGranule, input); - if (position >= 0) { - return position; - } - currentGranule = skipToPageOfGranule(input, targetGranule, -(position + 2)); + long position = getNextSeekPosition(input); + if (position != C.POSITION_UNSET) { + return position; } + state = STATE_SKIP; + // Fall through. + case STATE_SKIP: + skipToPageOfTargetGranule(input); state = STATE_IDLE; - return -(currentGranule + 2); + return -(startGranule + 2); default: // Never happens. throw new IllegalStateException(); } } - @Override - public long startSeek(long timeUs) { - Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK); - targetGranule = timeUs == 0 ? 0 : streamReader.convertTimeToGranule(timeUs); - state = STATE_SEEK; - resetSeeking(); - return targetGranule; - } - @Override public OggSeekMap createSeekMap() { return totalGranules != 0 ? new OggSeekMap() : null; } - //@VisibleForTesting - public void resetSeeking() { - start = startPosition; - end = endPosition; + @Override + public void startSeek(long targetGranule) { + this.targetGranule = Util.constrainValue(targetGranule, 0, totalGranules - 1); + state = STATE_SEEK; + start = payloadStartPosition; + end = payloadEndPosition; startGranule = 0; endGranule = totalGranules; } /** - * Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput} - * has to seek and then be passed for another call until a negative number is returned. If a - * negative number is returned the input is at a position which is before the target page and at - * which it is sensible to just skip pages to the target granule and pre-roll instead of doing - * another seek request. + * Performs a single step of a seeking binary search, returning the byte position from which data + * should be provided for the next step, or {@link C#POSITION_UNSET} if the search has converged. + * If the search has converged then {@link #skipToPageOfTargetGranule(ExtractorInput)} should be + * called to skip to the target page. * - * @param targetGranule the target granule position to seek to. - * @param input the {@link ExtractorInput} to read from. - * @return the position to seek the {@link ExtractorInput} to for a next call or - * -(currentGranule + 2) if it's close enough to skip to the target page. - * @throws IOException thrown if reading from the input fails. - * @throws InterruptedException thrown if interrupted while reading from the input. + * @param input The {@link ExtractorInput} to read from. + * @return The byte position from which data should be provided for the next step, or {@link + * C#POSITION_UNSET} if the search has converged. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. */ - //@VisibleForTesting - public long getNextSeekPosition(long targetGranule, ExtractorInput input) - throws IOException, InterruptedException { + private long getNextSeekPosition(ExtractorInput input) throws IOException, InterruptedException { if (start == end) { - return -(startGranule + 2); + return C.POSITION_UNSET; } - long initialPosition = input.getPosition(); + long currentPosition = input.getPosition(); if (!skipToNextPage(input, end)) { - if (start == initialPosition) { + if (start == currentPosition) { throw new IOException("No ogg page can be found."); } return start; } - pageHeader.populate(input, false); + pageHeader.populate(input, /* quiet= */ false); input.resetPeekPosition(); long granuleDistance = targetGranule - pageHeader.granulePosition; int pageSize = pageHeader.headerSize + pageHeader.bodySize; - if (granuleDistance < 0 || granuleDistance > MATCH_RANGE) { - if (granuleDistance < 0) { - end = initialPosition; - endGranule = pageHeader.granulePosition; - } else { - start = input.getPosition() + pageSize; - startGranule = pageHeader.granulePosition; - if (end - start + pageSize < MATCH_BYTE_RANGE) { - input.skipFully(pageSize); - return -(startGranule + 2); - } - } - - if (end - start < MATCH_BYTE_RANGE) { - end = start; - return start; - } - - long offset = pageSize * (granuleDistance <= 0 ? 2 : 1); - long nextPosition = input.getPosition() - offset - + (granuleDistance * (end - start) / (endGranule - startGranule)); - - nextPosition = Math.max(nextPosition, start); - nextPosition = Math.min(nextPosition, end - 1); - return nextPosition; + if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) { + return C.POSITION_UNSET; } - // position accepted (before target granule and within MATCH_RANGE) - input.skipFully(pageSize); - return -(pageHeader.granulePosition + 2); + if (granuleDistance < 0) { + end = currentPosition; + endGranule = pageHeader.granulePosition; + } else { + start = input.getPosition() + pageSize; + startGranule = pageHeader.granulePosition; + } + + if (end - start < MATCH_BYTE_RANGE) { + end = start; + return start; + } + + long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); + long nextPosition = + input.getPosition() + - offset + + (granuleDistance * (end - start) / (endGranule - startGranule)); + return Util.constrainValue(nextPosition, start, end - 1); } - private long getEstimatedPosition(long position, long granuleDistance, long offset) { - position += (granuleDistance * (endPosition - startPosition) / totalGranules) - offset; - if (position < startPosition) { - position = startPosition; + /** + * Skips forward to the start of the page containing the {@code targetGranule}. + * + * @param input The {@link ExtractorInput} to read from. + * @throws ParserException If populating the page header fails. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + private void skipToPageOfTargetGranule(ExtractorInput input) + throws IOException, InterruptedException { + pageHeader.populate(input, /* quiet= */ false); + while (pageHeader.granulePosition <= targetGranule) { + input.skipFully(pageHeader.headerSize + pageHeader.bodySize); + start = input.getPosition(); + startGranule = pageHeader.granulePosition; + pageHeader.populate(input, /* quiet= */ false); } - if (position >= endPosition) { - position = endPosition - 1; - } - return position; - } - - private class OggSeekMap implements SeekMap { - - @Override - public boolean isSeekable() { - return true; - } - - @Override - public long getPosition(long timeUs) { - if (timeUs == 0) { - return startPosition; - } - long granule = streamReader.convertTimeToGranule(timeUs); - return getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET); - } - - @Override - public long getDurationUs() { - return streamReader.convertGranuleToTime(totalGranules); - } - + input.resetPeekPosition(); } /** * Skips to the next page. * * @param input The {@code ExtractorInput} to skip to the next page. - * @throws IOException thrown if peeking/reading from the input fails. - * @throws InterruptedException thrown if interrupted while peeking/reading from the input. - * @throws EOFException if the next page can't be found before the end of the input. + * @throws IOException If peeking/reading from the input fails. + * @throws InterruptedException If the thread is interrupted. + * @throws EOFException If the next page can't be found before the end of the input. */ - //@VisibleForTesting + @VisibleForTesting void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException { - if (!skipToNextPage(input, endPosition)) { + if (!skipToNextPage(input, payloadEndPosition)) { // Not found until eof. throw new EOFException(); } @@ -255,21 +231,20 @@ import java.io.IOException; * Skips to the next page. Searches for the next page header. * * @param input The {@code ExtractorInput} to skip to the next page. - * @param until Searches until this position. - * @return true if the next page is found. - * @throws IOException thrown if peeking/reading from the input fails. - * @throws InterruptedException thrown if interrupted while peeking/reading from the input. + * @param limit The limit up to which the search should take place. + * @return Whether the next page was found. + * @throws IOException If peeking/reading from the input fails. + * @throws InterruptedException If interrupted while peeking/reading from the input. */ - //@VisibleForTesting - boolean skipToNextPage(ExtractorInput input, long until) + private boolean skipToNextPage(ExtractorInput input, long limit) throws IOException, InterruptedException { - until = Math.min(until + 3, endPosition); + limit = Math.min(limit + 3, payloadEndPosition); byte[] buffer = new byte[2048]; int peekLength = buffer.length; while (true) { - if (input.getPosition() + peekLength > until) { + if (input.getPosition() + peekLength > limit) { // Make sure to not peek beyond the end of the input. - peekLength = (int) (until - input.getPosition()); + peekLength = (int) (limit - input.getPosition()); if (peekLength < 4) { // Not found until end. return false; @@ -277,7 +252,9 @@ import java.io.IOException; } input.peekFully(buffer, 0, peekLength, false); for (int i = 0; i < peekLength - 3; i++) { - if (buffer[i] == 'O' && buffer[i + 1] == 'g' && buffer[i + 2] == 'g' + if (buffer[i] == 'O' + && buffer[i + 1] == 'g' + && buffer[i + 2] == 'g' && buffer[i + 3] == 'S') { // Match! Skip to the start of the pattern. input.skipFully(i); @@ -294,48 +271,43 @@ import java.io.IOException; * total number of samples per channel. * * @param input The {@link ExtractorInput} to read from. - * @return the total number of samples of this input. - * @throws IOException thrown if reading from the input fails. - * @throws InterruptedException thrown if interrupted while reading from the input. + * @return The total number of samples of this input. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If the thread is interrupted. */ - //@VisibleForTesting - long readGranuleOfLastPage(ExtractorInput input) - throws IOException, InterruptedException { + @VisibleForTesting + long readGranuleOfLastPage(ExtractorInput input) throws IOException, InterruptedException { skipToNextPage(input); pageHeader.reset(); - while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < endPosition) { - pageHeader.populate(input, false); + while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < payloadEndPosition) { + pageHeader.populate(input, /* quiet= */ false); input.skipFully(pageHeader.headerSize + pageHeader.bodySize); } return pageHeader.granulePosition; } - /** - * Skips to the position of the start of the page containing the {@code targetGranule} and - * returns the granule of the page previous to the target page. - * - * @param input the {@link ExtractorInput} to read from. - * @param targetGranule the target granule. - * @param currentGranule the current granule or -1 if it's unknown. - * @return the granule of the prior page or the {@code currentGranule} if there isn't a prior - * page. - * @throws ParserException thrown if populating the page header fails. - * @throws IOException thrown if reading from the input fails. - * @throws InterruptedException thrown if interrupted while reading from the input. - */ - //@VisibleForTesting - long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule) - throws IOException, InterruptedException { - pageHeader.populate(input, false); - while (pageHeader.granulePosition < targetGranule) { - input.skipFully(pageHeader.headerSize + pageHeader.bodySize); - // Store in a member field to be able to resume after IOExceptions. - currentGranule = pageHeader.granulePosition; - // Peek next header. - pageHeader.populate(input, false); - } - input.resetPeekPosition(); - return currentGranule; - } + private final class OggSeekMap implements SeekMap { + @Override + public boolean isSeekable() { + return true; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + long targetGranule = streamReader.convertTimeToGranule(timeUs); + long estimatedPosition = + payloadStartPosition + + (targetGranule * (payloadEndPosition - payloadStartPosition) / totalGranules) + - DEFAULT_OFFSET; + estimatedPosition = + Util.constrainValue(estimatedPosition, payloadStartPosition, payloadEndPosition - 1); + return new SeekPoints(new SeekPoint(timeUs, estimatedPosition)); + } + + @Override + public long getDurationUs() { + return streamReader.convertGranuleToTime(totalGranules); + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java index 3a3403d7c2ac..449bf35f786a 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java @@ -15,17 +15,18 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg; -import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacMetadataReader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacSeekTableSeekMap; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamInfo; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.Arrays; -import java.util.Collections; -import java.util.List; /** * {@link StreamReader} to extract Flac data out of Ogg byte stream. @@ -33,11 +34,10 @@ import java.util.List; /* package */ final class FlacReader extends StreamReader { private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF; - private static final byte SEEKTABLE_PACKET_TYPE = 0x03; private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4; - private FlacStreamInfo streamInfo; + private FlacStreamMetadata streamMetadata; private FlacOggSeeker flacOggSeeker; public static boolean verifyBitstreamType(ParsableByteArray data) { @@ -49,7 +49,7 @@ import java.util.List; protected void reset(boolean headerData) { super.reset(headerData); if (headerData) { - streamInfo = null; + streamMetadata = null; flacOggSeeker = null; } } @@ -67,20 +67,17 @@ import java.util.List; } @Override - protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) - throws IOException, InterruptedException { + protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) { byte[] data = packet.data; - if (streamInfo == null) { - streamInfo = new FlacStreamInfo(data, 17); + if (streamMetadata == null) { + streamMetadata = new FlacStreamMetadata(data, 17); byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit()); - metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks - List initializationData = Collections.singletonList(metadata); - setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, null, - Format.NO_VALUE, streamInfo.bitRate(), streamInfo.channels, streamInfo.sampleRate, - initializationData, null, 0, null); - } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) { + setupData.format = streamMetadata.getFormat(metadata, /* id3Metadata= */ null); + } else if ((data[0] & 0x7F) == FlacConstants.METADATA_TYPE_SEEK_TABLE) { flacOggSeeker = new FlacOggSeeker(); - flacOggSeeker.parseSeekTable(packet); + FlacStreamMetadata.SeekTable seekTable = + FlacMetadataReader.readSeekTableMetadataBlock(packet); + streamMetadata = streamMetadata.copyWithSeekTable(seekTable); } else if (isAudioPacket(data)) { if (flacOggSeeker != null) { flacOggSeeker.setFirstFrameOffset(position); @@ -92,43 +89,19 @@ import java.util.List; } private int getFlacFrameBlockSize(ParsableByteArray packet) { - int blockSizeCode = (packet.data[2] & 0xFF) >> 4; - switch (blockSizeCode) { - case 1: - return 192; - case 2: - case 3: - case 4: - case 5: - return 576 << (blockSizeCode - 2); - case 6: - case 7: - // skip the sample number - packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET); - packet.readUtf8EncodedLong(); - int value = blockSizeCode == 6 ? packet.readUnsignedByte() : packet.readUnsignedShort(); - packet.setPosition(0); - return value + 1; - case 8: - case 9: - case 10: - case 11: - case 12: - case 13: - case 14: - case 15: - return 256 << (blockSizeCode - 8); + int blockSizeKey = (packet.data[2] & 0xFF) >> 4; + if (blockSizeKey == 6 || blockSizeKey == 7) { + // Skip the sample number. + packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET); + packet.readUtf8EncodedLong(); } - return -1; + int result = FlacFrameReader.readFrameBlockSizeSamplesFromKey(packet, blockSizeKey); + packet.setPosition(0); + return result; } - private class FlacOggSeeker implements OggSeeker, SeekMap { + private class FlacOggSeeker implements OggSeeker { - private static final int METADATA_LENGTH_OFFSET = 1; - private static final int SEEK_POINT_SIZE = 18; - - private long[] seekPointGranules; - private long[] seekPointOffsets; private long firstFrameOffset; private long pendingSeekGranule; @@ -141,27 +114,6 @@ import java.util.List; this.firstFrameOffset = firstFrameOffset; } - /** - * Parses a FLAC file seek table metadata structure and initializes internal fields. - * - * @param data A {@link ParsableByteArray} including whole seek table metadata block. Its - * position should be set to the beginning of the block. - * @see FLAC format - * METADATA_BLOCK_SEEKTABLE - */ - public void parseSeekTable(ParsableByteArray data) { - data.skipBytes(METADATA_LENGTH_OFFSET); - int length = data.readUnsignedInt24(); - int numberOfSeekPoints = length / SEEK_POINT_SIZE; - seekPointGranules = new long[numberOfSeekPoints]; - seekPointOffsets = new long[numberOfSeekPoints]; - for (int i = 0; i < numberOfSeekPoints; i++) { - seekPointGranules[i] = data.readLong(); - seekPointOffsets[i] = data.readLong(); - data.skipBytes(2); // Skip "Number of samples in the target frame." - } - } - @Override public long read(ExtractorInput input) throws IOException, InterruptedException { if (pendingSeekGranule >= 0) { @@ -173,33 +125,17 @@ import java.util.List; } @Override - public long startSeek(long timeUs) { - long granule = convertTimeToGranule(timeUs); - int index = Util.binarySearchFloor(seekPointGranules, granule, true, true); + public void startSeek(long targetGranule) { + Assertions.checkNotNull(streamMetadata.seekTable); + long[] seekPointGranules = streamMetadata.seekTable.pointSampleNumbers; + int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true); pendingSeekGranule = seekPointGranules[index]; - return granule; } @Override public SeekMap createSeekMap() { - return this; - } - - @Override - public boolean isSeekable() { - return true; - } - - @Override - public long getPosition(long timeUs) { - long granule = convertTimeToGranule(timeUs); - int index = Util.binarySearchFloor(seekPointGranules, granule, true, true); - return firstFrameOffset + seekPointOffsets[index]; - } - - @Override - public long getDurationUs() { - return streamInfo.durationUs(); + Assertions.checkState(firstFrameOffset != -1); + return new FlacSeekTableSeekMap(streamMetadata, firstFrameOffset); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java index 55dec61855fb..da53a47dc0f6 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java @@ -27,48 +27,23 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArr import java.io.IOException; /** - * Ogg {@link Extractor}. + * Extracts data from the Ogg container format. */ public class OggExtractor implements Extractor { - /** - * Factory for {@link OggExtractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new OggExtractor()}; - } - - }; + /** Factory for {@link OggExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new OggExtractor()}; private static final int MAX_VERIFICATION_BYTES = 8; + private ExtractorOutput output; private StreamReader streamReader; + private boolean streamReaderInitialized; @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { try { - OggPageHeader header = new OggPageHeader(); - if (!header.populate(input, true) || (header.type & 0x02) != 0x02) { - return false; - } - - int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES); - ParsableByteArray scratch = new ParsableByteArray(length); - input.peekFully(scratch.data, 0, length); - - if (FlacReader.verifyBitstreamType(resetPosition(scratch))) { - streamReader = new FlacReader(); - } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) { - streamReader = new VorbisReader(); - } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) { - streamReader = new OpusReader(); - } else { - return false; - } - return true; + return sniffInternal(input); } catch (ParserException e) { return false; } @@ -76,15 +51,14 @@ public class OggExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); - output.endTracks(); - // TODO: fix the case if sniff() isn't called - streamReader.init(output, trackOutput); + this.output = output; } @Override public void seek(long position, long timeUs) { - streamReader.seek(position, timeUs); + if (streamReader != null) { + streamReader.seek(position, timeUs); + } } @Override @@ -95,12 +69,41 @@ public class OggExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + if (streamReader == null) { + if (!sniffInternal(input)) { + throw new ParserException("Failed to determine bitstream type"); + } + input.resetPeekPosition(); + } + if (!streamReaderInitialized) { + TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); + output.endTracks(); + streamReader.init(output, trackOutput); + streamReaderInitialized = true; + } return streamReader.read(input, seekPosition); } - //@VisibleForTesting - /* package */ StreamReader getStreamReader() { - return streamReader; + private boolean sniffInternal(ExtractorInput input) throws IOException, InterruptedException { + OggPageHeader header = new OggPageHeader(); + if (!header.populate(input, true) || (header.type & 0x02) != 0x02) { + return false; + } + + int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES); + ParsableByteArray scratch = new ParsableByteArray(length); + input.peekFully(scratch.data, 0, length); + + if (FlacReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new FlacReader(); + } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new VorbisReader(); + } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new OpusReader(); + } else { + return false; + } + return true; } private static ParsableByteArray resetPosition(ParsableByteArray scratch) { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java index f305a6737185..1f3bf38c7334 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java @@ -20,6 +20,7 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorI import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; +import java.util.Arrays; /** * OGG packet class. @@ -27,8 +28,8 @@ import java.io.IOException; /* package */ final class OggPacket { private final OggPageHeader pageHeader = new OggPageHeader(); - private final ParsableByteArray packetArray = - new ParsableByteArray(new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0); + private final ParsableByteArray packetArray = new ParsableByteArray( + new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0); private int currentSegmentIndex = C.INDEX_UNSET; private int segmentCount; @@ -50,11 +51,11 @@ import java.io.IOException; * can resume properly from an error while reading a continued packet spanned across multiple * pages. * - * @param input the {@link ExtractorInput} to read data from. - * @return {@code true} if the read was successful. {@code false} if the end of the input was - * encountered having read no data. - * @throws IOException thrown if reading from the input fails. - * @throws InterruptedException thrown if interrupted while reading from input. + * @param input The {@link ExtractorInput} to read data from. + * @return {@code true} if the read was successful. The read fails if the end of the input is + * encountered without reading data. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If the thread is interrupted. */ public boolean populate(ExtractorInput input) throws IOException, InterruptedException { Assertions.checkState(input != null); @@ -85,6 +86,9 @@ import java.io.IOException; int size = calculatePacketSize(currentSegmentIndex); int segmentIndex = currentSegmentIndex + segmentCount; if (size > 0) { + if (packetArray.capacity() < packetArray.limit() + size) { + packetArray.data = Arrays.copyOf(packetArray.data, packetArray.limit() + size); + } input.readFully(packetArray.data, packetArray.limit(), size); packetArray.setLimit(packetArray.limit() + size); populated = pageHeader.laces[segmentIndex - 1] != 255; @@ -99,14 +103,13 @@ import java.io.IOException; /** * An OGG Packet may span multiple pages. Returns the {@link OggPageHeader} of the last page read, * or an empty header if the packet has yet to be populated. - *

- * Note that the returned {@link OggPageHeader} is mutable and may be updated during subsequent + * + *

Note that the returned {@link OggPageHeader} is mutable and may be updated during subsequent * calls to {@link #populate(ExtractorInput)}. * * @return the {@code PageHeader} of the last page read or an empty header if the packet has yet * to be populated. */ - //@VisibleForTesting public OggPageHeader getPageHeader() { return pageHeader; } @@ -118,6 +121,17 @@ import java.io.IOException; return packetArray; } + /** + * Trims the packet data array. + */ + public void trimPayload() { + if (packetArray.data.length == OggPageHeader.MAX_PAGE_PAYLOAD) { + return; + } + packetArray.data = Arrays.copyOf(packetArray.data, Math.max(OggPageHeader.MAX_PAGE_PAYLOAD, + packetArray.limit())); + } + /** * Calculates the size of the packet starting from {@code startSegmentIndex}. * diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java index b1db0e677dba..afdccf80fd1c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java @@ -19,7 +19,6 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; @@ -34,11 +33,17 @@ import java.io.IOException; public static final int MAX_PAGE_SIZE = EMPTY_PAGE_HEADER_SIZE + MAX_SEGMENT_COUNT + MAX_PAGE_PAYLOAD; - private static final int TYPE_OGGS = Util.getIntegerCodeForString("OggS"); + private static final int TYPE_OGGS = 0x4f676753; public int revision; public int type; + /** + * The absolute granule position of the page. This is the total number of samples from the start + * of the file up to the end of the page. Samples partially in the page that continue on + * the next page do not count. + */ public long granulePosition; + public long streamSerialNumber; public long pageSequenceNumber; public long pageChecksum; @@ -71,13 +76,13 @@ import java.io.IOException; /** * Peeks an Ogg page header and updates this {@link OggPageHeader}. * - * @param input the {@link ExtractorInput} to read from. - * @param quiet if {@code true} no Exceptions are thrown but {@code false} is return if something - * goes wrong. - * @return {@code true} if the read was successful. {@code false} if the end of the input was - * encountered having read no data. - * @throws IOException thrown if reading data fails or the stream is invalid. - * @throws InterruptedException thrown if thread is interrupted when reading/peeking. + * @param input The {@link ExtractorInput} to read from. + * @param quiet Whether to return {@code false} rather than throwing an exception if the header + * cannot be populated. + * @return Whether the read was successful. The read fails if the end of the input is encountered + * without reading data. + * @throws IOException If reading data fails or the stream is invalid. + * @throws InterruptedException If the thread is interrupted. */ public boolean populate(ExtractorInput input, boolean quiet) throws IOException, InterruptedException { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java index f8c016d6f201..0a0be963f72b 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java @@ -33,16 +33,14 @@ import java.io.IOException; SeekMap createSeekMap(); /** - * Initializes a seek operation. + * Starts a seek operation. * - * @param timeUs The seek position in microseconds. - * @return The granule position targeted by the seek. + * @param targetGranule The target granule position. */ - long startSeek(long timeUs); + void startSeek(long targetGranule); /** - * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a - * progressive seek. + * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a seek. *

* If more data is required or if the position of the input needs to be modified then a position * from which data should be provided is returned. Else a negative value is returned. If a seek diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java index d7c303c4a3dc..c3f3a13d545b 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java @@ -19,8 +19,6 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; -import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; @@ -39,7 +37,7 @@ import java.util.List; */ private static final int SAMPLE_RATE = 48000; - private static final int OPUS_CODE = Util.getIntegerCodeForString("Opus"); + private static final int OPUS_CODE = 0x4f707573; private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'}; private boolean headerRead; @@ -67,8 +65,7 @@ import java.util.List; } @Override - protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) - throws IOException, InterruptedException { + protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) { if (!headerRead) { byte[] metadata = Arrays.copyOf(packet.data, packet.limit()); int channelCount = metadata[9] & 0xFF; @@ -130,6 +127,6 @@ import java.util.List; } else { length = 10000 << length; } - return frames * length; + return (long) frames * length; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java index 0efdd68b9941..067c8aef0311 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java @@ -26,9 +26,8 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutpu import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; -/** - * StreamReader abstract class. - */ +/** StreamReader abstract class. */ +@SuppressWarnings("UngroupedOverloads") /* package */ abstract class StreamReader { private static final int STATE_READ_HEADERS = 0; @@ -41,7 +40,8 @@ import java.io.IOException; OggSeeker oggSeeker; } - private OggPacket oggPacket; + private final OggPacket oggPacket; + private TrackOutput trackOutput; private ExtractorOutput extractorOutput; private OggSeeker oggSeeker; @@ -55,11 +55,13 @@ import java.io.IOException; private boolean seekMapSet; private boolean formatSet; + public StreamReader() { + oggPacket = new OggPacket(); + } + void init(ExtractorOutput output, TrackOutput trackOutput) { this.extractorOutput = output; this.trackOutput = trackOutput; - this.oggPacket = new OggPacket(); - reset(true); } @@ -89,7 +91,8 @@ import java.io.IOException; reset(!seekMapSet); } else { if (state != STATE_READ_HEADERS) { - targetGranule = oggSeeker.startSeek(timeUs); + targetGranule = convertTimeToGranule(timeUs); + oggSeeker.startSeek(targetGranule); state = STATE_READ_PAYLOAD; } } @@ -103,15 +106,12 @@ import java.io.IOException; switch (state) { case STATE_READ_HEADERS: return readHeaders(input); - case STATE_SKIP_HEADERS: input.skipFully((int) payloadStartPosition); state = STATE_READ_PAYLOAD; return Extractor.RESULT_CONTINUE; - case STATE_READ_PAYLOAD: return readPayload(input, seekPosition); - default: // Never happens. throw new IllegalStateException(); @@ -145,13 +145,21 @@ import java.io.IOException; oggSeeker = new UnseekableOggSeeker(); } else { OggPageHeader firstPayloadPageHeader = oggPacket.getPageHeader(); - oggSeeker = new DefaultOggSeeker(payloadStartPosition, input.getLength(), this, - firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize, - firstPayloadPageHeader.granulePosition); + boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream. + oggSeeker = + new DefaultOggSeeker( + this, + payloadStartPosition, + input.getLength(), + firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize, + firstPayloadPageHeader.granulePosition, + isLastPage); } setupData = null; state = STATE_READ_PAYLOAD; + // First payload packet. Trim the payload array of the ogg packet after headers have been read. + oggPacket.trimPayload(); return Extractor.RESULT_CONTINUE; } @@ -241,13 +249,13 @@ import java.io.IOException; private static final class UnseekableOggSeeker implements OggSeeker { @Override - public long read(ExtractorInput input) throws IOException, InterruptedException { + public long read(ExtractorInput input) { return -1; } @Override - public long startSeek(long timeUs) { - return 0; + public void startSeek(long targetGranule) { + // Do nothing. } @Override diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java index 07ff16f4357e..cb0678a28555 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java @@ -15,9 +15,11 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg; +import androidx.annotation.VisibleForTesting; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ogg.VorbisUtil.Mode; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.VorbisUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.VorbisUtil.Mode; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; @@ -101,13 +103,13 @@ import java.util.ArrayList; codecInitialisationData.add(vorbisSetup.setupHeaderData); setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, null, - this.vorbisSetup.idHeader.bitrateNominal, OggPageHeader.MAX_PAGE_PAYLOAD, + this.vorbisSetup.idHeader.bitrateNominal, Format.NO_VALUE, this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate, codecInitialisationData, null, 0, null); return true; } - //@VisibleForTesting + @VisibleForTesting /* package */ VorbisSetup readSetupHeaders(ParsableByteArray scratch) throws IOException { if (vorbisIdHeader == null) { @@ -133,27 +135,27 @@ import java.util.ArrayList; } /** - * Reads an int of {@code length} bits from {@code src} starting at - * {@code leastSignificantBitIndex}. + * Reads an int of {@code length} bits from {@code src} starting at {@code + * leastSignificantBitIndex}. * * @param src the {@code byte} to read from. * @param length the length in bits of the int to read. * @param leastSignificantBitIndex the index of the least significant bit of the int to read. * @return the int value read. */ - //@VisibleForTesting + @VisibleForTesting /* package */ static int readBits(byte src, int length, int leastSignificantBitIndex) { return (src >> leastSignificantBitIndex) & (255 >>> (8 - length)); } - //@VisibleForTesting - /* package */ static void appendNumberOfSamples(ParsableByteArray buffer, - long packetSampleCount) { + @VisibleForTesting + /* package */ static void appendNumberOfSamples( + ParsableByteArray buffer, long packetSampleCount) { buffer.setLimit(buffer.limit() + 4); // The vorbis decoder expects the number of samples in the packet // to be appended to the audio data as an int32 - buffer.data[buffer.limit() - 4] = (byte) ((packetSampleCount) & 0xFF); + buffer.data[buffer.limit() - 4] = (byte) (packetSampleCount & 0xFF); buffer.data[buffer.limit() - 3] = (byte) ((packetSampleCount >>> 8) & 0xFF); buffer.data[buffer.limit() - 2] = (byte) ((packetSampleCount >>> 16) & 0xFF); buffer.data[buffer.limit() - 1] = (byte) ((packetSampleCount >>> 24) & 0xFF); diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java index 6ed85f97f48e..a7b32782ffa0 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java @@ -25,17 +25,16 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHo import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; /** - * Extracts CEA data from a RawCC file. + * Extracts data from the RawCC container format. */ public final class RawCcExtractor implements Extractor { private static final int SCRATCH_SIZE = 9; private static final int HEADER_SIZE = 8; - private static final int HEADER_ID = Util.getIntegerCodeForString("RCC\u0001"); + private static final int HEADER_ID = 0x52434301; private static final int TIMESTAMP_SIZE_V0 = 4; private static final int TIMESTAMP_SIZE_V1 = 8; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index 67563366a4aa..a0a136593521 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -15,6 +15,10 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; + import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; @@ -25,26 +29,15 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHo import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; /** - * Facilitates the extraction of AC-3 samples from elementary audio files formatted as AC-3 - * bitstreams. + * Extracts data from (E-)AC-3 bitstreams. */ public final class Ac3Extractor implements Extractor { - /** - * Factory for {@link Ac3Extractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new Ac3Extractor()}; - } - - }; + /** Factory for {@link Ac3Extractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Ac3Extractor()}; /** * The maximum number of bytes to search when sniffing, excluding ID3 information, before giving @@ -53,35 +46,32 @@ public final class Ac3Extractor implements Extractor { private static final int MAX_SNIFF_BYTES = 8 * 1024; private static final int AC3_SYNC_WORD = 0x0B77; private static final int MAX_SYNC_FRAME_SIZE = 2786; - private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); - private final long firstSampleTimestampUs; + private final Ac3Reader reader; private final ParsableByteArray sampleData; - private Ac3Reader reader; private boolean startedPacket; + /** Creates a new extractor for AC-3 bitstreams. */ public Ac3Extractor() { - this(0); - } - - public Ac3Extractor(long firstSampleTimestampUs) { - this.firstSampleTimestampUs = firstSampleTimestampUs; + reader = new Ac3Reader(); sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE); } + // Extractor implementation. + @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { // Skip any ID3 headers. - ParsableByteArray scratch = new ParsableByteArray(10); + ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH); int startPosition = 0; while (true) { - input.peekFully(scratch.data, 0, 10); + input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH); scratch.setPosition(0); if (scratch.readUnsignedInt24() != ID3_TAG) { break; } - scratch.skipBytes(3); + scratch.skipBytes(3); // version, flags int length = scratch.readSynchSafeInt(); startPosition += 10 + length; input.advancePeekPosition(length); @@ -92,7 +82,7 @@ public final class Ac3Extractor implements Extractor { int headerPosition = startPosition; int validFramesCount = 0; while (true) { - input.peekFully(scratch.data, 0, 5); + input.peekFully(scratch.data, 0, 6); scratch.setPosition(0); int syncBytes = scratch.readUnsignedShort(); if (syncBytes != AC3_SYNC_WORD) { @@ -110,14 +100,13 @@ public final class Ac3Extractor implements Extractor { if (frameSize == C.LENGTH_UNSET) { return false; } - input.advancePeekPosition(frameSize - 5); + input.advancePeekPosition(frameSize - 6); } } } @Override public void init(ExtractorOutput output) { - reader = new Ac3Reader(); // TODO: Add support for embedded ID3. reader.createTracks(output, new TrackIdGenerator(0, 1)); output.endTracks(); output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); @@ -148,7 +137,7 @@ public final class Ac3Extractor implements Extractor { if (!startedPacket) { // Pass data to the reader as though it's contained within a single infinitely long packet. - reader.packetStarted(firstSampleTimestampUs, true); + reader.packetStarted(/* pesTimeUs= */ 0, FLAG_DATA_ALIGNMENT_INDICATOR); startedPacket = true; } // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index 63391719c112..3a6eebbcd2ef 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -15,15 +15,17 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; -import android.support.annotation.IntDef; +import androidx.annotation.IntDef; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac3Util.SyncFrameInfo; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -32,14 +34,16 @@ import java.lang.annotation.RetentionPolicy; */ public final class Ac3Reader implements ElementaryStreamReader { + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({STATE_FINDING_SYNC, STATE_READING_HEADER, STATE_READING_SAMPLE}) private @interface State {} + private static final int STATE_FINDING_SYNC = 0; private static final int STATE_READING_HEADER = 1; private static final int STATE_READING_SAMPLE = 2; - private static final int HEADER_SIZE = 8; + private static final int HEADER_SIZE = 128; private final ParsableBitArray headerScratchBits; private final ParsableByteArray headerScratchBytes; @@ -96,7 +100,7 @@ public final class Ac3Reader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { timeUs = pesTimeUs; } @@ -187,7 +191,7 @@ public final class Ac3Reader implements ElementaryStreamReader { @SuppressWarnings("ReferenceEquality") private void parseHeader() { headerScratchBits.setPosition(0); - Ac3Util.Ac3SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits); + SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits); if (format == null || frameInfo.channelCount != format.channelCount || frameInfo.sampleRate != format.sampleRate || frameInfo.mimeType != format.sampleMimeType) { @@ -197,7 +201,7 @@ public final class Ac3Reader implements ElementaryStreamReader { output.format(format); } sampleSize = frameInfo.frameSize; - // In this class a sample is an access unit (syncframe in AC-3), but the MediaFormat sample rate + // In this class a sample is an access unit (syncframe in AC-3), but Format#sampleRate // specifies the number of PCM audio samples per second. sampleDurationUs = C.MICROS_PER_SECOND * frameInfo.sampleCount / format.sampleRate; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java new file mode 100644 index 000000000000..9578d110b7fd --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java @@ -0,0 +1,156 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util.AC40_SYNCWORD; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util.AC41_SYNCWORD; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; + +/** Extracts data from AC-4 bitstreams. */ +public final class Ac4Extractor implements Extractor { + + /** Factory for {@link Ac4Extractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Ac4Extractor()}; + + /** + * The maximum number of bytes to search when sniffing, excluding ID3 information, before giving + * up. + */ + private static final int MAX_SNIFF_BYTES = 8 * 1024; + + /** + * The size of the reading buffer, in bytes. This value is determined based on the maximum frame + * size used in broadcast applications. + */ + private static final int READ_BUFFER_SIZE = 16384; + + /** The size of the frame header, in bytes. */ + private static final int FRAME_HEADER_SIZE = 7; + + private final Ac4Reader reader; + private final ParsableByteArray sampleData; + + private boolean startedPacket; + + /** Creates a new extractor for AC-4 bitstreams. */ + public Ac4Extractor() { + reader = new Ac4Reader(); + sampleData = new ParsableByteArray(READ_BUFFER_SIZE); + } + + // Extractor implementation. + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + // Skip any ID3 headers. + ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH); + int startPosition = 0; + while (true) { + input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH); + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != ID3_TAG) { + break; + } + scratch.skipBytes(3); // version, flags + int length = scratch.readSynchSafeInt(); + startPosition += 10 + length; + input.advancePeekPosition(length); + } + input.resetPeekPosition(); + input.advancePeekPosition(startPosition); + + int headerPosition = startPosition; + int validFramesCount = 0; + while (true) { + input.peekFully(scratch.data, /* offset= */ 0, /* length= */ FRAME_HEADER_SIZE); + scratch.setPosition(0); + int syncBytes = scratch.readUnsignedShort(); + if (syncBytes != AC40_SYNCWORD && syncBytes != AC41_SYNCWORD) { + validFramesCount = 0; + input.resetPeekPosition(); + if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) { + return false; + } + input.advancePeekPosition(headerPosition); + } else { + if (++validFramesCount >= 4) { + return true; + } + int frameSize = Ac4Util.parseAc4SyncframeSize(scratch.data, syncBytes); + if (frameSize == C.LENGTH_UNSET) { + return false; + } + input.advancePeekPosition(frameSize - FRAME_HEADER_SIZE); + } + } + } + + @Override + public void init(ExtractorOutput output) { + reader.createTracks( + output, new TrackIdGenerator(/* firstTrackId= */ 0, /* trackIdIncrement= */ 1)); + output.endTracks(); + output.seekMap(new SeekMap.Unseekable(/* durationUs= */ C.TIME_UNSET)); + } + + @Override + public void seek(long position, long timeUs) { + startedPacket = false; + reader.seek(); + } + + @Override + public void release() { + // Do nothing. + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + int bytesRead = input.read(sampleData.data, /* offset= */ 0, /* length= */ READ_BUFFER_SIZE); + if (bytesRead == C.RESULT_END_OF_INPUT) { + return RESULT_END_OF_INPUT; + } + + // Feed whatever data we have to the reader, regardless of whether the read finished or not. + sampleData.setPosition(0); + sampleData.setLimit(bytesRead); + + if (!startedPacket) { + // Pass data to the reader as though it's contained within a single infinitely long packet. + reader.packetStarted(/* pesTimeUs= */ 0, FLAG_DATA_ALIGNMENT_INDICATOR); + startedPacket = true; + } + // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes + // unnecessary to copy the data through packetBuffer. + reader.consume(sampleData); + return RESULT_CONTINUE; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java new file mode 100644 index 000000000000..2b9965b19b26 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java @@ -0,0 +1,216 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.Ac4Util.SyncFrameInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Parses a continuous AC-4 byte stream and extracts individual samples. */ +public final class Ac4Reader implements ElementaryStreamReader { + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_FINDING_SYNC, STATE_READING_HEADER, STATE_READING_SAMPLE}) + private @interface State {} + + private static final int STATE_FINDING_SYNC = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_SAMPLE = 2; + + private final ParsableBitArray headerScratchBits; + private final ParsableByteArray headerScratchBytes; + private final String language; + + private String trackFormatId; + private TrackOutput output; + + @State private int state; + private int bytesRead; + + // Used to find the header. + private boolean lastByteWasAC; + private boolean hasCRC; + + // Used when parsing the header. + private long sampleDurationUs; + private Format format; + private int sampleSize; + + // Used when reading the samples. + private long timeUs; + + /** Constructs a new reader for AC-4 elementary streams. */ + public Ac4Reader() { + this(null); + } + + /** + * Constructs a new reader for AC-4 elementary streams. + * + * @param language Track language. + */ + public Ac4Reader(String language) { + headerScratchBits = new ParsableBitArray(new byte[Ac4Util.HEADER_SIZE_FOR_PARSER]); + headerScratchBytes = new ParsableByteArray(headerScratchBits.data); + state = STATE_FINDING_SYNC; + bytesRead = 0; + lastByteWasAC = false; + hasCRC = false; + this.language = language; + } + + @Override + public void seek() { + state = STATE_FINDING_SYNC; + bytesRead = 0; + lastByteWasAC = false; + hasCRC = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { + generator.generateNewId(); + trackFormatId = generator.getFormatId(); + output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); + } + + @Override + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_SYNC: + if (skipToNextSync(data)) { + state = STATE_READING_HEADER; + headerScratchBytes.data[0] = (byte) 0xAC; + headerScratchBytes.data[1] = (byte) (hasCRC ? 0x41 : 0x40); + bytesRead = 2; + } + break; + case STATE_READING_HEADER: + if (continueRead(data, headerScratchBytes.data, Ac4Util.HEADER_SIZE_FOR_PARSER)) { + parseHeader(); + headerScratchBytes.setPosition(0); + output.sampleData(headerScratchBytes, Ac4Util.HEADER_SIZE_FOR_PARSER); + state = STATE_READING_SAMPLE; + } + break; + case STATE_READING_SAMPLE: + int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); + output.sampleData(data, bytesToRead); + bytesRead += bytesToRead; + if (bytesRead == sampleSize) { + output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + timeUs += sampleDurationUs; + state = STATE_FINDING_SYNC; + } + break; + default: + break; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read. + * @param targetLength The target length of the read. + * @return Whether the target length was reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + source.readBytes(target, bytesRead, bytesToRead); + bytesRead += bytesToRead; + return bytesRead == targetLength; + } + + /** + * Locates the next syncword, advancing the position to the byte that immediately follows it. If a + * syncword was not located, the position is advanced to the limit. + * + * @param pesBuffer The buffer whose position should be advanced. + * @return Whether a syncword position was found. + */ + private boolean skipToNextSync(ParsableByteArray pesBuffer) { + while (pesBuffer.bytesLeft() > 0) { + if (!lastByteWasAC) { + lastByteWasAC = (pesBuffer.readUnsignedByte() == 0xAC); + continue; + } + int secondByte = pesBuffer.readUnsignedByte(); + lastByteWasAC = secondByte == 0xAC; + if (secondByte == 0x40 || secondByte == 0x41) { + hasCRC = secondByte == 0x41; + return true; + } + } + return false; + } + + /** Parses the sample header. */ + @SuppressWarnings("ReferenceEquality") + private void parseHeader() { + headerScratchBits.setPosition(0); + SyncFrameInfo frameInfo = Ac4Util.parseAc4SyncframeInfo(headerScratchBits); + if (format == null + || frameInfo.channelCount != format.channelCount + || frameInfo.sampleRate != format.sampleRate + || !MimeTypes.AUDIO_AC4.equals(format.sampleMimeType)) { + format = + Format.createAudioSampleFormat( + trackFormatId, + MimeTypes.AUDIO_AC4, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + frameInfo.channelCount, + frameInfo.sampleRate, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + language); + output.format(format); + } + sampleSize = frameInfo.frameSize; + // In this class a sample is an AC-4 sync frame, but Format#sampleRate specifies the number of + // PCM audio samples per second. + sampleDurationUs = C.MICROS_PER_SECOND * frameInfo.sampleCount / format.sampleRate; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index bfd28f5c1e70..b91abfc75afa 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -15,7 +15,15 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -23,91 +31,116 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractors import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.EOFException; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** - * Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS - * headers. + * Extracts data from AAC bit streams with ADTS framing. */ public final class AdtsExtractor implements Extractor { + /** Factory for {@link AdtsExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AdtsExtractor()}; + /** - * Factory for {@link AdtsExtractor} instances. + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}. */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}) + public @interface Flags {} + /** + * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would + * otherwise not be possible. + * + *

Note that this approach may result in approximated stream duration and seek position that + * are not precise, especially when the stream bitrate varies a lot. + */ + public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new AdtsExtractor()}; - } - - }; - - private static final int MAX_PACKET_SIZE = 200; - private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + private static final int MAX_PACKET_SIZE = 2 * 1024; /** * The maximum number of bytes to search when sniffing, excluding the header, before giving up. * Frame sizes are represented by 13-bit fields, so expect a valid frame in the first 8192 bytes. */ private static final int MAX_SNIFF_BYTES = 8 * 1024; + /** + * The maximum number of frames to use when calculating the average frame size for constant + * bitrate seeking. + */ + private static final int NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE = 1000; - private final long firstSampleTimestampUs; + private final @Flags int flags; + + private final AdtsReader reader; private final ParsableByteArray packetBuffer; + private final ParsableByteArray scratch; + private final ParsableBitArray scratchBits; - // Accessed only by the loading thread. - private AdtsReader reader; + @Nullable private ExtractorOutput extractorOutput; + + private long firstSampleTimestampUs; + private long firstFramePosition; + private int averageFrameSize; + private boolean hasCalculatedAverageFrameSize; private boolean startedPacket; + private boolean hasOutputSeekMap; + /** Creates a new extractor for ADTS bitstreams. */ public AdtsExtractor() { - this(0); + this(/* flags= */ 0); } - public AdtsExtractor(long firstSampleTimestampUs) { - this.firstSampleTimestampUs = firstSampleTimestampUs; + /** + * Creates a new extractor for ADTS bitstreams. + * + * @param flags Flags that control the extractor's behavior. + */ + public AdtsExtractor(@Flags int flags) { + this.flags = flags; + reader = new AdtsReader(true); packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); + averageFrameSize = C.LENGTH_UNSET; + firstFramePosition = C.POSITION_UNSET; + // Allocate scratch space for an ID3 header. The same buffer is also used to read 4 byte values. + scratch = new ParsableByteArray(ID3_HEADER_LENGTH); + scratchBits = new ParsableBitArray(scratch.data); } + // Extractor implementation. + @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { // Skip any ID3 headers. - ParsableByteArray scratch = new ParsableByteArray(10); - ParsableBitArray scratchBits = new ParsableBitArray(scratch.data); - int startPosition = 0; - while (true) { - input.peekFully(scratch.data, 0, 10); - scratch.setPosition(0); - if (scratch.readUnsignedInt24() != ID3_TAG) { - break; - } - scratch.skipBytes(3); - int length = scratch.readSynchSafeInt(); - startPosition += 10 + length; - input.advancePeekPosition(length); - } - input.resetPeekPosition(); - input.advancePeekPosition(startPosition); + int startPosition = peekId3Header(input); // Try to find four or more consecutive AAC audio frames, exceeding the MPEG TS packet size. int headerPosition = startPosition; - int validFramesSize = 0; + int totalValidFramesSize = 0; int validFramesCount = 0; while (true) { input.peekFully(scratch.data, 0, 2); scratch.setPosition(0); int syncBytes = scratch.readUnsignedShort(); - if ((syncBytes & 0xFFF6) != 0xFFF0) { + if (!AdtsReader.isAdtsSyncWord(syncBytes)) { validFramesCount = 0; - validFramesSize = 0; + totalValidFramesSize = 0; input.resetPeekPosition(); if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) { return false; } input.advancePeekPosition(headerPosition); } else { - if (++validFramesCount >= 4 && validFramesSize > 188) { + if (++validFramesCount >= 4 && totalValidFramesSize > TsExtractor.TS_PACKET_SIZE) { return true; } @@ -120,23 +153,23 @@ public final class AdtsExtractor implements Extractor { return false; } input.advancePeekPosition(frameSize - 6); - validFramesSize += frameSize; + totalValidFramesSize += frameSize; } } } @Override public void init(ExtractorOutput output) { - reader = new AdtsReader(true); + this.extractorOutput = output; reader.createTracks(output, new TrackIdGenerator(0, 1)); output.endTracks(); - output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); } @Override public void seek(long position, long timeUs) { startedPacket = false; reader.seek(); + firstSampleTimestampUs = timeUs; } @Override @@ -147,8 +180,17 @@ public final class AdtsExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + long inputLength = input.getLength(); + boolean canUseConstantBitrateSeeking = + (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0 && inputLength != C.LENGTH_UNSET; + if (canUseConstantBitrateSeeking) { + calculateAverageFrameSize(input); + } + int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE); - if (bytesRead == C.RESULT_END_OF_INPUT) { + boolean readEndOfStream = bytesRead == RESULT_END_OF_INPUT; + maybeOutputSeekMap(inputLength, canUseConstantBitrateSeeking, readEndOfStream); + if (readEndOfStream) { return RESULT_END_OF_INPUT; } @@ -158,7 +200,7 @@ public final class AdtsExtractor implements Extractor { if (!startedPacket) { // Pass data to the reader as though it's contained within a single infinitely long packet. - reader.packetStarted(firstSampleTimestampUs, true); + reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR); startedPacket = true; } // TODO: Make it possible for reader to consume the dataSource directly, so that it becomes @@ -167,4 +209,124 @@ public final class AdtsExtractor implements Extractor { return RESULT_CONTINUE; } + private int peekId3Header(ExtractorInput input) throws IOException, InterruptedException { + int firstFramePosition = 0; + while (true) { + input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH); + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != ID3_TAG) { + break; + } + scratch.skipBytes(3); + int length = scratch.readSynchSafeInt(); + firstFramePosition += ID3_HEADER_LENGTH + length; + input.advancePeekPosition(length); + } + input.resetPeekPosition(); + input.advancePeekPosition(firstFramePosition); + if (this.firstFramePosition == C.POSITION_UNSET) { + this.firstFramePosition = firstFramePosition; + } + return firstFramePosition; + } + + private void maybeOutputSeekMap( + long inputLength, boolean canUseConstantBitrateSeeking, boolean readEndOfStream) { + if (hasOutputSeekMap) { + return; + } + boolean useConstantBitrateSeeking = canUseConstantBitrateSeeking && averageFrameSize > 0; + if (useConstantBitrateSeeking + && reader.getSampleDurationUs() == C.TIME_UNSET + && !readEndOfStream) { + // Wait for the sampleDurationUs to be available, or for the end of the stream to be reached, + // before creating seek map. + return; + } + + ExtractorOutput extractorOutput = Assertions.checkNotNull(this.extractorOutput); + if (useConstantBitrateSeeking && reader.getSampleDurationUs() != C.TIME_UNSET) { + extractorOutput.seekMap(getConstantBitrateSeekMap(inputLength)); + } else { + extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); + } + hasOutputSeekMap = true; + } + + private void calculateAverageFrameSize(ExtractorInput input) + throws IOException, InterruptedException { + if (hasCalculatedAverageFrameSize) { + return; + } + averageFrameSize = C.LENGTH_UNSET; + input.resetPeekPosition(); + if (input.getPosition() == 0) { + // Skip any ID3 headers. + peekId3Header(input); + } + + int numValidFrames = 0; + long totalValidFramesSize = 0; + try { + while (input.peekFully( + scratch.data, /* offset= */ 0, /* length= */ 2, /* allowEndOfInput= */ true)) { + scratch.setPosition(0); + int syncBytes = scratch.readUnsignedShort(); + if (!AdtsReader.isAdtsSyncWord(syncBytes)) { + // Invalid sync byte pattern. + // Constant bit-rate seeking will probably fail for this stream. + numValidFrames = 0; + break; + } else { + // Read the frame size. + if (!input.peekFully( + scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true)) { + break; + } + scratchBits.setPosition(14); + int currentFrameSize = scratchBits.readBits(13); + // Either the stream is malformed OR we're not parsing an ADTS stream. + if (currentFrameSize <= 6) { + hasCalculatedAverageFrameSize = true; + throw new ParserException("Malformed ADTS stream"); + } + totalValidFramesSize += currentFrameSize; + if (++numValidFrames == NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE) { + break; + } + if (!input.advancePeekPosition(currentFrameSize - 6, /* allowEndOfInput= */ true)) { + break; + } + } + } + } catch (EOFException e) { + // We reached the end of the input during a peekFully() or advancePeekPosition() operation. + // This is OK, it just means the input has an incomplete ADTS frame at the end. Ideally + // ExtractorInput would allow these operations to encounter end-of-input without throwing an + // exception [internal: b/145586657]. + } + input.resetPeekPosition(); + if (numValidFrames > 0) { + averageFrameSize = (int) (totalValidFramesSize / numValidFrames); + } else { + averageFrameSize = C.LENGTH_UNSET; + } + hasCalculatedAverageFrameSize = true; + } + + private SeekMap getConstantBitrateSeekMap(long inputLength) { + int bitrate = getBitrateFromFrameSize(averageFrameSize, reader.getSampleDurationUs()); + return new ConstantBitrateSeekMap(inputLength, firstFramePosition, bitrate, averageFrameSize); + } + + /** + * Returns the stream bitrate, given a frame size and the duration of that frame in microseconds. + * + * @param frameSize The size of each frame in the stream. + * @param durationUsPerFrame The duration of the given frame in microseconds. + * @return The stream bitrate. + */ + private static int getBitrateFromFrameSize(int frameSize, long durationUsPerFrame) { + return (int) ((frameSize * C.BITS_PER_BYTE * C.MICROS_PER_SECOND) / durationUsPerFrame); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index da49daef93fa..f577747ec217 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -15,15 +15,16 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; -import android.util.Log; import android.util.Pair; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DummyTrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; @@ -38,9 +39,10 @@ public final class AdtsReader implements ElementaryStreamReader { private static final String TAG = "AdtsReader"; private static final int STATE_FINDING_SAMPLE = 0; - private static final int STATE_READING_ID3_HEADER = 1; - private static final int STATE_READING_ADTS_HEADER = 2; - private static final int STATE_READING_SAMPLE = 3; + private static final int STATE_CHECKING_ADTS_HEADER = 1; + private static final int STATE_READING_ID3_HEADER = 2; + private static final int STATE_READING_ADTS_HEADER = 3; + private static final int STATE_READING_SAMPLE = 4; private static final int HEADER_SIZE = 5; private static final int CRC_SIZE = 2; @@ -55,6 +57,7 @@ public final class AdtsReader implements ElementaryStreamReader { private static final int ID3_HEADER_SIZE = 10; private static final int ID3_SIZE_OFFSET = 6; private static final byte[] ID3_IDENTIFIER = {'I', 'D', '3'}; + private static final int VERSION_UNSET = -1; private final boolean exposeId3; private final ParsableBitArray adtsScratch; @@ -71,6 +74,13 @@ public final class AdtsReader implements ElementaryStreamReader { private int matchState; private boolean hasCrc; + private boolean foundFirstFrame; + + // Used to verifies sync words + private int firstFrameVersion; + private int firstFrameSampleRateIndex; + + private int currentFrameVersion; // Used when parsing the header. private boolean hasOutputFormat; @@ -98,13 +108,21 @@ public final class AdtsReader implements ElementaryStreamReader { adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE)); setFindingSampleState(); + firstFrameVersion = VERSION_UNSET; + firstFrameSampleRateIndex = C.INDEX_UNSET; + sampleDurationUs = C.TIME_UNSET; this.exposeId3 = exposeId3; this.language = language; } + /** Returns whether an integer matches an ADTS SYNC word. */ + public static boolean isAdtsSyncWord(int candidateSyncWord) { + return (candidateSyncWord & 0xFFF6) == 0xFFF0; + } + @Override public void seek() { - setFindingSampleState(); + resetSync(); } @Override @@ -123,12 +141,12 @@ public final class AdtsReader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { timeUs = pesTimeUs; } @Override - public void consume(ParsableByteArray data) { + public void consume(ParsableByteArray data) throws ParserException { while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SAMPLE: @@ -139,6 +157,9 @@ public final class AdtsReader implements ElementaryStreamReader { parseId3Header(); } break; + case STATE_CHECKING_ADTS_HEADER: + checkAdtsHeader(data); + break; case STATE_READING_ADTS_HEADER: int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; if (continueRead(data, adtsScratch.data, targetLength)) { @@ -148,6 +169,8 @@ public final class AdtsReader implements ElementaryStreamReader { case STATE_READING_SAMPLE: readSample(data); break; + default: + throw new IllegalStateException(); } } } @@ -157,6 +180,19 @@ public final class AdtsReader implements ElementaryStreamReader { // Do nothing. } + /** + * Returns the duration in microseconds per sample, or {@link C#TIME_UNSET} if the sample duration + * is not available. + */ + public long getSampleDurationUs() { + return sampleDurationUs; + } + + private void resetSync() { + foundFirstFrame = false; + setFindingSampleState(); + } + /** * Continues a read from the provided {@code source} into a given {@code target}. It's assumed * that the data should be written into {@code target} starting from an offset of zero. @@ -218,6 +254,12 @@ public final class AdtsReader implements ElementaryStreamReader { bytesRead = 0; } + /** Sets the state to STATE_CHECKING_ADTS_HEADER. */ + private void setCheckingAdtsHeaderState() { + state = STATE_CHECKING_ADTS_HEADER; + bytesRead = 0; + } + /** * Locates the next sample start, advancing the position to the byte that immediately follows * identifier. If a sample was not located, the position is advanced to the limit. @@ -230,12 +272,21 @@ public final class AdtsReader implements ElementaryStreamReader { int endOffset = pesBuffer.limit(); while (position < endOffset) { int data = adtsData[position++] & 0xFF; - if (matchState == MATCH_STATE_FF && data >= 0xF0 && data != 0xFF) { - hasCrc = (data & 0x1) == 0; - setReadingAdtsHeaderState(); - pesBuffer.setPosition(position); - return; + if (matchState == MATCH_STATE_FF && isAdtsSyncBytes((byte) 0xFF, (byte) data)) { + if (foundFirstFrame + || checkSyncPositionValid(pesBuffer, /* syncPositionCandidate= */ position - 2)) { + currentFrameVersion = (data & 0x8) >> 3; + hasCrc = (data & 0x1) == 0; + if (!foundFirstFrame) { + setCheckingAdtsHeaderState(); + } else { + setReadingAdtsHeaderState(); + } + pesBuffer.setPosition(position); + return; + } } + switch (matchState | data) { case MATCH_STATE_START | 0xFF: matchState = MATCH_STATE_FF; @@ -263,6 +314,145 @@ public final class AdtsReader implements ElementaryStreamReader { pesBuffer.setPosition(position); } + /** + * Peeks the Adts header of the current frame and checks if it is valid. If the header is valid, + * transition to {@link #STATE_READING_ADTS_HEADER}; else, transition to {@link + * #STATE_FINDING_SAMPLE}. + */ + private void checkAdtsHeader(ParsableByteArray buffer) { + if (buffer.bytesLeft() == 0) { + // Not enough data to check yet, defer this check. + return; + } + // Peek the next byte of buffer into scratch array. + adtsScratch.data[0] = buffer.data[buffer.getPosition()]; + + adtsScratch.setPosition(2); + int currentFrameSampleRateIndex = adtsScratch.readBits(4); + if (firstFrameSampleRateIndex != C.INDEX_UNSET + && currentFrameSampleRateIndex != firstFrameSampleRateIndex) { + // Invalid header. + resetSync(); + return; + } + + if (!foundFirstFrame) { + foundFirstFrame = true; + firstFrameVersion = currentFrameVersion; + firstFrameSampleRateIndex = currentFrameSampleRateIndex; + } + setReadingAdtsHeaderState(); + } + + /** + * Checks whether a candidate SYNC word position is likely to be the position of a real SYNC word. + * The caller must check that the first byte of the SYNC word is 0xFF before calling this method. + * This method performs the following checks: + * + *

    + *
  • The MPEG version of this frame must match the previously detected version. + *
  • The sample rate index of this frame must match the previously detected sample rate index. + *
  • The frame size must be at least 7 bytes + *
  • The bytes following the frame must be either another SYNC word with the same MPEG + * version, or the start of an ID3 header. + *
+ * + * With the exception of the first check, if there is insufficient data in the buffer then checks + * are optimistically skipped and {@code true} is returned. + * + * @param pesBuffer The buffer containing at data to check. + * @param syncPositionCandidate The candidate SYNC word position. May be -1 if the first byte of + * the candidate was the last byte of the previously consumed buffer. + * @return True if all checks were passed or skipped, indicating the position is likely to be the + * position of a real SYNC word. False otherwise. + */ + private boolean checkSyncPositionValid(ParsableByteArray pesBuffer, int syncPositionCandidate) { + pesBuffer.setPosition(syncPositionCandidate + 1); + if (!tryRead(pesBuffer, adtsScratch.data, 1)) { + return false; + } + + // The MPEG version of this frame must match the previously detected version. + adtsScratch.setPosition(4); + int currentFrameVersion = adtsScratch.readBits(1); + if (firstFrameVersion != VERSION_UNSET && currentFrameVersion != firstFrameVersion) { + return false; + } + + // The sample rate index of this frame must match the previously detected sample rate index. + if (firstFrameSampleRateIndex != C.INDEX_UNSET) { + if (!tryRead(pesBuffer, adtsScratch.data, 1)) { + // Insufficient data for further checks. + return true; + } + adtsScratch.setPosition(2); + int currentFrameSampleRateIndex = adtsScratch.readBits(4); + if (currentFrameSampleRateIndex != firstFrameSampleRateIndex) { + return false; + } + pesBuffer.setPosition(syncPositionCandidate + 2); + } + + // The frame size must be at least 7 bytes. + if (!tryRead(pesBuffer, adtsScratch.data, 4)) { + // Insufficient data for further checks. + return true; + } + adtsScratch.setPosition(14); + int frameSize = adtsScratch.readBits(13); + if (frameSize < 7) { + return false; + } + + // The bytes following the frame must be either another SYNC word with the same MPEG version, or + // the start of an ID3 header. + byte[] data = pesBuffer.data; + int dataLimit = pesBuffer.limit(); + int nextSyncPosition = syncPositionCandidate + frameSize; + if (nextSyncPosition >= dataLimit) { + // Insufficient data for further checks. + return true; + } + if (data[nextSyncPosition] == (byte) 0xFF) { + if (nextSyncPosition + 1 == dataLimit) { + // Insufficient data for further checks. + return true; + } + return isAdtsSyncBytes((byte) 0xFF, data[nextSyncPosition + 1]) + && ((data[nextSyncPosition + 1] & 0x8) >> 3) == currentFrameVersion; + } else { + if (data[nextSyncPosition] != 'I') { + return false; + } + if (nextSyncPosition + 1 == dataLimit) { + // Insufficient data for further checks. + return true; + } + if (data[nextSyncPosition + 1] != 'D') { + return false; + } + if (nextSyncPosition + 2 == dataLimit) { + // Insufficient data for further checks. + return true; + } + return data[nextSyncPosition + 2] == '3'; + } + } + + private boolean isAdtsSyncBytes(byte firstByte, byte secondByte) { + int syncWord = (firstByte & 0xFF) << 8 | (secondByte & 0xFF); + return isAdtsSyncWord(syncWord); + } + + /** Reads {@code targetLength} bytes into target, and returns whether the read succeeded. */ + private boolean tryRead(ParsableByteArray source, byte[] target, int targetLength) { + if (source.bytesLeft() < targetLength) { + return false; + } + source.readBytes(target, /* offset= */ 0, targetLength); + return true; + } + /** * Parses the Id3 header. */ @@ -276,7 +466,7 @@ public final class AdtsReader implements ElementaryStreamReader { /** * Parses the sample header. */ - private void parseAdtsHeader() { + private void parseAdtsHeader() throws ParserException { adtsScratch.setPosition(0); if (!hasOutputFormat) { @@ -295,12 +485,12 @@ public final class AdtsReader implements ElementaryStreamReader { audioObjectType = 2; } - int sampleRateIndex = adtsScratch.readBits(4); - adtsScratch.skipBits(1); + adtsScratch.skipBits(5); int channelConfig = adtsScratch.readBits(3); - byte[] audioSpecificConfig = CodecSpecificDataUtil.buildAacAudioSpecificConfig( - audioObjectType, sampleRateIndex, channelConfig); + byte[] audioSpecificConfig = + CodecSpecificDataUtil.buildAacAudioSpecificConfig( + audioObjectType, firstFrameSampleRateIndex, channelConfig); Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( audioSpecificConfig); diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 314c1f620696..cfbc64d2ee3c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -15,12 +15,14 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; -import android.support.annotation.IntDef; import android.util.SparseArray; +import androidx.annotation.IntDef; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea.Cea708InitializationData; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -28,24 +30,69 @@ import java.util.Collections; import java.util.List; /** - * Default implementation for {@link TsPayloadReader.Factory}. + * Default {@link TsPayloadReader.Factory} implementation. */ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Factory { /** - * Flags controlling elementary stream readers' behavior. + * Flags controlling elementary stream readers' behavior. Possible flag values are {@link + * #FLAG_ALLOW_NON_IDR_KEYFRAMES}, {@link #FLAG_IGNORE_AAC_STREAM}, {@link + * #FLAG_IGNORE_H264_STREAM}, {@link #FLAG_DETECT_ACCESS_UNITS}, {@link + * #FLAG_IGNORE_SPLICE_INFO_STREAM}, {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} and {@link + * #FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_ALLOW_NON_IDR_KEYFRAMES, FLAG_IGNORE_AAC_STREAM, - FLAG_IGNORE_H264_STREAM, FLAG_DETECT_ACCESS_UNITS, FLAG_IGNORE_SPLICE_INFO_STREAM, - FLAG_OVERRIDE_CAPTION_DESCRIPTORS}) + @IntDef( + flag = true, + value = { + FLAG_ALLOW_NON_IDR_KEYFRAMES, + FLAG_IGNORE_AAC_STREAM, + FLAG_IGNORE_H264_STREAM, + FLAG_DETECT_ACCESS_UNITS, + FLAG_IGNORE_SPLICE_INFO_STREAM, + FLAG_OVERRIDE_CAPTION_DESCRIPTORS, + FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS + }) public @interface Flags {} + + /** + * When extracting H.264 samples, whether to treat samples consisting of non-IDR I slices as + * synchronization samples (key-frames). + */ public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1; + /** + * Prevents the creation of {@link AdtsReader} and {@link LatmReader} instances. This flag should + * be enabled if the transport stream contains no packets for an AAC elementary stream that is + * declared in the PMT. + */ public static final int FLAG_IGNORE_AAC_STREAM = 1 << 1; + /** + * Prevents the creation of {@link H264Reader} instances. This flag should be enabled if the + * transport stream contains no packets for an H.264 elementary stream that is declared in the + * PMT. + */ public static final int FLAG_IGNORE_H264_STREAM = 1 << 2; + /** + * When extracting H.264 samples, whether to split the input stream into access units (samples) + * based on slice headers. This flag should be disabled if the stream contains access unit + * delimiters (AUDs). + */ public static final int FLAG_DETECT_ACCESS_UNITS = 1 << 3; + /** Prevents the creation of {@link SpliceInfoSectionReader} instances. */ public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 1 << 4; + /** + * Whether the list of {@code closedCaptionFormats} passed to {@link + * DefaultTsPayloadReaderFactory#DefaultTsPayloadReaderFactory(int, List)} should be used in spite + * of any closed captions service descriptors. If this flag is disabled, {@code + * closedCaptionFormats} will be ignored if the PMT contains closed captions service descriptors. + */ public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5; + /** + * Sets whether HDMV DTS audio streams will be handled. If this flag is set, SCTE subtitles will + * not be detected, as they share the same elementary stream type as HDMV DTS. + */ + public static final int FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS = 1 << 6; private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86; @@ -61,7 +108,10 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact * readers. */ public DefaultTsPayloadReaderFactory(@Flags int flags) { - this(flags, Collections.emptyList()); + this( + flags, + Collections.singletonList( + Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null))); } /** @@ -76,10 +126,6 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact */ public DefaultTsPayloadReaderFactory(@Flags int flags, List closedCaptionFormats) { this.flags = flags; - if (!isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS) && closedCaptionFormats.isEmpty()) { - closedCaptionFormats = Collections.singletonList(Format.createTextSampleFormat(null, - MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); - } this.closedCaptionFormats = closedCaptionFormats; } @@ -89,22 +135,32 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact } @Override + @SuppressWarnings("fallthrough") public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { switch (streamType) { case TsExtractor.TS_STREAM_TYPE_MPA: case TsExtractor.TS_STREAM_TYPE_MPA_LSF: return new PesReader(new MpegAudioReader(esInfo.language)); - case TsExtractor.TS_STREAM_TYPE_AAC: + case TsExtractor.TS_STREAM_TYPE_AAC_ADTS: return isSet(FLAG_IGNORE_AAC_STREAM) ? null : new PesReader(new AdtsReader(false, esInfo.language)); + case TsExtractor.TS_STREAM_TYPE_AAC_LATM: + return isSet(FLAG_IGNORE_AAC_STREAM) + ? null : new PesReader(new LatmReader(esInfo.language)); case TsExtractor.TS_STREAM_TYPE_AC3: case TsExtractor.TS_STREAM_TYPE_E_AC3: return new PesReader(new Ac3Reader(esInfo.language)); - case TsExtractor.TS_STREAM_TYPE_DTS: + case TsExtractor.TS_STREAM_TYPE_AC4: + return new PesReader(new Ac4Reader(esInfo.language)); case TsExtractor.TS_STREAM_TYPE_HDMV_DTS: + if (!isSet(FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS)) { + return null; + } + // Fall through. + case TsExtractor.TS_STREAM_TYPE_DTS: return new PesReader(new DtsReader(esInfo.language)); case TsExtractor.TS_STREAM_TYPE_H262: - return new PesReader(new H262Reader()); + return new PesReader(new H262Reader(buildUserDataReader(esInfo))); case TsExtractor.TS_STREAM_TYPE_H264: return isSet(FLAG_IGNORE_H264_STREAM) ? null : new PesReader(new H264Reader(buildSeiReader(esInfo), @@ -134,8 +190,34 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact * @return A {@link SeiReader} for closed caption tracks. */ private SeiReader buildSeiReader(EsInfo esInfo) { + return new SeiReader(getClosedCaptionFormats(esInfo)); + } + + /** + * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link UserDataReader} for + * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a + * {@link UserDataReader} for the declared formats, or {@link #closedCaptionFormats} if the + * descriptor is not present. + * + * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}. + * @return A {@link UserDataReader} for closed caption tracks. + */ + private UserDataReader buildUserDataReader(EsInfo esInfo) { + return new UserDataReader(getClosedCaptionFormats(esInfo)); + } + + /** + * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link List} of {@link + * #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a {@link + * List} for the declared formats, or {@link #closedCaptionFormats} if the descriptor is + * not present. + * + * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}. + * @return A {@link List} containing list of closed caption formats. + */ + private List getClosedCaptionFormats(EsInfo esInfo) { if (isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS)) { - return new SeiReader(closedCaptionFormats); + return closedCaptionFormats; } ParsableByteArray scratchDescriptorData = new ParsableByteArray(esInfo.descriptorBytes); List closedCaptionFormats = this.closedCaptionFormats; @@ -160,21 +242,42 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact mimeType = MimeTypes.APPLICATION_CEA608; accessibilityChannel = 1; } - closedCaptionFormats.add(Format.createTextSampleFormat(null, mimeType, null, - Format.NO_VALUE, 0, language, accessibilityChannel, null)); - // Skip easy_reader(1), wide_aspect_ratio(1), reserved(14). - scratchDescriptorData.skipBytes(2); + + // easy_reader(1), wide_aspect_ratio(1), reserved(6). + byte flags = (byte) scratchDescriptorData.readUnsignedByte(); + // Skip reserved (8). + scratchDescriptorData.skipBytes(1); + + List initializationData = null; + // The wide_aspect_ratio flag only has meaning for CEA-708. + if (isDigital) { + boolean isWideAspectRatio = (flags & 0x40) != 0; + initializationData = Cea708InitializationData.buildData(isWideAspectRatio); + } + + closedCaptionFormats.add( + Format.createTextSampleFormat( + /* id= */ null, + mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* selectionFlags= */ 0, + language, + accessibilityChannel, + /* drmInitData= */ null, + Format.OFFSET_SAMPLE_RELATIVE, + initializationData)); } } else { // Unknown descriptor. Ignore. } scratchDescriptorData.setPosition(nextDescriptorPosition); } - return new SeiReader(closedCaptionFormats); + + return closedCaptionFormats; } private boolean isSet(@Flags int flag) { return (flags & flag) != 0; } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java index 4b7bc78c7ed4..a4205add7bff 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -32,9 +32,7 @@ public final class DtsReader implements ElementaryStreamReader { private static final int STATE_READING_HEADER = 1; private static final int STATE_READING_SAMPLE = 2; - private static final int HEADER_SIZE = 15; - private static final int SYNC_VALUE = 0x7FFE8001; - private static final int SYNC_VALUE_SIZE = 4; + private static final int HEADER_SIZE = 18; private final ParsableByteArray headerScratchBytes; private final String language; @@ -63,10 +61,6 @@ public final class DtsReader implements ElementaryStreamReader { */ public DtsReader(String language) { headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]); - headerScratchBytes.data[0] = (byte) ((SYNC_VALUE >> 24) & 0xFF); - headerScratchBytes.data[1] = (byte) ((SYNC_VALUE >> 16) & 0xFF); - headerScratchBytes.data[2] = (byte) ((SYNC_VALUE >> 8) & 0xFF); - headerScratchBytes.data[3] = (byte) (SYNC_VALUE & 0xFF); state = STATE_FINDING_SYNC; this.language = language; } @@ -86,7 +80,7 @@ public final class DtsReader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { timeUs = pesTimeUs; } @@ -96,7 +90,6 @@ public final class DtsReader implements ElementaryStreamReader { switch (state) { case STATE_FINDING_SYNC: if (skipToNextSync(data)) { - bytesRead = SYNC_VALUE_SIZE; state = STATE_READING_HEADER; } break; @@ -118,6 +111,8 @@ public final class DtsReader implements ElementaryStreamReader { state = STATE_FINDING_SYNC; } break; + default: + throw new IllegalStateException(); } } } @@ -154,7 +149,12 @@ public final class DtsReader implements ElementaryStreamReader { while (pesBuffer.bytesLeft() > 0) { syncBytes <<= 8; syncBytes |= pesBuffer.readUnsignedByte(); - if (syncBytes == SYNC_VALUE) { + if (DtsUtil.isSyncWord(syncBytes)) { + headerScratchBytes.data[0] = (byte) ((syncBytes >> 24) & 0xFF); + headerScratchBytes.data[1] = (byte) ((syncBytes >> 16) & 0xFF); + headerScratchBytes.data[2] = (byte) ((syncBytes >> 8) & 0xFF); + headerScratchBytes.data[3] = (byte) (syncBytes & 0xFF); + bytesRead = 4; syncBytes = 0; return true; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java index 3ca6bd8934ae..aceab78bf00e 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java @@ -15,6 +15,8 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; + import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -58,16 +60,23 @@ public final class DvbSubtitleReader implements ElementaryStreamReader { DvbSubtitleInfo subtitleInfo = subtitleInfos.get(i); idGenerator.generateNewId(); TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); - output.format(Format.createImageSampleFormat(idGenerator.getFormatId(), - MimeTypes.APPLICATION_DVBSUBS, null, Format.NO_VALUE, - Collections.singletonList(subtitleInfo.initializationData), subtitleInfo.language, null)); + output.format( + Format.createImageSampleFormat( + idGenerator.getFormatId(), + MimeTypes.APPLICATION_DVBSUBS, + null, + Format.NO_VALUE, + 0, + Collections.singletonList(subtitleInfo.initializationData), + subtitleInfo.language, + null)); outputs[i] = output; } } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { - if (!dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) { return; } writingSample = true; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java index 218fee59d1a3..edd33d02c22e 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java @@ -15,6 +15,7 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; @@ -42,16 +43,17 @@ public interface ElementaryStreamReader { * Called when a packet starts. * * @param pesTimeUs The timestamp associated with the packet. - * @param dataAlignmentIndicator The data alignment indicator associated with the packet. + * @param flags See {@link TsPayloadReader.Flags}. */ - void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator); + void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags); /** * Consumes (possibly partial) data from the current packet. * * @param data The data to consume. + * @throws ParserException If the data could not be parsed. */ - void consume(ParsableByteArray data); + void consume(ParsableByteArray data) throws ParserException; /** * Called when a packet ends. diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java index 56c08b2126bd..576607366eba 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -36,6 +36,7 @@ public final class H262Reader implements ElementaryStreamReader { private static final int START_SEQUENCE_HEADER = 0xB3; private static final int START_EXTENSION = 0xB5; private static final int START_GROUP = 0xB8; + private static final int START_USER_DATA = 0xB2; private String formatId; private TrackOutput output; @@ -48,33 +49,51 @@ public final class H262Reader implements ElementaryStreamReader { private boolean hasOutputFormat; private long frameDurationUs; + private final UserDataReader userDataReader; + private final ParsableByteArray userDataParsable; + // State that should be reset on seek. private final boolean[] prefixFlags; private final CsdBuffer csdBuffer; - private boolean foundFirstFrameInGroup; + private final NalUnitTargetBuffer userData; private long totalBytesWritten; + private boolean startedFirstSample; // Per packet state that gets reset at the start of each packet. private long pesTimeUs; - private boolean pesPtsUsAvailable; - // Per sample state that gets reset at the start of each frame. - private boolean isKeyframe; - private long framePosition; - private long frameTimeUs; + // Per sample state that gets reset at the start of each sample. + private long samplePosition; + private long sampleTimeUs; + private boolean sampleIsKeyframe; + private boolean sampleHasPicture; public H262Reader() { + this(null); + } + + /* package */ H262Reader(UserDataReader userDataReader) { + this.userDataReader = userDataReader; prefixFlags = new boolean[4]; csdBuffer = new CsdBuffer(128); + if (userDataReader != null) { + userData = new NalUnitTargetBuffer(START_USER_DATA, 128); + userDataParsable = new ParsableByteArray(); + } else { + userData = null; + userDataParsable = null; + } } @Override public void seek() { NalUnitUtil.clearPrefixFlags(prefixFlags); csdBuffer.reset(); - pesPtsUsAvailable = false; - foundFirstFrameInGroup = false; + if (userDataReader != null) { + userData.reset(); + } totalBytesWritten = 0; + startedFirstSample = false; } @Override @@ -82,14 +101,15 @@ public final class H262Reader implements ElementaryStreamReader { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); + if (userDataReader != null) { + userDataReader.createTracks(extractorOutput, idGenerator); + } } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { - pesPtsUsAvailable = pesTimeUs != C.TIME_UNSET; - if (pesPtsUsAvailable) { - this.pesTimeUs = pesTimeUs; - } + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + // TODO (Internal b/32267012): Consider using random access indicator. + this.pesTimeUs = pesTimeUs; } @Override @@ -102,30 +122,32 @@ public final class H262Reader implements ElementaryStreamReader { totalBytesWritten += data.bytesLeft(); output.sampleData(data, data.bytesLeft()); - int searchOffset = offset; while (true) { - int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, searchOffset, limit, prefixFlags); + int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags); if (startCodeOffset == limit) { // We've scanned to the end of the data without finding another start code. if (!hasOutputFormat) { csdBuffer.onData(dataArray, offset, limit); } + if (userDataReader != null) { + userData.appendToNalUnit(dataArray, offset, limit); + } return; } // We've found a start code with the following value. int startCodeValue = data.data[startCodeOffset + 3] & 0xFF; + // This is the number of bytes from the current offset to the start of the next start + // code. It may be negative if the start code started in the previously consumed data. + int lengthToStartCode = startCodeOffset - offset; if (!hasOutputFormat) { - // This is the number of bytes from the current offset to the start of the next start - // code. It may be negative if the start code started in the previously consumed data. - int lengthToStartCode = startCodeOffset - offset; if (lengthToStartCode > 0) { csdBuffer.onData(dataArray, offset, startCodeOffset); } // This is the number of bytes belonging to the next start code that have already been - // passed to csdDataTargetBuffer. + // passed to csdBuffer. int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0; if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) { // The csd data is complete, so we can decode and output the media format. @@ -135,28 +157,47 @@ public final class H262Reader implements ElementaryStreamReader { hasOutputFormat = true; } } - - if (hasOutputFormat && (startCodeValue == START_GROUP || startCodeValue == START_PICTURE)) { - int bytesWrittenPastStartCode = limit - startCodeOffset; - if (foundFirstFrameInGroup) { - @C.BufferFlags int flags = isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; - int size = (int) (totalBytesWritten - framePosition) - bytesWrittenPastStartCode; - output.sampleMetadata(frameTimeUs, flags, size, bytesWrittenPastStartCode, null); - isKeyframe = false; + if (userDataReader != null) { + int bytesAlreadyPassed = 0; + if (lengthToStartCode > 0) { + userData.appendToNalUnit(dataArray, offset, startCodeOffset); + } else { + bytesAlreadyPassed = -lengthToStartCode; } - if (startCodeValue == START_GROUP) { - foundFirstFrameInGroup = false; - isKeyframe = true; - } else /* startCodeValue == START_PICTURE */ { - frameTimeUs = pesPtsUsAvailable ? pesTimeUs : (frameTimeUs + frameDurationUs); - framePosition = totalBytesWritten - bytesWrittenPastStartCode; - pesPtsUsAvailable = false; - foundFirstFrameInGroup = true; + + if (userData.endNalUnit(bytesAlreadyPassed)) { + int unescapedLength = NalUnitUtil.unescapeStream(userData.nalData, userData.nalLength); + userDataParsable.reset(userData.nalData, unescapedLength); + userDataReader.consume(sampleTimeUs, userDataParsable); + } + + if (startCodeValue == START_USER_DATA && data.data[startCodeOffset + 2] == 0x1) { + userData.startNalUnit(startCodeValue); } } + if (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER) { + int bytesWrittenPastStartCode = limit - startCodeOffset; + if (startedFirstSample && sampleHasPicture && hasOutputFormat) { + // Output the sample. + @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; + int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastStartCode; + output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastStartCode, null); + } + if (!startedFirstSample || sampleHasPicture) { + // Start the next sample. + samplePosition = totalBytesWritten - bytesWrittenPastStartCode; + sampleTimeUs = pesTimeUs != C.TIME_UNSET ? pesTimeUs + : (startedFirstSample ? (sampleTimeUs + frameDurationUs) : 0); + sampleIsKeyframe = false; + pesTimeUs = C.TIME_UNSET; + startedFirstSample = true; + } + sampleHasPicture = startCodeValue == START_PICTURE; + } else if (startCodeValue == START_GROUP) { + sampleIsKeyframe = true; + } - offset = startCodeOffset; - searchOffset = offset + 3; + offset = startCodeOffset + 3; } } @@ -221,6 +262,8 @@ public final class H262Reader implements ElementaryStreamReader { private static final class CsdBuffer { + private static final byte[] START_CODE = new byte[] {0, 0, 1}; + private boolean isFilling; public int length; @@ -244,24 +287,25 @@ public final class H262Reader implements ElementaryStreamReader { * Called when a start code is encountered in the stream. * * @param startCodeValue The start code value. - * @param bytesAlreadyPassed The number of bytes of the start code that have already been - * passed to {@link #onData(byte[], int, int)}, or 0. + * @param bytesAlreadyPassed The number of bytes of the start code that have been passed to + * {@link #onData(byte[], int, int)}, or 0. * @return Whether the csd data is now complete. If true is returned, neither - * this method or {@link #onData(byte[], int, int)} should be called again without an + * this method nor {@link #onData(byte[], int, int)} should be called again without an * interleaving call to {@link #reset()}. */ public boolean onStartCode(int startCodeValue, int bytesAlreadyPassed) { if (isFilling) { + length -= bytesAlreadyPassed; if (sequenceExtensionPosition == 0 && startCodeValue == START_EXTENSION) { sequenceExtensionPosition = length; } else { - length -= bytesAlreadyPassed; isFilling = false; return true; } } else if (startCodeValue == START_SEQUENCE_HEADER) { isFilling = true; } + onData(START_CODE, 0, START_CODE.length); return false; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java index ec09c34c1432..164c115159ab 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -15,12 +15,15 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR; + import android.util.SparseArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil.SpsData; @@ -55,9 +58,12 @@ public final class H264Reader implements ElementaryStreamReader { // State that should not be reset on seek. private boolean hasOutputFormat; - // Per packet state that gets reset at the start of each packet. + // Per PES packet state that gets reset at the start of each PES packet. private long pesTimeUs; + // State inherited from the TS packet header. + private boolean randomAccessIndicator; + // Scratch variables to avoid allocations. private final ParsableByteArray seiWrapper; @@ -87,6 +93,7 @@ public final class H264Reader implements ElementaryStreamReader { sei.reset(); sampleReader.reset(); totalBytesWritten = 0; + randomAccessIndicator = false; } @Override @@ -99,8 +106,9 @@ public final class H264Reader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { this.pesTimeUs = pesTimeUs; + randomAccessIndicator |= (flags & FLAG_RANDOM_ACCESS_INDICATOR) != 0; } @Override @@ -180,9 +188,23 @@ public final class H264Reader implements ElementaryStreamReader { initializationData.add(Arrays.copyOf(pps.nalData, pps.nalLength)); NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength); NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength); - output.format(Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H264, null, - Format.NO_VALUE, Format.NO_VALUE, spsData.width, spsData.height, Format.NO_VALUE, - initializationData, Format.NO_VALUE, spsData.pixelWidthAspectRatio, null)); + output.format( + Format.createVideoSampleFormat( + formatId, + MimeTypes.VIDEO_H264, + CodecSpecificDataUtil.buildAvcCodecString( + spsData.profileIdc, + spsData.constraintsFlagsAndReservedZero2Bits, + spsData.levelIdc), + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + spsData.width, + spsData.height, + /* frameRate= */ Format.NO_VALUE, + initializationData, + /* rotationDegrees= */ Format.NO_VALUE, + spsData.pixelWidthAspectRatio, + /* drmInitData= */ null)); hasOutputFormat = true; sampleReader.putSps(spsData); sampleReader.putPps(ppsData); @@ -205,12 +227,17 @@ public final class H264Reader implements ElementaryStreamReader { seiWrapper.setPosition(4); // NAL prefix and nal_unit() header. seiReader.consume(pesTimeUs, seiWrapper); } - sampleReader.endNalUnit(position, offset); + boolean sampleIsKeyFrame = + sampleReader.endNalUnit(position, offset, hasOutputFormat, randomAccessIndicator); + if (sampleIsKeyFrame) { + // This is either an IDR frame or the first I-frame since the random access indicator, so mark + // it as a keyframe. Clear the flag so that subsequent non-IDR I-frames are not marked as + // keyframes until we see another random access indicator. + randomAccessIndicator = false; + } } - /** - * Consumes a stream of NAL units and outputs samples. - */ + /** Consumes a stream of NAL units and outputs samples. */ private static final class SampleReader { private static final int DEFAULT_BUFFER_SIZE = 128; @@ -316,7 +343,7 @@ public final class H264Reader implements ElementaryStreamReader { if (!bitArray.canReadBits(8)) { return; } - bitArray.skipBits(1); // forbidden_zero_bit + bitArray.skipBit(); // forbidden_zero_bit int nalRefIdc = bitArray.readBits(2); bitArray.skipBits(5); // nal_unit_type @@ -415,11 +442,12 @@ public final class H264Reader implements ElementaryStreamReader { isFilling = false; } - public void endNalUnit(long position, int offset) { + public boolean endNalUnit( + long position, int offset, boolean hasOutputFormat, boolean randomAccessIndicator) { if (nalUnitType == NAL_UNIT_TYPE_AUD || (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) { // If the NAL unit ending is the start of a new sample, output the previous one. - if (readingSample) { + if (hasOutputFormat && readingSample) { int nalUnitLength = (int) (position - nalUnitStartPosition); outputSample(offset + nalUnitLength); } @@ -428,8 +456,12 @@ public final class H264Reader implements ElementaryStreamReader { sampleIsKeyframe = false; readingSample = true; } - sampleIsKeyframe |= nalUnitType == NAL_UNIT_TYPE_IDR || (allowNonIdrKeyframes - && nalUnitType == NAL_UNIT_TYPE_NON_IDR && sliceHeader.isISlice()); + boolean treatIFrameAsKeyframe = + allowNonIdrKeyframes ? sliceHeader.isISlice() : randomAccessIndicator; + sampleIsKeyframe |= + nalUnitType == NAL_UNIT_TYPE_IDR + || (treatIFrameAsKeyframe && nalUnitType == NAL_UNIT_TYPE_NON_IDR); + return sampleIsKeyframe; } private void outputSample(int offset) { @@ -471,10 +503,21 @@ public final class H264Reader implements ElementaryStreamReader { hasSliceType = true; } - public void setAll(SpsData spsData, int nalRefIdc, int sliceType, int frameNum, - int picParameterSetId, boolean fieldPicFlag, boolean bottomFieldFlagPresent, - boolean bottomFieldFlag, boolean idrPicFlag, int idrPicId, int picOrderCntLsb, - int deltaPicOrderCntBottom, int deltaPicOrderCnt0, int deltaPicOrderCnt1) { + public void setAll( + SpsData spsData, + int nalRefIdc, + int sliceType, + int frameNum, + int picParameterSetId, + boolean fieldPicFlag, + boolean bottomFieldFlagPresent, + boolean bottomFieldFlag, + boolean idrPicFlag, + int idrPicId, + int picOrderCntLsb, + int deltaPicOrderCntBottom, + int deltaPicOrderCnt0, + int deltaPicOrderCnt1) { this.spsData = spsData; this.nalRefIdc = nalRefIdc; this.sliceType = sliceType; @@ -499,23 +542,26 @@ public final class H264Reader implements ElementaryStreamReader { private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) { // See ISO 14496-10 subsection 7.4.1.2.4. - return isComplete && (!other.isComplete || frameNum != other.frameNum - || picParameterSetId != other.picParameterSetId || fieldPicFlag != other.fieldPicFlag - || (bottomFieldFlagPresent && other.bottomFieldFlagPresent - && bottomFieldFlag != other.bottomFieldFlag) - || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0)) - || (spsData.picOrderCountType == 0 && other.spsData.picOrderCountType == 0 - && (picOrderCntLsb != other.picOrderCntLsb - || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom)) - || (spsData.picOrderCountType == 1 && other.spsData.picOrderCountType == 1 - && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0 - || deltaPicOrderCnt1 != other.deltaPicOrderCnt1)) - || idrPicFlag != other.idrPicFlag - || (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId)); + return isComplete + && (!other.isComplete + || frameNum != other.frameNum + || picParameterSetId != other.picParameterSetId + || fieldPicFlag != other.fieldPicFlag + || (bottomFieldFlagPresent + && other.bottomFieldFlagPresent + && bottomFieldFlag != other.bottomFieldFlag) + || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0)) + || (spsData.picOrderCountType == 0 + && other.spsData.picOrderCountType == 0 + && (picOrderCntLsb != other.picOrderCntLsb + || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom)) + || (spsData.picOrderCountType == 1 + && other.spsData.picOrderCountType == 1 + && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0 + || deltaPicOrderCnt1 != other.deltaPicOrderCnt1)) + || idrPicFlag != other.idrPicFlag + || (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId)); } - } - } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 1bd13bdb38c8..6aa7c5d71d25 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -15,12 +15,12 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; -import android.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; @@ -104,7 +104,8 @@ public final class H265Reader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + // TODO (Internal b/32267012): Consider using random access indicator. this.pesTimeUs = pesTimeUs; } @@ -225,7 +226,7 @@ public final class H265Reader implements ElementaryStreamReader { ParsableNalUnitBitArray bitArray = new ParsableNalUnitBitArray(sps.nalData, 0, sps.nalLength); bitArray.skipBits(40 + 4); // NAL header, sps_video_parameter_set_id int maxSubLayersMinus1 = bitArray.readBits(3); - bitArray.skipBits(1); // sps_temporal_id_nesting_flag + bitArray.skipBit(); // sps_temporal_id_nesting_flag // profile_tier_level(1, sps_max_sub_layers_minus1) bitArray.skipBits(88); // if (profilePresentFlag) {...} @@ -247,7 +248,7 @@ public final class H265Reader implements ElementaryStreamReader { bitArray.readUnsignedExpGolombCodedInt(); // sps_seq_parameter_set_id int chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt(); if (chromaFormatIdc == 3) { - bitArray.skipBits(1); // separate_colour_plane_flag + bitArray.skipBit(); // separate_colour_plane_flag } int picWidthInLumaSamples = bitArray.readUnsignedExpGolombCodedInt(); int picHeightInLumaSamples = bitArray.readUnsignedExpGolombCodedInt(); @@ -288,7 +289,7 @@ public final class H265Reader implements ElementaryStreamReader { bitArray.skipBits(8); bitArray.readUnsignedExpGolombCodedInt(); // log2_min_pcm_luma_coding_block_size_minus3 bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_pcm_luma_coding_block_size - bitArray.skipBits(1); // pcm_loop_filter_disabled_flag + bitArray.skipBit(); // pcm_loop_filter_disabled_flag } // Skips all short term reference picture sets. skipShortTermRefPicSets(bitArray); @@ -365,11 +366,11 @@ public final class H265Reader implements ElementaryStreamReader { interRefPicSetPredictionFlag = bitArray.readBit(); } if (interRefPicSetPredictionFlag) { - bitArray.skipBits(1); // delta_rps_sign + bitArray.skipBit(); // delta_rps_sign bitArray.readUnsignedExpGolombCodedInt(); // abs_delta_rps_minus1 for (int j = 0; j <= previousNumDeltaPocs; j++) { if (bitArray.readBit()) { // used_by_curr_pic_flag[j] - bitArray.skipBits(1); // use_delta_flag[j] + bitArray.skipBit(); // use_delta_flag[j] } } } else { @@ -378,11 +379,11 @@ public final class H265Reader implements ElementaryStreamReader { previousNumDeltaPocs = numNegativePics + numPositivePics; for (int i = 0; i < numNegativePics; i++) { bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s0_minus1[i] - bitArray.skipBits(1); // used_by_curr_pic_s0_flag[i] + bitArray.skipBit(); // used_by_curr_pic_s0_flag[i] } for (int i = 0; i < numPositivePics; i++) { bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s1_minus1[i] - bitArray.skipBits(1); // used_by_curr_pic_s1_flag[i] + bitArray.skipBit(); // used_by_curr_pic_s1_flag[i] } } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index 67c7206a320a..da63e143c2c8 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -15,12 +15,15 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; -import android.util.Log; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; + import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; @@ -31,8 +34,6 @@ public final class Id3Reader implements ElementaryStreamReader { private static final String TAG = "Id3Reader"; - private static final int ID3_HEADER_SIZE = 10; - private final ParsableByteArray id3Header; private TrackOutput output; @@ -46,7 +47,7 @@ public final class Id3Reader implements ElementaryStreamReader { private int sampleBytesRead; public Id3Reader() { - id3Header = new ParsableByteArray(ID3_HEADER_SIZE); + id3Header = new ParsableByteArray(ID3_HEADER_LENGTH); } @Override @@ -63,8 +64,8 @@ public final class Id3Reader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { - if (!dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) { return; } writingSample = true; @@ -79,12 +80,12 @@ public final class Id3Reader implements ElementaryStreamReader { return; } int bytesAvailable = data.bytesLeft(); - if (sampleBytesRead < ID3_HEADER_SIZE) { + if (sampleBytesRead < ID3_HEADER_LENGTH) { // We're still reading the ID3 header. - int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_SIZE - sampleBytesRead); + int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_LENGTH - sampleBytesRead); System.arraycopy(data.data, data.getPosition(), id3Header.data, sampleBytesRead, headerBytesAvailable); - if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_SIZE) { + if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_LENGTH) { // We've finished reading the ID3 header. Extract the sample size. id3Header.setPosition(0); if ('I' != id3Header.readUnsignedByte() || 'D' != id3Header.readUnsignedByte() @@ -94,7 +95,7 @@ public final class Id3Reader implements ElementaryStreamReader { return; } id3Header.skipBytes(3); // version (2) + flags (1) - sampleSize = ID3_HEADER_SIZE + id3Header.readSynchSafeInt(); + sampleSize = ID3_HEADER_LENGTH + id3Header.readSynchSafeInt(); } } // Write data to the output. diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/LatmReader.java new file mode 100644 index 000000000000..1a41adfa694c --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/LatmReader.java @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import android.util.Pair; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.Collections; + +/** + * Parses and extracts samples from an AAC/LATM elementary stream. + */ +public final class LatmReader implements ElementaryStreamReader { + + private static final int STATE_FINDING_SYNC_1 = 0; + private static final int STATE_FINDING_SYNC_2 = 1; + private static final int STATE_READING_HEADER = 2; + private static final int STATE_READING_SAMPLE = 3; + + private static final int INITIAL_BUFFER_SIZE = 1024; + private static final int SYNC_BYTE_FIRST = 0x56; + private static final int SYNC_BYTE_SECOND = 0xE0; + + private final String language; + private final ParsableByteArray sampleDataBuffer; + private final ParsableBitArray sampleBitArray; + + // Track output info. + private TrackOutput output; + private Format format; + private String formatId; + + // Parser state info. + private int state; + private int bytesRead; + private int sampleSize; + private int secondHeaderByte; + private long timeUs; + + // Container data. + private boolean streamMuxRead; + private int audioMuxVersionA; + private int numSubframes; + private int frameLengthType; + private boolean otherDataPresent; + private long otherDataLenBits; + private int sampleRateHz; + private long sampleDurationUs; + private int channelCount; + + /** + * @param language Track language. + */ + public LatmReader(@Nullable String language) { + this.language = language; + sampleDataBuffer = new ParsableByteArray(INITIAL_BUFFER_SIZE); + sampleBitArray = new ParsableBitArray(sampleDataBuffer.data); + } + + @Override + public void seek() { + state = STATE_FINDING_SYNC_1; + streamMuxRead = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); + formatId = idGenerator.getFormatId(); + } + + @Override + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) throws ParserException { + int bytesToRead; + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_SYNC_1: + if (data.readUnsignedByte() == SYNC_BYTE_FIRST) { + state = STATE_FINDING_SYNC_2; + } + break; + case STATE_FINDING_SYNC_2: + int secondByte = data.readUnsignedByte(); + if ((secondByte & SYNC_BYTE_SECOND) == SYNC_BYTE_SECOND) { + secondHeaderByte = secondByte; + state = STATE_READING_HEADER; + } else if (secondByte != SYNC_BYTE_FIRST) { + state = STATE_FINDING_SYNC_1; + } + break; + case STATE_READING_HEADER: + sampleSize = ((secondHeaderByte & ~SYNC_BYTE_SECOND) << 8) | data.readUnsignedByte(); + if (sampleSize > sampleDataBuffer.data.length) { + resetBufferForSize(sampleSize); + } + bytesRead = 0; + state = STATE_READING_SAMPLE; + break; + case STATE_READING_SAMPLE: + bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); + data.readBytes(sampleBitArray.data, bytesRead, bytesToRead); + bytesRead += bytesToRead; + if (bytesRead == sampleSize) { + sampleBitArray.setPosition(0); + parseAudioMuxElement(sampleBitArray); + state = STATE_FINDING_SYNC_1; + } + break; + default: + throw new IllegalStateException(); + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Parses an AudioMuxElement as defined in 14496-3:2009, Section 1.7.3.1, Table 1.41. + * + * @param data A {@link ParsableBitArray} containing the AudioMuxElement's bytes. + */ + private void parseAudioMuxElement(ParsableBitArray data) throws ParserException { + boolean useSameStreamMux = data.readBit(); + if (!useSameStreamMux) { + streamMuxRead = true; + parseStreamMuxConfig(data); + } else if (!streamMuxRead) { + return; // Parsing cannot continue without StreamMuxConfig information. + } + + if (audioMuxVersionA == 0) { + if (numSubframes != 0) { + throw new ParserException(); + } + int muxSlotLengthBytes = parsePayloadLengthInfo(data); + parsePayloadMux(data, muxSlotLengthBytes); + if (otherDataPresent) { + data.skipBits((int) otherDataLenBits); + } + } else { + throw new ParserException(); // Not defined by ISO/IEC 14496-3:2009. + } + } + + /** + * Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42. + */ + private void parseStreamMuxConfig(ParsableBitArray data) throws ParserException { + int audioMuxVersion = data.readBits(1); + audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0; + if (audioMuxVersionA == 0) { + if (audioMuxVersion == 1) { + latmGetValue(data); // Skip taraBufferFullness. + } + if (!data.readBit()) { + throw new ParserException(); + } + numSubframes = data.readBits(6); + int numProgram = data.readBits(4); + int numLayer = data.readBits(3); + if (numProgram != 0 || numLayer != 0) { + throw new ParserException(); + } + if (audioMuxVersion == 0) { + int startPosition = data.getPosition(); + int readBits = parseAudioSpecificConfig(data); + data.setPosition(startPosition); + byte[] initData = new byte[(readBits + 7) / 8]; + data.readBits(initData, 0, readBits); + Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null, + Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRateHz, + Collections.singletonList(initData), null, 0, language); + if (!format.equals(this.format)) { + this.format = format; + sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate; + output.format(format); + } + } else { + int ascLen = (int) latmGetValue(data); + int bitsRead = parseAudioSpecificConfig(data); + data.skipBits(ascLen - bitsRead); // fillBits. + } + parseFrameLength(data); + otherDataPresent = data.readBit(); + otherDataLenBits = 0; + if (otherDataPresent) { + if (audioMuxVersion == 1) { + otherDataLenBits = latmGetValue(data); + } else { + boolean otherDataLenEsc; + do { + otherDataLenEsc = data.readBit(); + otherDataLenBits = (otherDataLenBits << 8) + data.readBits(8); + } while (otherDataLenEsc); + } + } + boolean crcCheckPresent = data.readBit(); + if (crcCheckPresent) { + data.skipBits(8); // crcCheckSum. + } + } else { + throw new ParserException(); // This is not defined by ISO/IEC 14496-3:2009. + } + } + + private void parseFrameLength(ParsableBitArray data) { + frameLengthType = data.readBits(3); + switch (frameLengthType) { + case 0: + data.skipBits(8); // latmBufferFullness. + break; + case 1: + data.skipBits(9); // frameLength. + break; + case 3: + case 4: + case 5: + data.skipBits(6); // CELPframeLengthTableIndex. + break; + case 6: + case 7: + data.skipBits(1); // HVXCframeLengthTableIndex. + break; + default: + throw new IllegalStateException(); + } + } + + private int parseAudioSpecificConfig(ParsableBitArray data) throws ParserException { + int bitsLeft = data.bitsLeft(); + Pair config = CodecSpecificDataUtil.parseAacAudioSpecificConfig(data, true); + sampleRateHz = config.first; + channelCount = config.second; + return bitsLeft - data.bitsLeft(); + } + + private int parsePayloadLengthInfo(ParsableBitArray data) throws ParserException { + int muxSlotLengthBytes = 0; + // Assuming single program and single layer. + if (frameLengthType == 0) { + int tmp; + do { + tmp = data.readBits(8); + muxSlotLengthBytes += tmp; + } while (tmp == 255); + return muxSlotLengthBytes; + } else { + throw new ParserException(); + } + } + + private void parsePayloadMux(ParsableBitArray data, int muxLengthBytes) { + // The start of sample data in + int bitPosition = data.getPosition(); + if ((bitPosition & 0x07) == 0) { + // Sample data is byte-aligned. We can output it directly. + sampleDataBuffer.setPosition(bitPosition >> 3); + } else { + // Sample data is not byte-aligned and we need align it ourselves before outputting. + // Byte alignment is needed because LATM framing is not supported by MediaCodec. + data.readBits(sampleDataBuffer.data, 0, muxLengthBytes * 8); + sampleDataBuffer.setPosition(0); + } + output.sampleData(sampleDataBuffer, muxLengthBytes); + output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, muxLengthBytes, 0, null); + timeUs += sampleDurationUs; + } + + private void resetBufferForSize(int newSize) { + sampleDataBuffer.reset(newSize); + sampleBitArray.reset(sampleDataBuffer.data); + } + + private static long latmGetValue(ParsableBitArray data) { + int bytesForValue = data.readBits(2); + return data.readBits((bytesForValue + 1) * 8); + } + +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java index 9258ba1e6c8b..6fefab6314af 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -83,7 +83,7 @@ public final class MpegAudioReader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { timeUs = pesTimeUs; } @@ -100,6 +100,8 @@ public final class MpegAudioReader implements ElementaryStreamReader { case STATE_READING_FRAME: readFrameRemainder(data); break; + default: + throw new IllegalStateException(); } } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.java index 8f1a7c14269b..86afe2256358 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.java @@ -15,9 +15,10 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; -import android.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; @@ -77,8 +78,8 @@ public final class PesReader implements TsPayloadReader { } @Override - public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { - if (payloadUnitStartIndicator) { + public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException { + if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) { switch (state) { case STATE_FINDING_HEADER: case STATE_READING_HEADER: @@ -98,6 +99,8 @@ public final class PesReader implements TsPayloadReader { // Either way, notify the reader that it has now finished. reader.packetFinished(); break; + default: + throw new IllegalStateException(); } setState(STATE_READING_HEADER); } @@ -118,7 +121,8 @@ public final class PesReader implements TsPayloadReader { if (continueRead(data, pesScratch.data, readLength) && continueRead(data, null, extendedHeaderLength)) { parseHeaderExtension(); - reader.packetStarted(timeUs, dataAlignmentIndicator); + flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0; + reader.packetStarted(timeUs, flags); setState(STATE_READING_BODY); } break; @@ -138,6 +142,8 @@ public final class PesReader implements TsPayloadReader { } } break; + default: + throw new IllegalStateException(); } } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java new file mode 100644 index 000000000000..acd08a2f1279 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.BinarySearchSeeker; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * A seeker that supports seeking within PS stream using binary search. + * + *

This seeker uses the first and last SCR values within the stream, as well as the stream + * duration to interpolate the SCR value of the seeking position. Then it performs binary search + * within the stream to find a packets whose SCR value is with in {@link #SEEK_TOLERANCE_US} from + * the target SCR. + */ +/* package */ final class PsBinarySearchSeeker extends BinarySearchSeeker { + + private static final long SEEK_TOLERANCE_US = 100_000; + private static final int MINIMUM_SEARCH_RANGE_BYTES = 1000; + private static final int TIMESTAMP_SEARCH_BYTES = 20000; + + public PsBinarySearchSeeker( + TimestampAdjuster scrTimestampAdjuster, long streamDurationUs, long inputLength) { + super( + new DefaultSeekTimestampConverter(), + new PsScrSeeker(scrTimestampAdjuster), + streamDurationUs, + /* floorTimePosition= */ 0, + /* ceilingTimePosition= */ streamDurationUs + 1, + /* floorBytePosition= */ 0, + /* ceilingBytePosition= */ inputLength, + /* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE, + MINIMUM_SEARCH_RANGE_BYTES); + } + + /** + * A seeker that looks for a given SCR timestamp at a given position in a PS stream. + * + *

Given a SCR timestamp, and a position within a PS stream, this seeker will peek up to {@link + * #TIMESTAMP_SEARCH_BYTES} bytes from that stream position, look for all packs in that range, and + * then compare the SCR timestamps (if available) of these packets to the target timestamp. + */ + private static final class PsScrSeeker implements TimestampSeeker { + + private final TimestampAdjuster scrTimestampAdjuster; + private final ParsableByteArray packetBuffer; + + private PsScrSeeker(TimestampAdjuster scrTimestampAdjuster) { + this.scrTimestampAdjuster = scrTimestampAdjuster; + packetBuffer = new ParsableByteArray(); + } + + @Override + public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp) + throws IOException, InterruptedException { + long inputPosition = input.getPosition(); + int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition); + + packetBuffer.reset(bytesToSearch); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch); + + return searchForScrValueInBuffer(packetBuffer, targetTimestamp, inputPosition); + } + + @Override + public void onSeekFinished() { + packetBuffer.reset(Util.EMPTY_BYTE_ARRAY); + } + + private TimestampSearchResult searchForScrValueInBuffer( + ParsableByteArray packetBuffer, long targetScrTimeUs, long bufferStartOffset) { + int startOfLastPacketPosition = C.POSITION_UNSET; + int endOfLastPacketPosition = C.POSITION_UNSET; + long lastScrTimeUsInRange = C.TIME_UNSET; + + while (packetBuffer.bytesLeft() >= 4) { + int nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition()); + if (nextStartCode != PsExtractor.PACK_START_CODE) { + packetBuffer.skipBytes(1); + continue; + } else { + packetBuffer.skipBytes(4); + } + + // We found a pack. + long scrValue = PsDurationReader.readScrValueFromPack(packetBuffer); + if (scrValue != C.TIME_UNSET) { + long scrTimeUs = scrTimestampAdjuster.adjustTsTimestamp(scrValue); + if (scrTimeUs > targetScrTimeUs) { + if (lastScrTimeUsInRange == C.TIME_UNSET) { + // First SCR timestamp is already over target. + return TimestampSearchResult.overestimatedResult(scrTimeUs, bufferStartOffset); + } else { + // Last SCR timestamp < target timestamp < this timestamp. + return TimestampSearchResult.targetFoundResult( + bufferStartOffset + startOfLastPacketPosition); + } + } else if (scrTimeUs + SEEK_TOLERANCE_US > targetScrTimeUs) { + long startOfPacketInStream = bufferStartOffset + packetBuffer.getPosition(); + return TimestampSearchResult.targetFoundResult(startOfPacketInStream); + } + + lastScrTimeUsInRange = scrTimeUs; + startOfLastPacketPosition = packetBuffer.getPosition(); + } + skipToEndOfCurrentPack(packetBuffer); + endOfLastPacketPosition = packetBuffer.getPosition(); + } + + if (lastScrTimeUsInRange != C.TIME_UNSET) { + long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition; + return TimestampSearchResult.underestimatedResult( + lastScrTimeUsInRange, endOfLastPacketPositionInStream); + } else { + return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT; + } + } + + /** + * Skips the buffer position to the position after the end of the current PS pack in the buffer, + * given the byte position right after the {@link PsExtractor#PACK_START_CODE} of the pack in + * the buffer. If the pack ends after the end of the buffer, skips to the end of the buffer. + */ + private static void skipToEndOfCurrentPack(ParsableByteArray packetBuffer) { + int limit = packetBuffer.limit(); + + if (packetBuffer.bytesLeft() < 10) { + // We require at least 9 bytes for pack header to read SCR value + 1 byte for pack_stuffing + // length. + packetBuffer.setPosition(limit); + return; + } + packetBuffer.skipBytes(9); + + int packStuffingLength = packetBuffer.readUnsignedByte() & 0x07; + if (packetBuffer.bytesLeft() < packStuffingLength) { + packetBuffer.setPosition(limit); + return; + } + packetBuffer.skipBytes(packStuffingLength); + + if (packetBuffer.bytesLeft() < 4) { + packetBuffer.setPosition(limit); + return; + } + + int nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition()); + if (nextStartCode == PsExtractor.SYSTEM_HEADER_START_CODE) { + packetBuffer.skipBytes(4); + int systemHeaderLength = packetBuffer.readUnsignedShort(); + if (packetBuffer.bytesLeft() < systemHeaderLength) { + packetBuffer.setPosition(limit); + return; + } + packetBuffer.skipBytes(systemHeaderLength); + } + + // Find the position of the next PACK_START_CODE or MPEG_PROGRAM_END_CODE, which is right + // after the end position of this pack. + // If we couldn't find these codes within the buffer, return the buffer limit, or return + // the first position which PES packets pattern does not match (some malformed packets). + while (packetBuffer.bytesLeft() >= 4) { + nextStartCode = peekIntAtPosition(packetBuffer.data, packetBuffer.getPosition()); + if (nextStartCode == PsExtractor.PACK_START_CODE + || nextStartCode == PsExtractor.MPEG_PROGRAM_END_CODE) { + break; + } + if (nextStartCode >>> 8 != PsExtractor.PACKET_START_CODE_PREFIX) { + break; + } + packetBuffer.skipBytes(4); + + if (packetBuffer.bytesLeft() < 2) { + // 2 bytes for PES_packet length. + packetBuffer.setPosition(limit); + return; + } + int pesPacketLength = packetBuffer.readUnsignedShort(); + packetBuffer.setPosition( + Math.min(packetBuffer.limit(), packetBuffer.getPosition() + pesPacketLength)); + } + } + } + + private static int peekIntAtPosition(byte[] data, int position) { + return (data[position] & 0xFF) << 24 + | (data[position + 1] & 0xFF) << 16 + | (data[position + 2] & 0xFF) << 8 + | (data[position + 3] & 0xFF); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java new file mode 100644 index 000000000000..a5960fbe1518 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * A reader that can extract the approximate duration from a given MPEG program stream (PS). + * + *

This reader extracts the duration by reading system clock reference (SCR) values from the + * header of a pack at the start and at the end of the stream, calculating the difference, and + * converting that into stream duration. This reader also handles the case when a single SCR + * wraparound takes place within the stream, which can make SCR values at the beginning of the + * stream larger than SCR values at the end. This class can only be used once to read duration from + * a given stream, and the usage of the class is not thread-safe, so all calls should be made from + * the same thread. + * + *

Note: See ISO/IEC 13818-1, Table 2-33 for details of the SCR field in pack_header. + */ +/* package */ final class PsDurationReader { + + private static final int TIMESTAMP_SEARCH_BYTES = 20000; + + private final TimestampAdjuster scrTimestampAdjuster; + private final ParsableByteArray packetBuffer; + + private boolean isDurationRead; + private boolean isFirstScrValueRead; + private boolean isLastScrValueRead; + + private long firstScrValue; + private long lastScrValue; + private long durationUs; + + /* package */ PsDurationReader() { + scrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); + firstScrValue = C.TIME_UNSET; + lastScrValue = C.TIME_UNSET; + durationUs = C.TIME_UNSET; + packetBuffer = new ParsableByteArray(); + } + + /** Returns true if a PS duration has been read. */ + public boolean isDurationReadFinished() { + return isDurationRead; + } + + public TimestampAdjuster getScrTimestampAdjuster() { + return scrTimestampAdjuster; + } + + /** + * Reads a PS duration from the input. + * + *

This reader reads the duration by reading SCR values from the header of a pack at the start + * and at the end of the stream, calculating the difference, and converting that into stream + * duration. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated + * to hold the position of the required seek. + * @return One of the {@code RESULT_} values defined in {@link Extractor}. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + public @Extractor.ReadResult int readDuration( + ExtractorInput input, PositionHolder seekPositionHolder) + throws IOException, InterruptedException { + if (!isLastScrValueRead) { + return readLastScrValue(input, seekPositionHolder); + } + if (lastScrValue == C.TIME_UNSET) { + return finishReadDuration(input); + } + if (!isFirstScrValueRead) { + return readFirstScrValue(input, seekPositionHolder); + } + if (firstScrValue == C.TIME_UNSET) { + return finishReadDuration(input); + } + + long minScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(firstScrValue); + long maxScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(lastScrValue); + durationUs = maxScrPositionUs - minScrPositionUs; + return finishReadDuration(input); + } + + /** Returns the duration last read from {@link #readDuration(ExtractorInput, PositionHolder)}. */ + public long getDurationUs() { + return durationUs; + } + + /** + * Returns the SCR value read from the next pack in the stream, given the buffer at the pack + * header start position (just behind the pack start code). + */ + public static long readScrValueFromPack(ParsableByteArray packetBuffer) { + int originalPosition = packetBuffer.getPosition(); + if (packetBuffer.bytesLeft() < 9) { + // We require at 9 bytes for pack header to read scr value + return C.TIME_UNSET; + } + byte[] scrBytes = new byte[9]; + packetBuffer.readBytes(scrBytes, /* offset= */ 0, scrBytes.length); + packetBuffer.setPosition(originalPosition); + if (!checkMarkerBits(scrBytes)) { + return C.TIME_UNSET; + } + return readScrValueFromPackHeader(scrBytes); + } + + private int finishReadDuration(ExtractorInput input) { + packetBuffer.reset(Util.EMPTY_BYTE_ARRAY); + isDurationRead = true; + input.resetPeekPosition(); + return Extractor.RESULT_CONTINUE; + } + + private int readFirstScrValue(ExtractorInput input, PositionHolder seekPositionHolder) + throws IOException, InterruptedException { + int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength()); + int searchStartPosition = 0; + if (input.getPosition() != searchStartPosition) { + seekPositionHolder.position = searchStartPosition; + return Extractor.RESULT_SEEK; + } + + packetBuffer.reset(bytesToSearch); + input.resetPeekPosition(); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch); + + firstScrValue = readFirstScrValueFromBuffer(packetBuffer); + isFirstScrValueRead = true; + return Extractor.RESULT_CONTINUE; + } + + private long readFirstScrValueFromBuffer(ParsableByteArray packetBuffer) { + int searchStartPosition = packetBuffer.getPosition(); + int searchEndPosition = packetBuffer.limit(); + for (int searchPosition = searchStartPosition; + searchPosition < searchEndPosition - 3; + searchPosition++) { + int nextStartCode = peekIntAtPosition(packetBuffer.data, searchPosition); + if (nextStartCode == PsExtractor.PACK_START_CODE) { + packetBuffer.setPosition(searchPosition + 4); + long scrValue = readScrValueFromPack(packetBuffer); + if (scrValue != C.TIME_UNSET) { + return scrValue; + } + } + } + return C.TIME_UNSET; + } + + private int readLastScrValue(ExtractorInput input, PositionHolder seekPositionHolder) + throws IOException, InterruptedException { + long inputLength = input.getLength(); + int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, inputLength); + long searchStartPosition = inputLength - bytesToSearch; + if (input.getPosition() != searchStartPosition) { + seekPositionHolder.position = searchStartPosition; + return Extractor.RESULT_SEEK; + } + + packetBuffer.reset(bytesToSearch); + input.resetPeekPosition(); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch); + + lastScrValue = readLastScrValueFromBuffer(packetBuffer); + isLastScrValueRead = true; + return Extractor.RESULT_CONTINUE; + } + + private long readLastScrValueFromBuffer(ParsableByteArray packetBuffer) { + int searchStartPosition = packetBuffer.getPosition(); + int searchEndPosition = packetBuffer.limit(); + for (int searchPosition = searchEndPosition - 4; + searchPosition >= searchStartPosition; + searchPosition--) { + int nextStartCode = peekIntAtPosition(packetBuffer.data, searchPosition); + if (nextStartCode == PsExtractor.PACK_START_CODE) { + packetBuffer.setPosition(searchPosition + 4); + long scrValue = readScrValueFromPack(packetBuffer); + if (scrValue != C.TIME_UNSET) { + return scrValue; + } + } + } + return C.TIME_UNSET; + } + + private int peekIntAtPosition(byte[] data, int position) { + return (data[position] & 0xFF) << 24 + | (data[position + 1] & 0xFF) << 16 + | (data[position + 2] & 0xFF) << 8 + | (data[position + 3] & 0xFF); + } + + private static boolean checkMarkerBits(byte[] scrBytes) { + // Verify the 01xxx1xx marker on the 0th byte + if ((scrBytes[0] & 0xC4) != 0x44) { + return false; + } + // 1st byte belongs to scr field. + // Verify the xxxxx1xx marker on the 2nd byte + if ((scrBytes[2] & 0x04) != 0x04) { + return false; + } + // 3rd byte belongs to scr field. + // Verify the xxxxx1xx marker on the 4rd byte + if ((scrBytes[4] & 0x04) != 0x04) { + return false; + } + // Verify the xxxxxxx1 marker on the 5th byte + if ((scrBytes[5] & 0x01) != 0x01) { + return false; + } + // 6th and 7th bytes belongs to program_max_rate field. + // Verify the xxxxxx11 marker on the 8th byte + return (scrBytes[8] & 0x03) == 0x03; + } + + /** + * Returns the value of SCR base - 33 bits in big endian order from the PS pack header, ignoring + * the marker bits. Note: See ISO/IEC 13818-1, Table 2-33 for details of the SCR field in + * pack_header. + * + *

We ignore SCR Ext, because it's too small to have any significance. + */ + private static long readScrValueFromPackHeader(byte[] scrBytes) { + return ((scrBytes[0] & 0b00111000L) >> 3) << 30 + | (scrBytes[0] & 0b00000011L) << 28 + | (scrBytes[1] & 0xFFL) << 20 + | ((scrBytes[2] & 0b11111000L) >> 3) << 15 + | (scrBytes[2] & 0b00000011L) << 13 + | (scrBytes[3] & 0xFFL) << 5 + | (scrBytes[4] & 0b11111000L) >> 3; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index e01e08197491..8dcccbe45998 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -17,6 +17,7 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -30,28 +31,24 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjust import java.io.IOException; /** - * Facilitates the extraction of data from the MPEG-2 TS container format. + * Extracts data from the MPEG-2 PS container format. */ public final class PsExtractor implements Extractor { - /** - * Factory for {@link PsExtractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { + /** Factory for {@link PsExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new PsExtractor()}; - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new PsExtractor()}; - } - - }; - - private static final int PACK_START_CODE = 0x000001BA; - private static final int SYSTEM_HEADER_START_CODE = 0x000001BB; - private static final int PACKET_START_CODE_PREFIX = 0x000001; - private static final int MPEG_PROGRAM_END_CODE = 0x000001B9; + /* package */ static final int PACK_START_CODE = 0x000001BA; + /* package */ static final int SYSTEM_HEADER_START_CODE = 0x000001BB; + /* package */ static final int PACKET_START_CODE_PREFIX = 0x000001; + /* package */ static final int MPEG_PROGRAM_END_CODE = 0x000001B9; private static final int MAX_STREAM_ID_PLUS_ONE = 0x100; + + // Max search length for first audio and video track in input data. private static final long MAX_SEARCH_LENGTH = 1024 * 1024; + // Max search length for additional audio and video tracks in input data after at least one audio + // and video track has been found. + private static final long MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND = 8 * 1024; public static final int PRIVATE_STREAM_1 = 0xBD; public static final int AUDIO_STREAM = 0xC0; @@ -62,12 +59,17 @@ public final class PsExtractor implements Extractor { private final TimestampAdjuster timestampAdjuster; private final SparseArray psPayloadReaders; // Indexed by pid private final ParsableByteArray psPacketBuffer; + private final PsDurationReader durationReader; + private boolean foundAllTracks; private boolean foundAudioTrack; private boolean foundVideoTrack; + private long lastTrackPosition; // Accessed only by the loading thread. + private PsBinarySearchSeeker psBinarySearchSeeker; private ExtractorOutput output; + private boolean hasOutputSeekMap; public PsExtractor() { this(new TimestampAdjuster(0)); @@ -77,6 +79,7 @@ public final class PsExtractor implements Extractor { this.timestampAdjuster = timestampAdjuster; psPacketBuffer = new ParsableByteArray(4096); psPayloadReaders = new SparseArray<>(); + durationReader = new PsDurationReader(); } // Extractor implementation. @@ -123,12 +126,27 @@ public final class PsExtractor implements Extractor { @Override public void init(ExtractorOutput output) { this.output = output; - output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); } @Override public void seek(long position, long timeUs) { - timestampAdjuster.reset(); + boolean hasNotEncounteredFirstTimestamp = + timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET; + if (hasNotEncounteredFirstTimestamp + || (timestampAdjuster.getFirstSampleTimestampUs() != 0 + && timestampAdjuster.getFirstSampleTimestampUs() != timeUs)) { + // - If the timestamp adjuster in the PS stream has not encountered any sample, it's going to + // treat the first timestamp encountered as sample time 0, which is incorrect. In this case, + // we have to set the first sample timestamp manually. + // - If the timestamp adjuster has its timestamp set manually before, and now we seek to a + // different position, we need to set the first sample timestamp manually again. + timestampAdjuster.reset(); + timestampAdjuster.setFirstSampleTimestampUs(timeUs); + } + + if (psBinarySearchSeeker != null) { + psBinarySearchSeeker.setSeekTargetUs(timeUs); + } for (int i = 0; i < psPayloadReaders.size(); i++) { psPayloadReaders.valueAt(i).seek(); } @@ -142,6 +160,23 @@ public final class PsExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + + long inputLength = input.getLength(); + boolean canReadDuration = inputLength != C.LENGTH_UNSET; + if (canReadDuration && !durationReader.isDurationReadFinished()) { + return durationReader.readDuration(input, seekPosition); + } + maybeOutputSeekMap(inputLength); + if (psBinarySearchSeeker != null && psBinarySearchSeeker.isSeeking()) { + return psBinarySearchSeeker.handlePendingSeek(input, seekPosition); + } + + input.resetPeekPosition(); + long peekBytesLeft = + inputLength != C.LENGTH_UNSET ? inputLength - input.getPeekPosition() : C.LENGTH_UNSET; + if (peekBytesLeft != C.LENGTH_UNSET && peekBytesLeft < 4) { + return RESULT_END_OF_INPUT; + } // First peek and check what type of start code is next. if (!input.peekFully(psPacketBuffer.data, 0, 4, true)) { return RESULT_END_OF_INPUT; @@ -187,18 +222,21 @@ public final class PsExtractor implements Extractor { if (!foundAllTracks) { if (payloadReader == null) { ElementaryStreamReader elementaryStreamReader = null; - if (!foundAudioTrack && streamId == PRIVATE_STREAM_1) { + if (streamId == PRIVATE_STREAM_1) { // Private stream, used for AC3 audio. // NOTE: This may need further parsing to determine if its DTS, but that's likely only // valid for DVDs. elementaryStreamReader = new Ac3Reader(); foundAudioTrack = true; - } else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) { + lastTrackPosition = input.getPosition(); + } else if ((streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) { elementaryStreamReader = new MpegAudioReader(); foundAudioTrack = true; - } else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) { + lastTrackPosition = input.getPosition(); + } else if ((streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) { elementaryStreamReader = new H262Reader(); foundVideoTrack = true; + lastTrackPosition = input.getPosition(); } if (elementaryStreamReader != null) { TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE); @@ -207,7 +245,11 @@ public final class PsExtractor implements Extractor { psPayloadReaders.put(streamId, payloadReader); } } - if ((foundAudioTrack && foundVideoTrack) || input.getPosition() > MAX_SEARCH_LENGTH) { + long maxSearchPosition = + foundAudioTrack && foundVideoTrack + ? lastTrackPosition + MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND + : MAX_SEARCH_LENGTH; + if (input.getPosition() > maxSearchPosition) { foundAllTracks = true; output.endTracks(); } @@ -236,6 +278,22 @@ public final class PsExtractor implements Extractor { // Internals. + private void maybeOutputSeekMap(long inputLength) { + if (!hasOutputSeekMap) { + hasOutputSeekMap = true; + if (durationReader.getDurationUs() != C.TIME_UNSET) { + psBinarySearchSeeker = + new PsBinarySearchSeeker( + durationReader.getScrTimestampAdjuster(), + durationReader.getDurationUs(), + inputLength); + output.seekMap(psBinarySearchSeeker.getSeekMap()); + } else { + output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs())); + } + } + } + /** * Parses PES packet data and extracts samples. */ @@ -275,15 +333,16 @@ public final class PsExtractor implements Extractor { * Consumes the payload of a PS packet. * * @param data The PES packet. The position will be set to the start of the payload. + * @throws ParserException If the payload could not be parsed. */ - public void consume(ParsableByteArray data) { + public void consume(ParsableByteArray data) throws ParserException { data.readBytes(pesScratch.data, 0, 3); pesScratch.setPosition(0); parseHeader(); data.readBytes(pesScratch.data, 0, extendedHeaderLength); pesScratch.setPosition(0); parseHeaderExtension(); - pesPayloadReader.packetStarted(timeUs, true); + pesPayloadReader.packetStarted(timeUs, TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR); pesPayloadReader.consume(data); // We always have complete PES packets with program stream. pesPayloadReader.packetFinished(); diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java index 606c0e065533..61b53cfa72c5 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java @@ -57,7 +57,8 @@ public final class SectionReader implements TsPayloadReader { } @Override - public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { + public void consume(ParsableByteArray data, @Flags int flags) { + boolean payloadUnitStartIndicator = (flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0; int payloadStartPosition = C.POSITION_UNSET; if (payloadUnitStartIndicator) { int payloadStartOffset = data.readUnsignedByte(); @@ -113,7 +114,7 @@ public final class SectionReader implements TsPayloadReader { if (bytesRead == totalSectionLength) { if (sectionSyntaxIndicator) { // This section has common syntax as defined in ISO/IEC 13818-1, section 2.4.4.11. - if (Util.crc(sectionData.data, 0, totalSectionLength, 0xFFFFFFFF) != 0) { + if (Util.crc32(sectionData.data, 0, totalSectionLength, 0xFFFFFFFF) != 0) { // The CRC is invalid so discard the section. waitingForPayloadStart = true; return; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.java index 9a57b4568b8b..88ea482be4da 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -26,10 +26,8 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import java.util.List; -/** - * Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}. - */ -/* package */ final class SeiReader { +/** Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}. */ +public final class SeiReader { private final List closedCaptionFormats; private final TrackOutput[] outputs; @@ -51,9 +49,19 @@ import java.util.List; Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType) || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), "Invalid closed caption mime type provided: " + channelMimeType); - output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), channelMimeType, null, - Format.NO_VALUE, channelFormat.selectionFlags, channelFormat.language, - channelFormat.accessibilityChannel, null)); + String formatId = channelFormat.id != null ? channelFormat.id : idGenerator.getFormatId(); + output.format( + Format.createTextSampleFormat( + formatId, + channelMimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + channelFormat.selectionFlags, + channelFormat.language, + channelFormat.accessibilityChannel, + /* drmInitData= */ null, + Format.OFFSET_SAMPLE_RELATIVE, + channelFormat.initializationData)); outputs[i] = output; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java new file mode 100644 index 000000000000..136691bdaf6b --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.BinarySearchSeeker; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * A seeker that supports seeking within TS stream using binary search. + * + *

This seeker uses the first and last PCR values within the stream, as well as the stream + * duration to interpolate the PCR value of the seeking position. Then it performs binary search + * within the stream to find a packets whose PCR value is within {@link #SEEK_TOLERANCE_US} from the + * target PCR. + */ +/* package */ final class TsBinarySearchSeeker extends BinarySearchSeeker { + + private static final long SEEK_TOLERANCE_US = 100_000; + private static final int MINIMUM_SEARCH_RANGE_BYTES = 5 * TsExtractor.TS_PACKET_SIZE; + private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE; + + public TsBinarySearchSeeker( + TimestampAdjuster pcrTimestampAdjuster, long streamDurationUs, long inputLength, int pcrPid) { + super( + new DefaultSeekTimestampConverter(), + new TsPcrSeeker(pcrPid, pcrTimestampAdjuster), + streamDurationUs, + /* floorTimePosition= */ 0, + /* ceilingTimePosition= */ streamDurationUs + 1, + /* floorBytePosition= */ 0, + /* ceilingBytePosition= */ inputLength, + /* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE, + MINIMUM_SEARCH_RANGE_BYTES); + } + + /** + * A {@link TimestampSeeker} implementation that looks for a given PCR timestamp at a given + * position in a TS stream. + * + *

Given a PCR timestamp, and a position within a TS stream, this seeker will peek up to {@link + * #TIMESTAMP_SEARCH_BYTES} from that stream position, look for all packets with PID equal to + * PCR_PID, and then compare the PCR timestamps (if available) of these packets to the target + * timestamp. + */ + private static final class TsPcrSeeker implements TimestampSeeker { + + private final TimestampAdjuster pcrTimestampAdjuster; + private final ParsableByteArray packetBuffer; + private final int pcrPid; + + public TsPcrSeeker(int pcrPid, TimestampAdjuster pcrTimestampAdjuster) { + this.pcrPid = pcrPid; + this.pcrTimestampAdjuster = pcrTimestampAdjuster; + packetBuffer = new ParsableByteArray(); + } + + @Override + public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp) + throws IOException, InterruptedException { + long inputPosition = input.getPosition(); + int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition); + + packetBuffer.reset(bytesToSearch); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch); + + return searchForPcrValueInBuffer(packetBuffer, targetTimestamp, inputPosition); + } + + private TimestampSearchResult searchForPcrValueInBuffer( + ParsableByteArray packetBuffer, long targetPcrTimeUs, long bufferStartOffset) { + int limit = packetBuffer.limit(); + + long startOfLastPacketPosition = C.POSITION_UNSET; + long endOfLastPacketPosition = C.POSITION_UNSET; + long lastPcrTimeUsInRange = C.TIME_UNSET; + + while (packetBuffer.bytesLeft() >= TsExtractor.TS_PACKET_SIZE) { + int startOfPacket = + TsUtil.findSyncBytePosition(packetBuffer.data, packetBuffer.getPosition(), limit); + int endOfPacket = startOfPacket + TsExtractor.TS_PACKET_SIZE; + if (endOfPacket > limit) { + break; + } + long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, startOfPacket, pcrPid); + if (pcrValue != C.TIME_UNSET) { + long pcrTimeUs = pcrTimestampAdjuster.adjustTsTimestamp(pcrValue); + if (pcrTimeUs > targetPcrTimeUs) { + if (lastPcrTimeUsInRange == C.TIME_UNSET) { + // First PCR timestamp is already over target. + return TimestampSearchResult.overestimatedResult(pcrTimeUs, bufferStartOffset); + } else { + // Last PCR timestamp < target timestamp < this timestamp. + return TimestampSearchResult.targetFoundResult( + bufferStartOffset + startOfLastPacketPosition); + } + } else if (pcrTimeUs + SEEK_TOLERANCE_US > targetPcrTimeUs) { + long startOfPacketInStream = bufferStartOffset + startOfPacket; + return TimestampSearchResult.targetFoundResult(startOfPacketInStream); + } + + lastPcrTimeUsInRange = pcrTimeUs; + startOfLastPacketPosition = startOfPacket; + } + packetBuffer.setPosition(endOfPacket); + endOfLastPacketPosition = endOfPacket; + } + + if (lastPcrTimeUsInRange != C.TIME_UNSET) { + long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition; + return TimestampSearchResult.underestimatedResult( + lastPcrTimeUsInRange, endOfLastPacketPositionInStream); + } else { + return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT; + } + } + + @Override + public void onSeekFinished() { + packetBuffer.reset(Util.EMPTY_BYTE_ARRAY); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java new file mode 100644 index 000000000000..ed4b66a7e42a --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * A reader that can extract the approximate duration from a given MPEG transport stream (TS). + * + *

This reader extracts the duration by reading PCR values of the PCR PID packets at the start + * and at the end of the stream, calculating the difference, and converting that into stream + * duration. This reader also handles the case when a single PCR wraparound takes place within the + * stream, which can make PCR values at the beginning of the stream larger than PCR values at the + * end. This class can only be used once to read duration from a given stream, and the usage of the + * class is not thread-safe, so all calls should be made from the same thread. + */ +/* package */ final class TsDurationReader { + + private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE; + + private final TimestampAdjuster pcrTimestampAdjuster; + private final ParsableByteArray packetBuffer; + + private boolean isDurationRead; + private boolean isFirstPcrValueRead; + private boolean isLastPcrValueRead; + + private long firstPcrValue; + private long lastPcrValue; + private long durationUs; + + /* package */ TsDurationReader() { + pcrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); + firstPcrValue = C.TIME_UNSET; + lastPcrValue = C.TIME_UNSET; + durationUs = C.TIME_UNSET; + packetBuffer = new ParsableByteArray(); + } + + /** Returns true if a TS duration has been read. */ + public boolean isDurationReadFinished() { + return isDurationRead; + } + + /** + * Reads a TS duration from the input, using the given PCR PID. + * + *

This reader reads the duration by reading PCR values of the PCR PID packets at the start and + * at the end of the stream, calculating the difference, and converting that into stream duration. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated + * to hold the position of the required seek. + * @param pcrPid The PID of the packet stream within this TS stream that contains PCR values. + * @return One of the {@code RESULT_} values defined in {@link Extractor}. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + public @Extractor.ReadResult int readDuration( + ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) + throws IOException, InterruptedException { + if (pcrPid <= 0) { + return finishReadDuration(input); + } + if (!isLastPcrValueRead) { + return readLastPcrValue(input, seekPositionHolder, pcrPid); + } + if (lastPcrValue == C.TIME_UNSET) { + return finishReadDuration(input); + } + if (!isFirstPcrValueRead) { + return readFirstPcrValue(input, seekPositionHolder, pcrPid); + } + if (firstPcrValue == C.TIME_UNSET) { + return finishReadDuration(input); + } + + long minPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(firstPcrValue); + long maxPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(lastPcrValue); + durationUs = maxPcrPositionUs - minPcrPositionUs; + return finishReadDuration(input); + } + + /** + * Returns the duration last read from {@link #readDuration(ExtractorInput, PositionHolder, int)}. + */ + public long getDurationUs() { + return durationUs; + } + + /** + * Returns the {@link TimestampAdjuster} that this class uses to adjust timestamps read from the + * input TS stream. + */ + public TimestampAdjuster getPcrTimestampAdjuster() { + return pcrTimestampAdjuster; + } + + private int finishReadDuration(ExtractorInput input) { + packetBuffer.reset(Util.EMPTY_BYTE_ARRAY); + isDurationRead = true; + input.resetPeekPosition(); + return Extractor.RESULT_CONTINUE; + } + + private int readFirstPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) + throws IOException, InterruptedException { + int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength()); + int searchStartPosition = 0; + if (input.getPosition() != searchStartPosition) { + seekPositionHolder.position = searchStartPosition; + return Extractor.RESULT_SEEK; + } + + packetBuffer.reset(bytesToSearch); + input.resetPeekPosition(); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch); + + firstPcrValue = readFirstPcrValueFromBuffer(packetBuffer, pcrPid); + isFirstPcrValueRead = true; + return Extractor.RESULT_CONTINUE; + } + + private long readFirstPcrValueFromBuffer(ParsableByteArray packetBuffer, int pcrPid) { + int searchStartPosition = packetBuffer.getPosition(); + int searchEndPosition = packetBuffer.limit(); + for (int searchPosition = searchStartPosition; + searchPosition < searchEndPosition; + searchPosition++) { + if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) { + continue; + } + long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid); + if (pcrValue != C.TIME_UNSET) { + return pcrValue; + } + } + return C.TIME_UNSET; + } + + private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) + throws IOException, InterruptedException { + long inputLength = input.getLength(); + int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, inputLength); + long searchStartPosition = inputLength - bytesToSearch; + if (input.getPosition() != searchStartPosition) { + seekPositionHolder.position = searchStartPosition; + return Extractor.RESULT_SEEK; + } + + packetBuffer.reset(bytesToSearch); + input.resetPeekPosition(); + input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch); + + lastPcrValue = readLastPcrValueFromBuffer(packetBuffer, pcrPid); + isLastPcrValueRead = true; + return Extractor.RESULT_CONTINUE; + } + + private long readLastPcrValueFromBuffer(ParsableByteArray packetBuffer, int pcrPid) { + int searchStartPosition = packetBuffer.getPosition(); + int searchEndPosition = packetBuffer.limit(); + for (int searchPosition = searchEndPosition - 1; + searchPosition >= searchStartPosition; + searchPosition--) { + if (packetBuffer.data[searchPosition] != TsExtractor.TS_SYNC_BYTE) { + continue; + } + long pcrValue = TsUtil.readPcrFromPacket(packetBuffer, searchPosition, pcrPid); + if (pcrValue != C.TIME_UNSET) { + return pcrValue; + } + } + return C.TIME_UNSET; + } + +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index a24b0f6ac59b..a52e56bd320f 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -15,11 +15,14 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; -import android.support.annotation.IntDef; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_PAYLOAD_UNIT_START_INDICATOR; + import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; +import androidx.annotation.IntDef; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -37,6 +40,7 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArr import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -45,33 +49,26 @@ import java.util.Collections; import java.util.List; /** - * Facilitates the extraction of data from the MPEG-2 TS container format. + * Extracts data from the MPEG-2 TS container format. */ public final class TsExtractor implements Extractor { - /** - * Factory for {@link TsExtractor} instances. - */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { - - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new TsExtractor()}; - } - - }; + /** Factory for {@link TsExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new TsExtractor()}; /** - * Modes for the extractor. + * Modes for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} or {@link + * #MODE_HLS}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({MODE_NORMAL, MODE_SINGLE_PMT, MODE_HLS}) + @IntDef({MODE_MULTI_PMT, MODE_SINGLE_PMT, MODE_HLS}) public @interface Mode {} /** * Behave as defined in ISO/IEC 13818-1. */ - public static final int MODE_NORMAL = 0; + public static final int MODE_MULTI_PMT = 0; /** * Assume only one PMT will be contained in the stream, even if more are declared by the PAT. */ @@ -84,11 +81,13 @@ public final class TsExtractor implements Extractor { public static final int TS_STREAM_TYPE_MPA = 0x03; public static final int TS_STREAM_TYPE_MPA_LSF = 0x04; - public static final int TS_STREAM_TYPE_AAC = 0x0F; + public static final int TS_STREAM_TYPE_AAC_ADTS = 0x0F; + public static final int TS_STREAM_TYPE_AAC_LATM = 0x11; public static final int TS_STREAM_TYPE_AC3 = 0x81; public static final int TS_STREAM_TYPE_DTS = 0x8A; public static final int TS_STREAM_TYPE_HDMV_DTS = 0x82; public static final int TS_STREAM_TYPE_E_AC3 = 0x87; + public static final int TS_STREAM_TYPE_AC4 = 0xAC; // DVB/ATSC AC-4 Descriptor public static final int TS_STREAM_TYPE_H262 = 0x02; public static final int TS_STREAM_TYPE_H264 = 0x1B; public static final int TS_STREAM_TYPE_H265 = 0x24; @@ -96,32 +95,40 @@ public final class TsExtractor implements Extractor { public static final int TS_STREAM_TYPE_SPLICE_INFO = 0x86; public static final int TS_STREAM_TYPE_DVBSUBS = 0x59; - private static final int TS_PACKET_SIZE = 188; - private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. + public static final int TS_PACKET_SIZE = 188; + public static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. + private static final int TS_PAT_PID = 0; private static final int MAX_PID_PLUS_ONE = 0x2000; - private static final long AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("AC-3"); - private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("EAC3"); - private static final long HEVC_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("HEVC"); + private static final long AC3_FORMAT_IDENTIFIER = 0x41432d33; + private static final long E_AC3_FORMAT_IDENTIFIER = 0x45414333; + private static final long AC4_FORMAT_IDENTIFIER = 0x41432d34; + private static final long HEVC_FORMAT_IDENTIFIER = 0x48455643; - private static final int BUFFER_PACKET_COUNT = 5; // Should be at least 2 - private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT; + private static final int BUFFER_SIZE = TS_PACKET_SIZE * 50; + private static final int SNIFF_TS_PACKET_COUNT = 5; - @Mode private final int mode; + private final @Mode int mode; private final List timestampAdjusters; private final ParsableByteArray tsPacketBuffer; - private final ParsableBitArray tsScratch; private final SparseIntArray continuityCounters; private final TsPayloadReader.Factory payloadReaderFactory; private final SparseArray tsPayloadReaders; // Indexed by pid private final SparseBooleanArray trackIds; + private final SparseBooleanArray trackPids; + private final TsDurationReader durationReader; // Accessed only by the loading thread. + private TsBinarySearchSeeker tsBinarySearchSeeker; private ExtractorOutput output; private int remainingPmts; private boolean tracksEnded; + private boolean hasOutputSeekMap; + private boolean pendingSeekToStart; private TsPayloadReader id3Reader; + private int bytesSinceLastSync; + private int pcrPid; public TsExtractor() { this(0); @@ -132,17 +139,31 @@ public final class TsExtractor implements Extractor { * {@code FLAG_*} values that control the behavior of the payload readers. */ public TsExtractor(@Flags int defaultTsPayloadReaderFlags) { - this(MODE_NORMAL, new TimestampAdjuster(0), + this(MODE_SINGLE_PMT, defaultTsPayloadReaderFlags); + } + + /** + * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} + * and {@link #MODE_HLS}. + * @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory} + * {@code FLAG_*} values that control the behavior of the payload readers. + */ + public TsExtractor(@Mode int mode, @Flags int defaultTsPayloadReaderFlags) { + this( + mode, + new TimestampAdjuster(0), new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags)); } /** - * @param mode Mode for the extractor. One of {@link #MODE_NORMAL}, {@link #MODE_SINGLE_PMT} + * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} * and {@link #MODE_HLS}. * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. * @param payloadReaderFactory Factory for injecting a custom set of payload readers. */ - public TsExtractor(@Mode int mode, TimestampAdjuster timestampAdjuster, + public TsExtractor( + @Mode int mode, + TimestampAdjuster timestampAdjuster, TsPayloadReader.Factory payloadReaderFactory) { this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory); this.mode = mode; @@ -152,11 +173,13 @@ public final class TsExtractor implements Extractor { timestampAdjusters = new ArrayList<>(); timestampAdjusters.add(timestampAdjuster); } - tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE); - tsScratch = new ParsableBitArray(new byte[3]); + tsPacketBuffer = new ParsableByteArray(new byte[BUFFER_SIZE], 0); trackIds = new SparseBooleanArray(); + trackPids = new SparseBooleanArray(); tsPayloadReaders = new SparseArray<>(); continuityCounters = new SparseIntArray(); + durationReader = new TsDurationReader(); + pcrPid = -1; resetPayloadReaders(); } @@ -165,17 +188,20 @@ public final class TsExtractor implements Extractor { @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { byte[] buffer = tsPacketBuffer.data; - input.peekFully(buffer, 0, BUFFER_SIZE); - for (int j = 0; j < TS_PACKET_SIZE; j++) { - for (int i = 0; true; i++) { - if (i == BUFFER_PACKET_COUNT) { - input.skipFully(j); - return true; - } - if (buffer[j + i * TS_PACKET_SIZE] != TS_SYNC_BYTE) { + input.peekFully(buffer, 0, TS_PACKET_SIZE * SNIFF_TS_PACKET_COUNT); + for (int startPosCandidate = 0; startPosCandidate < TS_PACKET_SIZE; startPosCandidate++) { + // Try to identify at least SNIFF_TS_PACKET_COUNT packets starting with TS_SYNC_BYTE. + boolean isSyncBytePatternCorrect = true; + for (int i = 0; i < SNIFF_TS_PACKET_COUNT; i++) { + if (buffer[startPosCandidate + i * TS_PACKET_SIZE] != TS_SYNC_BYTE) { + isSyncBytePatternCorrect = false; break; } } + if (isSyncBytePatternCorrect) { + input.skipFully(startPosCandidate); + return true; + } } return false; } @@ -183,19 +209,37 @@ public final class TsExtractor implements Extractor { @Override public void init(ExtractorOutput output) { this.output = output; - output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); } @Override public void seek(long position, long timeUs) { + Assertions.checkState(mode != MODE_HLS); int timestampAdjustersCount = timestampAdjusters.size(); for (int i = 0; i < timestampAdjustersCount; i++) { - timestampAdjusters.get(i).reset(); + TimestampAdjuster timestampAdjuster = timestampAdjusters.get(i); + boolean hasNotEncounteredFirstTimestamp = + timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET; + if (hasNotEncounteredFirstTimestamp + || (timestampAdjuster.getTimestampOffsetUs() != 0 + && timestampAdjuster.getFirstSampleTimestampUs() != timeUs)) { + // - If a track in the TS stream has not encountered any sample, it's going to treat the + // first sample encountered as timestamp 0, which is incorrect. So we have to set the first + // sample timestamp for that track manually. + // - If the timestamp adjuster has its timestamp set manually before, and now we seek to a + // different position, we need to set the first sample timestamp manually again. + timestampAdjuster.reset(); + timestampAdjuster.setFirstSampleTimestampUs(timeUs); + } + } + if (timeUs != 0 && tsBinarySearchSeeker != null) { + tsBinarySearchSeeker.setSeekTargetUs(timeUs); } tsPacketBuffer.reset(); continuityCounters.clear(); - // Elementary stream readers' state should be cleared to get consistent behaviours when seeking. - resetPayloadReaders(); + for (int i = 0; i < tsPayloadReaders.size(); i++) { + tsPayloadReaders.valueAt(i).seek(); + } + bytesSinceLastSync = 0; } @Override @@ -204,90 +248,101 @@ public final class TsExtractor implements Extractor { } @Override - public int read(ExtractorInput input, PositionHolder seekPosition) + public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { - byte[] data = tsPacketBuffer.data; - // Shift bytes to the start of the buffer if there isn't enough space left at the end - if (BUFFER_SIZE - tsPacketBuffer.getPosition() < TS_PACKET_SIZE) { - int bytesLeft = tsPacketBuffer.bytesLeft(); - if (bytesLeft > 0) { - System.arraycopy(data, tsPacketBuffer.getPosition(), data, 0, bytesLeft); + long inputLength = input.getLength(); + if (tracksEnded) { + boolean canReadDuration = inputLength != C.LENGTH_UNSET && mode != MODE_HLS; + if (canReadDuration && !durationReader.isDurationReadFinished()) { + return durationReader.readDuration(input, seekPosition, pcrPid); } - tsPacketBuffer.reset(data, bytesLeft); - } - // Read more bytes until there is at least one packet size - while (tsPacketBuffer.bytesLeft() < TS_PACKET_SIZE) { - int limit = tsPacketBuffer.limit(); - int read = input.read(data, limit, BUFFER_SIZE - limit); - if (read == C.RESULT_END_OF_INPUT) { - return RESULT_END_OF_INPUT; + maybeOutputSeekMap(inputLength); + + if (pendingSeekToStart) { + pendingSeekToStart = false; + seek(/* position= */ 0, /* timeUs= */ 0); + if (input.getPosition() != 0) { + seekPosition.position = 0; + return RESULT_SEEK; + } + } + + if (tsBinarySearchSeeker != null && tsBinarySearchSeeker.isSeeking()) { + return tsBinarySearchSeeker.handlePendingSeek(input, seekPosition); } - tsPacketBuffer.setLimit(limit + read); } - // Note: see ISO/IEC 13818-1, section 2.4.3.2 for detailed information on the format of - // the header. - final int limit = tsPacketBuffer.limit(); - int position = tsPacketBuffer.getPosition(); - while (position < limit && data[position] != TS_SYNC_BYTE) { - position++; + if (!fillBufferWithAtLeastOnePacket(input)) { + return RESULT_END_OF_INPUT; } - tsPacketBuffer.setPosition(position); - int endOfPacket = position + TS_PACKET_SIZE; + int endOfPacket = findEndOfFirstTsPacketInBuffer(); + int limit = tsPacketBuffer.limit(); if (endOfPacket > limit) { return RESULT_CONTINUE; } - tsPacketBuffer.skipBytes(1); - tsPacketBuffer.readBytes(tsScratch, 3); - if (tsScratch.readBit()) { // transport_error_indicator + @TsPayloadReader.Flags int packetHeaderFlags = 0; + + // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format. + int tsPacketHeader = tsPacketBuffer.readInt(); + if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator // There are uncorrectable errors in this packet. tsPacketBuffer.setPosition(endOfPacket); return RESULT_CONTINUE; } - boolean payloadUnitStartIndicator = tsScratch.readBit(); - tsScratch.skipBits(1); // transport_priority - int pid = tsScratch.readBits(13); - tsScratch.skipBits(2); // transport_scrambling_control - boolean adaptationFieldExists = tsScratch.readBit(); - boolean payloadExists = tsScratch.readBit(); + packetHeaderFlags |= (tsPacketHeader & 0x400000) != 0 ? FLAG_PAYLOAD_UNIT_START_INDICATOR : 0; + // Ignoring transport_priority (tsPacketHeader & 0x200000) + int pid = (tsPacketHeader & 0x1FFF00) >> 8; + // Ignoring transport_scrambling_control (tsPacketHeader & 0xC0) + boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0; + boolean payloadExists = (tsPacketHeader & 0x10) != 0; + + TsPayloadReader payloadReader = payloadExists ? tsPayloadReaders.get(pid) : null; + if (payloadReader == null) { + tsPacketBuffer.setPosition(endOfPacket); + return RESULT_CONTINUE; + } // Discontinuity check. - boolean discontinuityFound = false; - int continuityCounter = tsScratch.readBits(4); if (mode != MODE_HLS) { + int continuityCounter = tsPacketHeader & 0xF; int previousCounter = continuityCounters.get(pid, continuityCounter - 1); continuityCounters.put(pid, continuityCounter); if (previousCounter == continuityCounter) { - if (payloadExists) { - // Duplicate packet found. - tsPacketBuffer.setPosition(endOfPacket); - return RESULT_CONTINUE; - } - } else if (continuityCounter != (previousCounter + 1) % 16) { - discontinuityFound = true; + // Duplicate packet found. + tsPacketBuffer.setPosition(endOfPacket); + return RESULT_CONTINUE; + } else if (continuityCounter != ((previousCounter + 1) & 0xF)) { + // Discontinuity found. + payloadReader.seek(); } } // Skip the adaptation field. if (adaptationFieldExists) { int adaptationFieldLength = tsPacketBuffer.readUnsignedByte(); - tsPacketBuffer.skipBytes(adaptationFieldLength); + int adaptationFieldFlags = tsPacketBuffer.readUnsignedByte(); + + packetHeaderFlags |= + (adaptationFieldFlags & 0x40) != 0 // random_access_indicator. + ? TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR + : 0; + tsPacketBuffer.skipBytes(adaptationFieldLength - 1 /* flags */); } // Read the payload. - if (payloadExists) { - TsPayloadReader payloadReader = tsPayloadReaders.get(pid); - if (payloadReader != null) { - if (discontinuityFound) { - payloadReader.seek(); - } - tsPacketBuffer.setLimit(endOfPacket); - payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator); - Assertions.checkState(tsPacketBuffer.getPosition() <= endOfPacket); - tsPacketBuffer.setLimit(limit); - } + boolean wereTracksEnded = tracksEnded; + if (shouldConsumePacketPayload(pid)) { + tsPacketBuffer.setLimit(endOfPacket); + payloadReader.consume(tsPacketBuffer, packetHeaderFlags); + tsPacketBuffer.setLimit(limit); + } + if (mode != MODE_HLS && !wereTracksEnded && tracksEnded && inputLength != C.LENGTH_UNSET) { + // We have read all tracks from all PMTs in this non-live stream. Now seek to the beginning + // and read again to make sure we output all media, including any contained in packets prior + // to those containing the track information. + pendingSeekToStart = true; } tsPacketBuffer.setPosition(endOfPacket); @@ -296,6 +351,78 @@ public final class TsExtractor implements Extractor { // Internals. + private void maybeOutputSeekMap(long inputLength) { + if (!hasOutputSeekMap) { + hasOutputSeekMap = true; + if (durationReader.getDurationUs() != C.TIME_UNSET) { + tsBinarySearchSeeker = + new TsBinarySearchSeeker( + durationReader.getPcrTimestampAdjuster(), + durationReader.getDurationUs(), + inputLength, + pcrPid); + output.seekMap(tsBinarySearchSeeker.getSeekMap()); + } else { + output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs())); + } + } + } + + private boolean fillBufferWithAtLeastOnePacket(ExtractorInput input) + throws IOException, InterruptedException { + byte[] data = tsPacketBuffer.data; + // Shift bytes to the start of the buffer if there isn't enough space left at the end. + if (BUFFER_SIZE - tsPacketBuffer.getPosition() < TS_PACKET_SIZE) { + int bytesLeft = tsPacketBuffer.bytesLeft(); + if (bytesLeft > 0) { + System.arraycopy(data, tsPacketBuffer.getPosition(), data, 0, bytesLeft); + } + tsPacketBuffer.reset(data, bytesLeft); + } + // Read more bytes until we have at least one packet. + while (tsPacketBuffer.bytesLeft() < TS_PACKET_SIZE) { + int limit = tsPacketBuffer.limit(); + int read = input.read(data, limit, BUFFER_SIZE - limit); + if (read == C.RESULT_END_OF_INPUT) { + return false; + } + tsPacketBuffer.setLimit(limit + read); + } + return true; + } + + /** + * Returns the position of the end of the first TS packet (exclusive) in the packet buffer. + * + *

This may be a position beyond the buffer limit if the packet has not been read fully into + * the buffer, or if no packet could be found within the buffer. + */ + private int findEndOfFirstTsPacketInBuffer() throws ParserException { + int searchStart = tsPacketBuffer.getPosition(); + int limit = tsPacketBuffer.limit(); + int syncBytePosition = TsUtil.findSyncBytePosition(tsPacketBuffer.data, searchStart, limit); + // Discard all bytes before the sync byte. + // If sync byte is not found, this means discard the whole buffer. + tsPacketBuffer.setPosition(syncBytePosition); + int endOfPacket = syncBytePosition + TS_PACKET_SIZE; + if (endOfPacket > limit) { + bytesSinceLastSync += syncBytePosition - searchStart; + if (mode == MODE_HLS && bytesSinceLastSync > TS_PACKET_SIZE * 2) { + throw new ParserException("Cannot find sync byte. Most likely not a Transport Stream."); + } + } else { + // We have found a packet within the buffer. + bytesSinceLastSync = 0; + } + return endOfPacket; + } + + private boolean shouldConsumePacketPayload(int packetPid) { + return mode == MODE_HLS + || tracksEnded + || !trackPids.get(packetPid, /* valueIfKeyNotFound= */ false); // It's a PSI packet + } + private void resetPayloadReaders() { trackIds.clear(); tsPayloadReaders.clear(); @@ -368,13 +495,20 @@ public final class TsExtractor implements Extractor { private static final int TS_PMT_DESC_AC3 = 0x6A; private static final int TS_PMT_DESC_EAC3 = 0x7A; private static final int TS_PMT_DESC_DTS = 0x7B; + private static final int TS_PMT_DESC_DVB_EXT = 0x7F; private static final int TS_PMT_DESC_DVBSUBS = 0x59; + private static final int TS_PMT_DESC_DVB_EXT_AC4 = 0x15; + private final ParsableBitArray pmtScratch; + private final SparseArray trackIdToReaderScratch; + private final SparseIntArray trackIdToPidScratch; private final int pid; public PmtReader(int pid) { pmtScratch = new ParsableBitArray(new byte[5]); + trackIdToReaderScratch = new SparseArray<>(); + trackIdToPidScratch = new SparseIntArray(); this.pid = pid; } @@ -404,9 +538,16 @@ public final class TsExtractor implements Extractor { // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12) sectionData.skipBytes(2); int programNumber = sectionData.readUnsignedShort(); + + // Skip 3 bytes (24 bits), including: // reserved (2), version_number (5), current_next_indicator (1), section_number (8), - // last_section_number (8), reserved (3), PCR_PID (13) - sectionData.skipBytes(5); + // last_section_number (8) + sectionData.skipBytes(3); + + sectionData.readBytes(pmtScratch, 2); + // reserved (3), PCR_PID (13) + pmtScratch.skipBits(3); + pcrPid = pmtScratch.readBits(13); // Read program_info_length. sectionData.readBytes(pmtScratch, 2); @@ -419,12 +560,14 @@ public final class TsExtractor implements Extractor { if (mode == MODE_HLS && id3Reader == null) { // Setup an ID3 track regardless of whether there's a corresponding entry, in case one // appears intermittently during playback. See [Internal: b/20261500]. - EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, null, new byte[0]); + EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, null, Util.EMPTY_BYTE_ARRAY); id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, dummyEsInfo); id3Reader.init(timestampAdjuster, output, new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); } + trackIdToReaderScratch.clear(); + trackIdToPidScratch.clear(); int remainingEntriesLength = sectionData.bytesLeft(); while (remainingEntriesLength > 0) { sectionData.readBytes(pmtScratch, 5); @@ -443,23 +586,32 @@ public final class TsExtractor implements Extractor { if (trackIds.get(trackId)) { continue; } - trackIds.put(trackId, true); - TsPayloadReader reader; - if (mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3) { - reader = id3Reader; - } else { - reader = payloadReaderFactory.createPayloadReader(streamType, esInfo); - if (reader != null) { + TsPayloadReader reader = mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 ? id3Reader + : payloadReaderFactory.createPayloadReader(streamType, esInfo); + if (mode != MODE_HLS + || elementaryPid < trackIdToPidScratch.get(trackId, MAX_PID_PLUS_ONE)) { + trackIdToPidScratch.put(trackId, elementaryPid); + trackIdToReaderScratch.put(trackId, reader); + } + } + + int trackIdCount = trackIdToPidScratch.size(); + for (int i = 0; i < trackIdCount; i++) { + int trackId = trackIdToPidScratch.keyAt(i); + int trackPid = trackIdToPidScratch.valueAt(i); + trackIds.put(trackId, true); + trackPids.put(trackPid, true); + TsPayloadReader reader = trackIdToReaderScratch.valueAt(i); + if (reader != null) { + if (reader != id3Reader) { reader.init(timestampAdjuster, output, new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE)); } - } - - if (reader != null) { - tsPayloadReaders.put(elementaryPid, reader); + tsPayloadReaders.put(trackPid, reader); } } + if (mode == MODE_HLS) { if (!tracksEnded) { output.endTracks(); @@ -500,6 +652,8 @@ public final class TsExtractor implements Extractor { streamType = TS_STREAM_TYPE_AC3; } else if (formatIdentifier == E_AC3_FORMAT_IDENTIFIER) { streamType = TS_STREAM_TYPE_E_AC3; + } else if (formatIdentifier == AC4_FORMAT_IDENTIFIER) { + streamType = TS_STREAM_TYPE_AC4; } else if (formatIdentifier == HEVC_FORMAT_IDENTIFIER) { streamType = TS_STREAM_TYPE_H265; } @@ -507,6 +661,13 @@ public final class TsExtractor implements Extractor { streamType = TS_STREAM_TYPE_AC3; } else if (descriptorTag == TS_PMT_DESC_EAC3) { // enhanced_AC-3_descriptor streamType = TS_STREAM_TYPE_E_AC3; + } else if (descriptorTag == TS_PMT_DESC_DVB_EXT) { + // Extension descriptor in DVB (ETSI EN 300 468). + int descriptorTagExt = data.readUnsignedByte(); + if (descriptorTagExt == TS_PMT_DESC_DVB_EXT_AC4) { + // AC-4_descriptor in DVB (ETSI EN 300 468). + streamType = TS_STREAM_TYPE_AC4; + } } else if (descriptorTag == TS_PMT_DESC_DTS) { // DTS_descriptor streamType = TS_STREAM_TYPE_DTS; } else if (descriptorTag == TS_PMT_DESC_ISO639_LANG) { @@ -534,5 +695,4 @@ public final class TsExtractor implements Extractor { } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index 25ccecd3bfbd..940c1c793777 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -16,10 +16,15 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.List; @@ -76,8 +81,10 @@ public interface TsPayloadReader { byte[] descriptorBytes) { this.streamType = streamType; this.language = language; - this.dvbSubtitleInfos = dvbSubtitleInfos == null ? Collections.emptyList() - : Collections.unmodifiableList(dvbSubtitleInfos); + this.dvbSubtitleInfos = + dvbSubtitleInfos == null + ? Collections.emptyList() + : Collections.unmodifiableList(dvbSubtitleInfos); this.descriptorBytes = descriptorBytes; } @@ -93,7 +100,7 @@ public interface TsPayloadReader { public final byte[] initializationData; /** - * @param language The ISO 639-2 three character language. + * @param language The ISO 639-2 three-letter language code. * @param type The subtitling type. * @param initializationData The composition and ancillary page ids. */ @@ -171,6 +178,29 @@ public interface TsPayloadReader { } + /** + * Contextual flags indicating the presence of indicators in the TS packet or PES packet headers. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + FLAG_PAYLOAD_UNIT_START_INDICATOR, + FLAG_RANDOM_ACCESS_INDICATOR, + FLAG_DATA_ALIGNMENT_INDICATOR + }) + @interface Flags {} + + /** Indicates the presence of the payload_unit_start_indicator in the TS packet header. */ + int FLAG_PAYLOAD_UNIT_START_INDICATOR = 1; + /** + * Indicates the presence of the random_access_indicator in the TS packet header adaptation field. + */ + int FLAG_RANDOM_ACCESS_INDICATOR = 1 << 1; + /** Indicates the presence of the data_alignment_indicator in the PES header. */ + int FLAG_DATA_ALIGNMENT_INDICATOR = 1 << 2; + /** * Initializes the payload reader. * @@ -184,10 +214,10 @@ public interface TsPayloadReader { /** * Notifies the reader that a seek has occurred. - *

- * Following a call to this method, the data passed to the next invocation of - * {@link #consume(ParsableByteArray, boolean)} will not be a continuation of the data that was - * previously passed. Hence the reader should reset any internal state. + * + *

Following a call to this method, the data passed to the next invocation of {@link #consume} + * will not be a continuation of the data that was previously passed. Hence the reader should + * reset any internal state. */ void seek(); @@ -195,8 +225,8 @@ public interface TsPayloadReader { * Consumes the payload of a TS packet. * * @param data The TS packet. The position will be set to the start of the payload. - * @param payloadUnitStartIndicator Whether payloadUnitStartIndicator was set on the TS packet. + * @param flags See {@link Flags}. + * @throws ParserException If the payload could not be parsed. */ - void consume(ParsableByteArray data, boolean payloadUnitStartIndicator); - + void consume(ParsableByteArray data, @Flags int flags) throws ParserException; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsUtil.java new file mode 100644 index 000000000000..8cd24ff1e9ae --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsUtil.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; + +/** Utilities method for extracting MPEG-TS streams. */ +public final class TsUtil { + /** + * Returns the position of the first TS_SYNC_BYTE within the range [startPosition, limitPosition) + * from the provided data array, or returns limitPosition if sync byte could not be found. + */ + public static int findSyncBytePosition(byte[] data, int startPosition, int limitPosition) { + int position = startPosition; + while (position < limitPosition && data[position] != TsExtractor.TS_SYNC_BYTE) { + position++; + } + return position; + } + + /** + * Returns the PCR value read from a given TS packet. + * + * @param packetBuffer The buffer that holds the packet. + * @param startOfPacket The starting position of the packet in the buffer. + * @param pcrPid The PID for valid packets that contain PCR values. + * @return The PCR value read from the packet, if its PID is equal to {@code pcrPid} and it + * contains a valid PCR value. Returns {@link C#TIME_UNSET} otherwise. + */ + public static long readPcrFromPacket( + ParsableByteArray packetBuffer, int startOfPacket, int pcrPid) { + packetBuffer.setPosition(startOfPacket); + if (packetBuffer.bytesLeft() < 5) { + // Header = 4 bytes, adaptationFieldLength = 1 byte. + return C.TIME_UNSET; + } + // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format. + int tsPacketHeader = packetBuffer.readInt(); + if ((tsPacketHeader & 0x800000) != 0) { + // transport_error_indicator != 0 means there are uncorrectable errors in this packet. + return C.TIME_UNSET; + } + int pid = (tsPacketHeader & 0x1FFF00) >> 8; + if (pid != pcrPid) { + return C.TIME_UNSET; + } + boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0; + if (!adaptationFieldExists) { + return C.TIME_UNSET; + } + + int adaptationFieldLength = packetBuffer.readUnsignedByte(); + if (adaptationFieldLength >= 7 && packetBuffer.bytesLeft() >= 7) { + int flags = packetBuffer.readUnsignedByte(); + boolean pcrFlagSet = (flags & 0x10) == 0x10; + if (pcrFlagSet) { + byte[] pcrBytes = new byte[6]; + packetBuffer.readBytes(pcrBytes, /* offset= */ 0, pcrBytes.length); + return readPcrValueFromPcrBytes(pcrBytes); + } + } + return C.TIME_UNSET; + } + + /** + * Returns the value of PCR base - first 33 bits in big endian order from the PCR bytes. + * + *

We ignore PCR Ext, because it's too small to have any significance. + */ + private static long readPcrValueFromPcrBytes(byte[] pcrBytes) { + return (pcrBytes[0] & 0xFFL) << 25 + | (pcrBytes[1] & 0xFFL) << 17 + | (pcrBytes[2] & 0xFFL) << 9 + | (pcrBytes[3] & 0xFFL) << 1 + | (pcrBytes[4] & 0xFFL) >> 7; + } + + private TsUtil() { + // Prevent instantiation. + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/UserDataReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/UserDataReader.java new file mode 100644 index 000000000000..fb56fe379ce7 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/UserDataReader.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea.CeaUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.List; + +/** Consumes user data, outputting contained CEA-608/708 messages to a {@link TrackOutput}. */ +/* package */ final class UserDataReader { + + private static final int USER_DATA_START_CODE = 0x0001B2; + + private final List closedCaptionFormats; + private final TrackOutput[] outputs; + + public UserDataReader(List closedCaptionFormats) { + this.closedCaptionFormats = closedCaptionFormats; + outputs = new TrackOutput[closedCaptionFormats.size()]; + } + + public void createTracks( + ExtractorOutput extractorOutput, TsPayloadReader.TrackIdGenerator idGenerator) { + for (int i = 0; i < outputs.length; i++) { + idGenerator.generateNewId(); + TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); + Format channelFormat = closedCaptionFormats.get(i); + String channelMimeType = channelFormat.sampleMimeType; + Assertions.checkArgument( + MimeTypes.APPLICATION_CEA608.equals(channelMimeType) + || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), + "Invalid closed caption mime type provided: " + channelMimeType); + output.format( + Format.createTextSampleFormat( + idGenerator.getFormatId(), + channelMimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + channelFormat.selectionFlags, + channelFormat.language, + channelFormat.accessibilityChannel, + /* drmInitData= */ null, + Format.OFFSET_SAMPLE_RELATIVE, + channelFormat.initializationData)); + outputs[i] = output; + } + } + + public void consume(long pesTimeUs, ParsableByteArray userDataPayload) { + if (userDataPayload.bytesLeft() < 9) { + return; + } + int userDataStartCode = userDataPayload.readInt(); + int userDataIdentifier = userDataPayload.readInt(); + int userDataTypeCode = userDataPayload.readUnsignedByte(); + if (userDataStartCode == USER_DATA_START_CODE + && userDataIdentifier == CeaUtil.USER_DATA_IDENTIFIER_GA94 + && userDataTypeCode == CeaUtil.USER_DATA_TYPE_CODE_MPEG_CC) { + CeaUtil.consumeCcData(pesTimeUs, userDataPayload, outputs); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index 20e35515dc9a..d4ac3ef8e15a 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -15,42 +15,50 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.wav; +import android.util.Pair; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.WavUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** {@link Extractor} to extract samples from a WAV byte stream. */ -public final class WavExtractor implements Extractor, SeekMap { +/** + * Extracts data from WAV byte streams. + */ +public final class WavExtractor implements Extractor { /** - * Factory for {@link WavExtractor} instances. + * When outputting PCM data to a {@link TrackOutput}, we can choose how many frames are grouped + * into each sample, and hence each sample's duration. This is the target number of samples to + * output for each second of media, meaning that each sample will have a duration of ~100ms. */ - public static final ExtractorsFactory FACTORY = new ExtractorsFactory() { + private static final int TARGET_SAMPLES_PER_SECOND = 10; - @Override - public Extractor[] createExtractors() { - return new Extractor[] {new WavExtractor()}; - } + /** Factory for {@link WavExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()}; - }; + @MonotonicNonNull private ExtractorOutput extractorOutput; + @MonotonicNonNull private TrackOutput trackOutput; + @MonotonicNonNull private OutputWriter outputWriter; + private int dataStartPosition; + private long dataEndPosition; - /** Arbitrary maximum input size of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */ - private static final int MAX_INPUT_SIZE = 32 * 1024; - - private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; - private WavHeader wavHeader; - private int bytesPerFrame; - private int pendingBytes; + public WavExtractor() { + dataStartPosition = C.POSITION_UNSET; + dataEndPosition = C.POSITION_UNSET; + } @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { @@ -61,13 +69,14 @@ public final class WavExtractor implements Extractor, SeekMap { public void init(ExtractorOutput output) { extractorOutput = output; trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); - wavHeader = null; output.endTracks(); } @Override public void seek(long position, long timeUs) { - pendingBytes = 0; + if (outputWriter != null) { + outputWriter.reset(timeUs); + } } @Override @@ -78,55 +87,476 @@ public final class WavExtractor implements Extractor, SeekMap { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { - if (wavHeader == null) { - wavHeader = WavHeaderReader.peek(input); - if (wavHeader == null) { + assertInitialized(); + if (outputWriter == null) { + WavHeader header = WavHeaderReader.peek(input); + if (header == null) { // Should only happen if the media wasn't sniffed. throw new ParserException("Unsupported or unrecognized wav header."); } - Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, - wavHeader.getBitrate(), MAX_INPUT_SIZE, wavHeader.getNumChannels(), - wavHeader.getSampleRateHz(), wavHeader.getEncoding(), null, null, 0, null); + + if (header.formatType == WavUtil.TYPE_IMA_ADPCM) { + outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, header); + } else if (header.formatType == WavUtil.TYPE_ALAW) { + outputWriter = + new PassthroughOutputWriter( + extractorOutput, + trackOutput, + header, + MimeTypes.AUDIO_ALAW, + /* pcmEncoding= */ Format.NO_VALUE); + } else if (header.formatType == WavUtil.TYPE_MLAW) { + outputWriter = + new PassthroughOutputWriter( + extractorOutput, + trackOutput, + header, + MimeTypes.AUDIO_MLAW, + /* pcmEncoding= */ Format.NO_VALUE); + } else { + @C.PcmEncoding + int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample); + if (pcmEncoding == C.ENCODING_INVALID) { + throw new ParserException("Unsupported WAV format type: " + header.formatType); + } + outputWriter = + new PassthroughOutputWriter( + extractorOutput, trackOutput, header, MimeTypes.AUDIO_RAW, pcmEncoding); + } + } + + if (dataStartPosition == C.POSITION_UNSET) { + Pair dataBounds = WavHeaderReader.skipToData(input); + dataStartPosition = dataBounds.first.intValue(); + dataEndPosition = dataBounds.second; + outputWriter.init(dataStartPosition, dataEndPosition); + } else if (input.getPosition() == 0) { + input.skipFully(dataStartPosition); + } + + Assertions.checkState(dataEndPosition != C.POSITION_UNSET); + long bytesLeft = dataEndPosition - input.getPosition(); + return outputWriter.sampleData(input, bytesLeft) ? RESULT_END_OF_INPUT : RESULT_CONTINUE; + } + + @EnsuresNonNull({"extractorOutput", "trackOutput"}) + private void assertInitialized() { + Assertions.checkStateNotNull(trackOutput); + Util.castNonNull(extractorOutput); + } + + /** Writes to the extractor's output. */ + private interface OutputWriter { + + /** + * Resets the writer. + * + * @param timeUs The new start position in microseconds. + */ + void reset(long timeUs); + + /** + * Initializes the writer. + * + *

Must be called once, before any calls to {@link #sampleData(ExtractorInput, long)}. + * + * @param dataStartPosition The byte position (inclusive) in the stream at which data starts. + * @param dataEndPosition The end position (exclusive) in the stream at which data ends. + * @throws ParserException If an error occurs initializing the writer. + */ + void init(int dataStartPosition, long dataEndPosition) throws ParserException; + + /** + * Consumes sample data from {@code input}, writing corresponding samples to the extractor's + * output. + * + *

Must not be called until after {@link #init(int, long)} has been called. + * + * @param input The input from which to read. + * @param bytesLeft The number of sample data bytes left to be read from the input. + * @return Whether the end of the sample data has been reached. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + boolean sampleData(ExtractorInput input, long bytesLeft) + throws IOException, InterruptedException; + } + + private static final class PassthroughOutputWriter implements OutputWriter { + + private final ExtractorOutput extractorOutput; + private final TrackOutput trackOutput; + private final WavHeader header; + private final Format format; + /** The target size of each output sample, in bytes. */ + private final int targetSampleSizeBytes; + + /** The time at which the writer was last {@link #reset}. */ + private long startTimeUs; + /** + * The number of bytes that have been written to {@link #trackOutput} but have yet to be + * included as part of a sample (i.e. the corresponding call to {@link + * TrackOutput#sampleMetadata} has yet to be made). + */ + private int pendingOutputBytes; + /** + * The total number of frames in samples that have been written to the trackOutput since the + * last call to {@link #reset}. + */ + private long outputFrameCount; + + public PassthroughOutputWriter( + ExtractorOutput extractorOutput, + TrackOutput trackOutput, + WavHeader header, + String mimeType, + @C.PcmEncoding int pcmEncoding) + throws ParserException { + this.extractorOutput = extractorOutput; + this.trackOutput = trackOutput; + this.header = header; + + int bytesPerFrame = header.numChannels * header.bitsPerSample / 8; + // Validate the header. Blocks are expected to correspond to single frames. + if (header.blockSize != bytesPerFrame) { + throw new ParserException( + "Expected block size: " + bytesPerFrame + "; got: " + header.blockSize); + } + + targetSampleSizeBytes = + Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND); + format = + Format.createAudioSampleFormat( + /* id= */ null, + mimeType, + /* codecs= */ null, + /* bitrate= */ header.frameRateHz * bytesPerFrame * 8, + /* maxInputSize= */ targetSampleSizeBytes, + header.numChannels, + header.frameRateHz, + pcmEncoding, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + } + + @Override + public void reset(long timeUs) { + startTimeUs = timeUs; + pendingOutputBytes = 0; + outputFrameCount = 0; + } + + @Override + public void init(int dataStartPosition, long dataEndPosition) { + extractorOutput.seekMap( + new WavSeekMap(header, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition)); trackOutput.format(format); - bytesPerFrame = wavHeader.getBytesPerFrame(); } - if (!wavHeader.hasDataBounds()) { - WavHeaderReader.skipToData(input, wavHeader); - extractorOutput.seekMap(this); - } + @Override + public boolean sampleData(ExtractorInput input, long bytesLeft) + throws IOException, InterruptedException { + // Write sample data until we've reached the target sample size, or the end of the data. + while (bytesLeft > 0 && pendingOutputBytes < targetSampleSizeBytes) { + int bytesToRead = (int) Math.min(targetSampleSizeBytes - pendingOutputBytes, bytesLeft); + int bytesAppended = trackOutput.sampleData(input, bytesToRead, true); + if (bytesAppended == RESULT_END_OF_INPUT) { + bytesLeft = 0; + } else { + pendingOutputBytes += bytesAppended; + bytesLeft -= bytesAppended; + } + } - int bytesAppended = trackOutput.sampleData(input, MAX_INPUT_SIZE - pendingBytes, true); - if (bytesAppended != RESULT_END_OF_INPUT) { - pendingBytes += bytesAppended; - } + // Write the corresponding sample metadata. Samples must be a whole number of frames. It's + // possible that the number of pending output bytes is not a whole number of frames if the + // stream ended unexpectedly. + int bytesPerFrame = header.blockSize; + int pendingFrames = pendingOutputBytes / bytesPerFrame; + if (pendingFrames > 0) { + long timeUs = + startTimeUs + + Util.scaleLargeTimestamp( + outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); + int size = pendingFrames * bytesPerFrame; + int offset = pendingOutputBytes - size; + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); + outputFrameCount += pendingFrames; + pendingOutputBytes = offset; + } - // Samples must consist of a whole number of frames. - int pendingFrames = pendingBytes / bytesPerFrame; - if (pendingFrames > 0) { - long timeUs = wavHeader.getTimeUs(input.getPosition() - pendingBytes); - int size = pendingFrames * bytesPerFrame; - pendingBytes -= size; - trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, size, pendingBytes, null); + return bytesLeft <= 0; } - - return bytesAppended == RESULT_END_OF_INPUT ? RESULT_END_OF_INPUT : RESULT_CONTINUE; } - // SeekMap implementation. + private static final class ImaAdPcmOutputWriter implements OutputWriter { - @Override - public long getDurationUs() { - return wavHeader.getDurationUs(); - } + private static final int[] INDEX_TABLE = { + -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 + }; - @Override - public boolean isSeekable() { - return true; - } + private static final int[] STEP_TABLE = { + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, + 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, + 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, + 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, + 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, + 32767 + }; - @Override - public long getPosition(long timeUs) { - return wavHeader.getPosition(timeUs); + private final ExtractorOutput extractorOutput; + private final TrackOutput trackOutput; + private final WavHeader header; + + /** Number of frames per block of the input (yet to be decoded) data. */ + private final int framesPerBlock; + /** Target for the input (yet to be decoded) data. */ + private final byte[] inputData; + /** Target for decoded (yet to be output) data. */ + private final ParsableByteArray decodedData; + /** The target size of each output sample, in frames. */ + private final int targetSampleSizeFrames; + /** The output format. */ + private final Format format; + + /** The number of pending bytes in {@link #inputData}. */ + private int pendingInputBytes; + /** The time at which the writer was last {@link #reset}. */ + private long startTimeUs; + /** + * The number of bytes that have been written to {@link #trackOutput} but have yet to be + * included as part of a sample (i.e. the corresponding call to {@link + * TrackOutput#sampleMetadata} has yet to be made). + */ + private int pendingOutputBytes; + /** + * The total number of frames in samples that have been written to the trackOutput since the + * last call to {@link #reset}. + */ + private long outputFrameCount; + + public ImaAdPcmOutputWriter( + ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header) + throws ParserException { + this.extractorOutput = extractorOutput; + this.trackOutput = trackOutput; + this.header = header; + targetSampleSizeFrames = Math.max(1, header.frameRateHz / TARGET_SAMPLES_PER_SECOND); + + ParsableByteArray scratch = new ParsableByteArray(header.extraData); + scratch.readLittleEndianUnsignedShort(); + framesPerBlock = scratch.readLittleEndianUnsignedShort(); + + int numChannels = header.numChannels; + // Validate the header. This calculation is defined in "Microsoft Multimedia Standards Update + // - New Multimedia Types and Data Techniques" (1994). See the "IMA ADPCM Wave Type" and "DVI + // ADPCM Wave Type" sections, and the calculation of wSamplesPerBlock in the latter. + int expectedFramesPerBlock = + (((header.blockSize - (4 * numChannels)) * 8) / (header.bitsPerSample * numChannels)) + 1; + if (framesPerBlock != expectedFramesPerBlock) { + throw new ParserException( + "Expected frames per block: " + expectedFramesPerBlock + "; got: " + framesPerBlock); + } + + // Calculate the number of blocks we'll need to decode to obtain an output sample of the + // target sample size, and allocate suitably sized buffers for input and decoded data. + int maxBlocksToDecode = Util.ceilDivide(targetSampleSizeFrames, framesPerBlock); + inputData = new byte[maxBlocksToDecode * header.blockSize]; + decodedData = + new ParsableByteArray( + maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock, numChannels)); + + // Create the format. We calculate the bitrate of the data before decoding, since this is the + // bitrate of the stream itself. + int bitrate = header.frameRateHz * header.blockSize * 8 / framesPerBlock; + format = + Format.createAudioSampleFormat( + /* id= */ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + bitrate, + /* maxInputSize= */ numOutputFramesToBytes(targetSampleSizeFrames, numChannels), + header.numChannels, + header.frameRateHz, + C.ENCODING_PCM_16BIT, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + } + + @Override + public void reset(long timeUs) { + pendingInputBytes = 0; + startTimeUs = timeUs; + pendingOutputBytes = 0; + outputFrameCount = 0; + } + + @Override + public void init(int dataStartPosition, long dataEndPosition) { + extractorOutput.seekMap( + new WavSeekMap(header, framesPerBlock, dataStartPosition, dataEndPosition)); + trackOutput.format(format); + } + + @Override + public boolean sampleData(ExtractorInput input, long bytesLeft) + throws IOException, InterruptedException { + // Calculate the number of additional frames that we need on the output side to complete a + // sample of the target size. + int targetFramesRemaining = + targetSampleSizeFrames - numOutputBytesToFrames(pendingOutputBytes); + // Calculate the whole number of blocks that we need to decode to obtain this many frames. + int blocksToDecode = Util.ceilDivide(targetFramesRemaining, framesPerBlock); + int targetReadBytes = blocksToDecode * header.blockSize; + + // Read input data until we've reached the target number of blocks, or the end of the data. + boolean endOfSampleData = bytesLeft == 0; + while (!endOfSampleData && pendingInputBytes < targetReadBytes) { + int bytesToRead = (int) Math.min(targetReadBytes - pendingInputBytes, bytesLeft); + int bytesAppended = input.read(inputData, pendingInputBytes, bytesToRead); + if (bytesAppended == RESULT_END_OF_INPUT) { + endOfSampleData = true; + } else { + pendingInputBytes += bytesAppended; + } + } + + int pendingBlockCount = pendingInputBytes / header.blockSize; + if (pendingBlockCount > 0) { + // We have at least one whole block to decode. + decode(inputData, pendingBlockCount, decodedData); + pendingInputBytes -= pendingBlockCount * header.blockSize; + + // Write all of the decoded data to the track output. + int decodedDataSize = decodedData.limit(); + trackOutput.sampleData(decodedData, decodedDataSize); + pendingOutputBytes += decodedDataSize; + + // Output the next sample at the target size. + int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes); + if (pendingOutputFrames >= targetSampleSizeFrames) { + writeSampleMetadata(targetSampleSizeFrames); + } + } + + // If we've reached the end of the data, we might need to output a final partial sample. + if (endOfSampleData) { + int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes); + if (pendingOutputFrames > 0) { + writeSampleMetadata(pendingOutputFrames); + } + } + + return endOfSampleData; + } + + private void writeSampleMetadata(int sampleFrames) { + long timeUs = + startTimeUs + + Util.scaleLargeTimestamp(outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); + int size = numOutputFramesToBytes(sampleFrames); + int offset = pendingOutputBytes - size; + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); + outputFrameCount += sampleFrames; + pendingOutputBytes -= size; + } + + /** + * Decodes IMA ADPCM data to 16 bit PCM. + * + * @param input The input data to decode. + * @param blockCount The number of blocks to decode. + * @param output The output into which the decoded data will be written. + */ + private void decode(byte[] input, int blockCount, ParsableByteArray output) { + for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) { + for (int channelIndex = 0; channelIndex < header.numChannels; channelIndex++) { + decodeBlockForChannel(input, blockIndex, channelIndex, output.data); + } + } + int decodedDataSize = numOutputFramesToBytes(framesPerBlock * blockCount); + output.reset(decodedDataSize); + } + + private void decodeBlockForChannel( + byte[] input, int blockIndex, int channelIndex, byte[] output) { + int blockSize = header.blockSize; + int numChannels = header.numChannels; + + // The input data consists for a four byte header [Ci] for each of the N channels, followed + // by interleaved data segments [Ci-DATAj], each of which are four bytes long. + // + // [C1][C2]...[CN] [C1-Data0][C2-Data0]...[CN-Data0] [C1-Data1][C2-Data1]...[CN-Data1] etc + // + // Compute the start indices for the [Ci] and [Ci-Data0] for the current channel, as well as + // the number of data bytes for the channel in the block. + int blockStartIndex = blockIndex * blockSize; + int headerStartIndex = blockStartIndex + channelIndex * 4; + int dataStartIndex = headerStartIndex + numChannels * 4; + int dataSizeBytes = blockSize / numChannels - 4; + + // Decode initialization. Casting to a short is necessary for the most significant bit to be + // treated as -2^15 rather than 2^15. + int predictedSample = + (short) (((input[headerStartIndex + 1] & 0xFF) << 8) | (input[headerStartIndex] & 0xFF)); + int stepIndex = Math.min(input[headerStartIndex + 2] & 0xFF, 88); + int step = STEP_TABLE[stepIndex]; + + // Output the initial 16 bit PCM sample from the header. + int outputIndex = (blockIndex * framesPerBlock * numChannels + channelIndex) * 2; + output[outputIndex] = (byte) (predictedSample & 0xFF); + output[outputIndex + 1] = (byte) (predictedSample >> 8); + + // We examine each data byte twice during decode. + for (int i = 0; i < dataSizeBytes * 2; i++) { + int dataSegmentIndex = i / 8; + int dataSegmentOffset = (i / 2) % 4; + int dataIndex = dataStartIndex + (dataSegmentIndex * numChannels * 4) + dataSegmentOffset; + + int originalSample = input[dataIndex] & 0xFF; + if (i % 2 == 0) { + originalSample &= 0x0F; // Bottom four bits. + } else { + originalSample >>= 4; // Top four bits. + } + + int delta = originalSample & 0x07; + int difference = ((2 * delta + 1) * step) >> 3; + + if ((originalSample & 0x08) != 0) { + difference = -difference; + } + + predictedSample += difference; + predictedSample = Util.constrainValue(predictedSample, /* min= */ -32768, /* max= */ 32767); + + // Output the next 16 bit PCM sample to the correct position in the output. + outputIndex += 2 * numChannels; + output[outputIndex] = (byte) (predictedSample & 0xFF); + output[outputIndex + 1] = (byte) (predictedSample >> 8); + + stepIndex += INDEX_TABLE[originalSample]; + stepIndex = Util.constrainValue(stepIndex, /* min= */ 0, /* max= */ STEP_TABLE.length - 1); + step = STEP_TABLE[stepIndex]; + } + } + + private int numOutputBytesToFrames(int bytes) { + return bytes / (2 * header.numChannels); + } + + private int numOutputFramesToBytes(int frames) { + return numOutputFramesToBytes(frames, header.numChannels); + } + + private static int numOutputFramesToBytes(int frames, int numChannels) { + return frames * 2 * numChannels; + } } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.java index 09466aa74e45..bc6cf8999b7e 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.java @@ -15,94 +15,41 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.wav; -import org.mozilla.thirdparty.com.google.android.exoplayer2.C; - /** Header for a WAV file. */ -/*package*/ final class WavHeader { +/* package */ final class WavHeader { - /** Number of audio chanels. */ - private final int numChannels; - /** Sample rate in Hertz. */ - private final int sampleRateHz; - /** Average bytes per second for the sample data. */ - private final int averageBytesPerSecond; - /** Alignment for frames of audio data; should equal {@code numChannels * bitsPerSample / 8}. */ - private final int blockAlignment; - /** Bits per sample for the audio data. */ - private final int bitsPerSample; - /** The PCM encoding */ - @C.PcmEncoding - private final int encoding; + /** + * The format type. Standard format types are the "WAVE form Registration Number" constants + * defined in RFC 2361 Appendix A. + */ + public final int formatType; + /** The number of channels. */ + public final int numChannels; + /** The sample rate in Hertz. */ + public final int frameRateHz; + /** The average bytes per second for the sample data. */ + public final int averageBytesPerSecond; + /** The block size in bytes. */ + public final int blockSize; + /** Bits per sample for a single channel. */ + public final int bitsPerSample; + /** Extra data appended to the format chunk of the header. */ + public final byte[] extraData; - /** Offset to the start of sample data. */ - private long dataStartPosition; - /** Total size in bytes of the sample data. */ - private long dataSize; - - public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, int blockAlignment, - int bitsPerSample, @C.PcmEncoding int encoding) { + public WavHeader( + int formatType, + int numChannels, + int frameRateHz, + int averageBytesPerSecond, + int blockSize, + int bitsPerSample, + byte[] extraData) { + this.formatType = formatType; this.numChannels = numChannels; - this.sampleRateHz = sampleRateHz; + this.frameRateHz = frameRateHz; this.averageBytesPerSecond = averageBytesPerSecond; - this.blockAlignment = blockAlignment; + this.blockSize = blockSize; this.bitsPerSample = bitsPerSample; - this.encoding = encoding; + this.extraData = extraData; } - - /** Returns the duration in microseconds of this WAV. */ - public long getDurationUs() { - long numFrames = dataSize / blockAlignment; - return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz; - } - - /** Returns the bytes per frame of this WAV. */ - public int getBytesPerFrame() { - return blockAlignment; - } - - /** Returns the bitrate of this WAV. */ - public int getBitrate() { - return sampleRateHz * bitsPerSample * numChannels; - } - - /** Returns the sample rate in Hertz of this WAV. */ - public int getSampleRateHz() { - return sampleRateHz; - } - - /** Returns the number of audio channels in this WAV. */ - public int getNumChannels() { - return numChannels; - } - - /** Returns the position in bytes in this WAV for the given time in microseconds. */ - public long getPosition(long timeUs) { - long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; - // Round down to nearest frame. - long position = (unroundedPosition / blockAlignment) * blockAlignment; - return Math.min(position, dataSize - blockAlignment) + dataStartPosition; - } - - /** Returns the time in microseconds for the given position in bytes in this WAV. */ - public long getTimeUs(long position) { - return position * C.MICROS_PER_SECOND / averageBytesPerSecond; - } - - /** Returns true if the data start position and size have been set. */ - public boolean hasDataBounds() { - return dataStartPosition != 0 && dataSize != 0; - } - - /** Sets the start position and size in bytes of sample data in this WAV. */ - public void setDataBounds(long dataStartPosition, long dataSize) { - this.dataStartPosition = dataStartPosition; - this.dataSize = dataSize; - } - - /** Returns the PCM encoding. **/ - @C.PcmEncoding - public int getEncoding() { - return encoding; - } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index 95e9db684d71..1c36aaa3c3bb 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -15,25 +15,23 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.wav; -import android.util.Log; +import android.util.Pair; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.WavUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; /** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */ -/*package*/ final class WavHeaderReader { +/* package */ final class WavHeaderReader { private static final String TAG = "WavHeaderReader"; - /** Integer PCM audio data. */ - private static final int TYPE_PCM = 0x0001; - /** Extended WAVE format. */ - private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; - /** * Peeks and returns a {@code WavHeader}. * @@ -44,6 +42,7 @@ import java.io.IOException; * @return A new {@code WavHeader} peeked from {@code input}, or null if the input is not a * supported WAV format. */ + @Nullable public static WavHeader peek(ExtractorInput input) throws IOException, InterruptedException { Assertions.checkNotNull(input); @@ -52,21 +51,21 @@ import java.io.IOException; // Attempt to read the RIFF chunk. ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); - if (chunkHeader.id != Util.getIntegerCodeForString("RIFF")) { + if (chunkHeader.id != WavUtil.RIFF_FOURCC) { return null; } input.peekFully(scratch.data, 0, 4); scratch.setPosition(0); int riffFormat = scratch.readInt(); - if (riffFormat != Util.getIntegerCodeForString("WAVE")) { + if (riffFormat != WavUtil.WAVE_FOURCC) { Log.e(TAG, "Unsupported RIFF format: " + riffFormat); return null; } // Skip chunks until we find the format chunk. chunkHeader = ChunkHeader.peek(input, scratch); - while (chunkHeader.id != Util.getIntegerCodeForString("fmt ")) { + while (chunkHeader.id != WavUtil.FMT_FOURCC) { input.advancePeekPosition((int) chunkHeader.size); chunkHeader = ChunkHeader.peek(input, scratch); } @@ -74,54 +73,46 @@ import java.io.IOException; Assertions.checkState(chunkHeader.size >= 16); input.peekFully(scratch.data, 0, 16); scratch.setPosition(0); - int type = scratch.readLittleEndianUnsignedShort(); + int audioFormatType = scratch.readLittleEndianUnsignedShort(); int numChannels = scratch.readLittleEndianUnsignedShort(); - int sampleRateHz = scratch.readLittleEndianUnsignedIntToInt(); + int frameRateHz = scratch.readLittleEndianUnsignedIntToInt(); int averageBytesPerSecond = scratch.readLittleEndianUnsignedIntToInt(); - int blockAlignment = scratch.readLittleEndianUnsignedShort(); + int blockSize = scratch.readLittleEndianUnsignedShort(); int bitsPerSample = scratch.readLittleEndianUnsignedShort(); - int expectedBlockAlignment = numChannels * bitsPerSample / 8; - if (blockAlignment != expectedBlockAlignment) { - throw new ParserException("Expected block alignment: " + expectedBlockAlignment + "; got: " - + blockAlignment); + int bytesLeft = (int) chunkHeader.size - 16; + byte[] extraData; + if (bytesLeft > 0) { + extraData = new byte[bytesLeft]; + input.peekFully(extraData, 0, bytesLeft); + } else { + extraData = Util.EMPTY_BYTE_ARRAY; } - @C.PcmEncoding int encoding = Util.getPcmEncoding(bitsPerSample); - if (encoding == C.ENCODING_INVALID) { - Log.e(TAG, "Unsupported WAV bit depth: " + bitsPerSample); - return null; - } - - if (type != TYPE_PCM && type != TYPE_WAVE_FORMAT_EXTENSIBLE) { - Log.e(TAG, "Unsupported WAV format type: " + type); - return null; - } - - // If present, skip extensionSize, validBitsPerSample, channelMask, subFormatGuid, ... - input.advancePeekPosition((int) chunkHeader.size - 16); - - return new WavHeader(numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, - bitsPerSample, encoding); + return new WavHeader( + audioFormatType, + numChannels, + frameRateHz, + averageBytesPerSecond, + blockSize, + bitsPerSample, + extraData); } /** - * Skips to the data in the given WAV input stream and returns its data size. After calling, the - * input stream's position will point to the start of sample data in the WAV. - *

- * If an exception is thrown, the input position will be left pointing to a chunk header. + * Skips to the data in the given WAV input stream, and returns its bounds. After calling, the + * input stream's position will point to the start of sample data in the WAV. If an exception is + * thrown, the input position will be left pointing to a chunk header. * - * @param input Input stream to skip to the data chunk in. Its peek position must be pointing to - * a valid chunk header. - * @param wavHeader WAV header to populate with data bounds. + * @param input The input stream, whose read position must be pointing to a valid chunk header. + * @return The byte positions at which the data starts (inclusive) and ends (exclusive). * @throws ParserException If an error occurs parsing chunks. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from input. */ - public static void skipToData(ExtractorInput input, WavHeader wavHeader) + public static Pair skipToData(ExtractorInput input) throws IOException, InterruptedException { Assertions.checkNotNull(input); - Assertions.checkNotNull(wavHeader); // Make sure the peek position is set to the read position before we peek the first header. input.resetPeekPosition(); @@ -129,11 +120,13 @@ import java.io.IOException; ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); // Skip all chunks until we hit the data header. ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); - while (chunkHeader.id != Util.getIntegerCodeForString("data")) { - Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); + while (chunkHeader.id != WavUtil.DATA_FOURCC) { + if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.FMT_FOURCC) { + Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); + } long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size; // Override size of RIFF chunk, since it describes its size as the entire file. - if (chunkHeader.id == Util.getIntegerCodeForString("RIFF")) { + if (chunkHeader.id == WavUtil.RIFF_FOURCC) { bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4; } if (bytesToSkip > Integer.MAX_VALUE) { @@ -145,7 +138,18 @@ import java.io.IOException; // Skip past the "data" header. input.skipFully(ChunkHeader.SIZE_IN_BYTES); - wavHeader.setDataBounds(input.getPosition(), chunkHeader.size); + long dataStartPosition = input.getPosition(); + long dataEndPosition = dataStartPosition + chunkHeader.size; + long inputLength = input.getLength(); + if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) { + Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength); + dataEndPosition = inputLength; + } + return Pair.create(dataStartPosition, dataEndPosition); + } + + private WavHeaderReader() { + // Prevent instantiation. } /** Container for a WAV chunk header. */ @@ -175,7 +179,7 @@ import java.io.IOException; */ public static ChunkHeader peek(ExtractorInput input, ParsableByteArray scratch) throws IOException, InterruptedException { - input.peekFully(scratch.data, 0, SIZE_IN_BYTES); + input.peekFully(scratch.data, /* offset= */ 0, /* length= */ SIZE_IN_BYTES); scratch.setPosition(0); int id = scratch.readInt(); diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java new file mode 100644 index 000000000000..d14268d12041 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java @@ -0,0 +1,74 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.wav; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/* package */ final class WavSeekMap implements SeekMap { + + private final WavHeader wavHeader; + private final int framesPerBlock; + private final long firstBlockPosition; + private final long blockCount; + private final long durationUs; + + public WavSeekMap( + WavHeader wavHeader, int framesPerBlock, long dataStartPosition, long dataEndPosition) { + this.wavHeader = wavHeader; + this.framesPerBlock = framesPerBlock; + this.firstBlockPosition = dataStartPosition; + this.blockCount = (dataEndPosition - dataStartPosition) / wavHeader.blockSize; + durationUs = blockIndexToTimeUs(blockCount); + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getDurationUs() { + return durationUs; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + // Calculate the containing block index, constraining to valid indices. + long blockIndex = (timeUs * wavHeader.frameRateHz) / (C.MICROS_PER_SECOND * framesPerBlock); + blockIndex = Util.constrainValue(blockIndex, 0, blockCount - 1); + + long seekPosition = firstBlockPosition + (blockIndex * wavHeader.blockSize); + long seekTimeUs = blockIndexToTimeUs(blockIndex); + SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); + if (seekTimeUs >= timeUs || blockIndex == blockCount - 1) { + return new SeekPoints(seekPoint); + } else { + long secondBlockIndex = blockIndex + 1; + long secondSeekPosition = firstBlockPosition + (secondBlockIndex * wavHeader.blockSize); + long secondSeekTimeUs = blockIndexToTimeUs(secondBlockIndex); + SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); + return new SeekPoints(seekPoint, secondSeekPoint); + } + } + + private long blockIndexToTimeUs(long blockIndex) { + return Util.scaleLargeTimestamp( + blockIndex * framesPerBlock, C.MICROS_PER_SECOND, wavHeader.frameRateHz); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index cb77fc4f9a23..7e38c9a173b6 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -22,21 +22,26 @@ import android.media.MediaCodecInfo.AudioCapabilities; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecInfo.VideoCapabilities; -import android.util.Log; import android.util.Pair; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; -/** - * Information about a {@link MediaCodec} for a given mime type. - */ -@TargetApi(16) +/** Information about a {@link MediaCodec} for a given mime type. */ +@SuppressWarnings("InlinedApi") public final class MediaCodecInfo { public static final String TAG = "MediaCodecInfo"; + /** + * The value returned by {@link #getMaxSupportedInstances()} if the upper bound on the maximum + * number of supported instances is unknown. + */ + public static final int MAX_SUPPORTED_INSTANCES_UNKNOWN = -1; + /** * The name of the decoder. *

@@ -45,6 +50,22 @@ public final class MediaCodecInfo { */ public final String name; + /** The MIME type handled by the codec, or {@code null} if this is a passthrough codec. */ + @Nullable public final String mimeType; + + /** + * The MIME type that the codec uses for media of type {@link #mimeType}, or {@code null} if this + * is a passthrough codec. Equal to {@link #mimeType} unless the codec is known to use a + * non-standard MIME type alias. + */ + @Nullable public final String codecMimeType; + + /** + * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if not + * known. + */ + @Nullable public final CodecCapabilities capabilities; + /** * Whether the decoder supports seamless resolution switches. * @@ -61,8 +82,45 @@ public final class MediaCodecInfo { */ public final boolean tunneling; - private final String mimeType; - private final CodecCapabilities capabilities; + /** + * Whether the decoder is secure. + * + * @see CodecCapabilities#isFeatureSupported(String) + * @see CodecCapabilities#FEATURE_SecurePlayback + */ + public final boolean secure; + + /** Whether this instance describes a passthrough codec. */ + public final boolean passthrough; + + /** + * Whether the codec is hardware accelerated. + * + *

This could be an approximation as the exact information is only provided in API levels 29+. + * + * @see android.media.MediaCodecInfo#isHardwareAccelerated() + */ + public final boolean hardwareAccelerated; + + /** + * Whether the codec is software only. + * + *

This could be an approximation as the exact information is only provided in API levels 29+. + * + * @see android.media.MediaCodecInfo#isSoftwareOnly() + */ + public final boolean softwareOnly; + + /** + * Whether the codec is from the vendor. + * + *

This could be an approximation as the exact information is only provided in API levels 29+. + * + * @see android.media.MediaCodecInfo#isVendor() + */ + public final boolean vendor; + + private final boolean isVideo; /** * Creates an instance representing an audio passthrough decoder. @@ -71,7 +129,17 @@ public final class MediaCodecInfo { * @return The created instance. */ public static MediaCodecInfo newPassthroughInstance(String name) { - return new MediaCodecInfo(name, null, null); + return new MediaCodecInfo( + name, + /* mimeType= */ null, + /* codecMimeType= */ null, + /* capabilities= */ null, + /* passthrough= */ true, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ true, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false); } /** @@ -79,24 +147,68 @@ public final class MediaCodecInfo { * * @param name The name of the {@link MediaCodec}. * @param mimeType A mime type supported by the {@link MediaCodec}. - * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type. + * @param codecMimeType The MIME type that the codec uses for media of type {@code #mimeType}. + * Equal to {@code mimeType} unless the codec is known to use a non-standard MIME type alias. + * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type, or + * {@code null} if not known. + * @param hardwareAccelerated Whether the {@link MediaCodec} is hardware accelerated. + * @param softwareOnly Whether the {@link MediaCodec} is software only. + * @param vendor Whether the {@link MediaCodec} is provided by the vendor. + * @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}. + * @param forceSecure Whether {@link #secure} should be forced to {@code true}. * @return The created instance. */ - public static MediaCodecInfo newInstance(String name, String mimeType, - CodecCapabilities capabilities) { - return new MediaCodecInfo(name, mimeType, capabilities); + public static MediaCodecInfo newInstance( + String name, + String mimeType, + String codecMimeType, + @Nullable CodecCapabilities capabilities, + boolean hardwareAccelerated, + boolean softwareOnly, + boolean vendor, + boolean forceDisableAdaptive, + boolean forceSecure) { + return new MediaCodecInfo( + name, + mimeType, + codecMimeType, + capabilities, + /* passthrough= */ false, + hardwareAccelerated, + softwareOnly, + vendor, + forceDisableAdaptive, + forceSecure); } - /** - * @param name The name of the decoder. - * @param capabilities The capabilities of the decoder. - */ - private MediaCodecInfo(String name, String mimeType, CodecCapabilities capabilities) { + private MediaCodecInfo( + String name, + @Nullable String mimeType, + @Nullable String codecMimeType, + @Nullable CodecCapabilities capabilities, + boolean passthrough, + boolean hardwareAccelerated, + boolean softwareOnly, + boolean vendor, + boolean forceDisableAdaptive, + boolean forceSecure) { this.name = Assertions.checkNotNull(name); this.mimeType = mimeType; + this.codecMimeType = codecMimeType; this.capabilities = capabilities; - adaptive = capabilities != null && isAdaptive(capabilities); + this.passthrough = passthrough; + this.hardwareAccelerated = hardwareAccelerated; + this.softwareOnly = softwareOnly; + this.vendor = vendor; + adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities); tunneling = capabilities != null && isTunneling(capabilities); + secure = forceSecure || (capabilities != null && isSecure(capabilities)); + isVideo = MimeTypes.isVideo(mimeType); + } + + @Override + public String toString() { + return name; } /** @@ -110,48 +222,175 @@ public final class MediaCodecInfo { } /** - * Whether the decoder supports the given {@code codec}. If there is insufficient information to - * decide, returns true. + * Returns an upper bound on the maximum number of supported instances, or {@link + * #MAX_SUPPORTED_INSTANCES_UNKNOWN} if unknown. Applications should not expect to operate more + * instances than the returned maximum. * - * @param codec Codec string as defined in RFC 6381. - * @return True if the given codec is supported by the decoder. + * @see CodecCapabilities#getMaxSupportedInstances() */ - public boolean isCodecSupported(String codec) { - if (codec == null || mimeType == null) { + public int getMaxSupportedInstances() { + return (Util.SDK_INT < 23 || capabilities == null) + ? MAX_SUPPORTED_INSTANCES_UNKNOWN + : getMaxSupportedInstancesV23(capabilities); + } + + /** + * Returns whether the decoder may support decoding the given {@code format}. + * + * @param format The input media format. + * @return Whether the decoder may support decoding the given {@code format}. + * @throws MediaCodecUtil.DecoderQueryException Thrown if an error occurs while querying decoders. + */ + public boolean isFormatSupported(Format format) throws MediaCodecUtil.DecoderQueryException { + if (!isCodecSupported(format)) { + return false; + } + + if (isVideo) { + if (format.width <= 0 || format.height <= 0) { + return true; + } + if (Util.SDK_INT >= 21) { + return isVideoSizeAndRateSupportedV21(format.width, format.height, format.frameRate); + } else { + boolean isFormatSupported = + format.width * format.height <= MediaCodecUtil.maxH264DecodableFrameSize(); + if (!isFormatSupported) { + logNoSupport("legacyFrameSize, " + format.width + "x" + format.height); + } + return isFormatSupported; + } + } else { // Audio + return Util.SDK_INT < 21 + || ((format.sampleRate == Format.NO_VALUE + || isAudioSampleRateSupportedV21(format.sampleRate)) + && (format.channelCount == Format.NO_VALUE + || isAudioChannelCountSupportedV21(format.channelCount))); + } + } + + /** + * Whether the decoder supports the codec of the given {@code format}. If there is insufficient + * information to decide, returns true. + * + * @param format The input media format. + * @return True if the codec of the given {@code format} is supported by the decoder. + */ + public boolean isCodecSupported(Format format) { + if (format.codecs == null || mimeType == null) { return true; } - String codecMimeType = MimeTypes.getMediaMimeType(codec); + String codecMimeType = MimeTypes.getMediaMimeType(format.codecs); if (codecMimeType == null) { return true; } if (!mimeType.equals(codecMimeType)) { - logNoSupport("codec.mime " + codec + ", " + codecMimeType); + logNoSupport("codec.mime " + format.codecs + ", " + codecMimeType); return false; } - Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(codec); + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); if (codecProfileAndLevel == null) { // If we don't know any better, we assume that the profile and level are supported. return true; } + int profile = codecProfileAndLevel.first; + int level = codecProfileAndLevel.second; + if (!isVideo && profile != CodecProfileLevel.AACObjectXHE) { + // Some devices/builds underreport audio capabilities, so assume support except for xHE-AAC + // which may not be widely supported. See https://github.com/google/ExoPlayer/issues/5145. + return true; + } for (CodecProfileLevel capabilities : getProfileLevels()) { - if (capabilities.profile == codecProfileAndLevel.first - && capabilities.level >= codecProfileAndLevel.second) { + if (capabilities.profile == profile && capabilities.level >= level) { return true; } } - logNoSupport("codec.profileLevel, " + codec + ", " + codecMimeType); + logNoSupport("codec.profileLevel, " + format.codecs + ", " + codecMimeType); + return false; + } + + /** Whether the codec handles HDR10+ out-of-band metadata. */ + public boolean isHdr10PlusOutOfBandMetadataSupported() { + if (Util.SDK_INT >= 29 && MimeTypes.VIDEO_VP9.equals(mimeType)) { + for (CodecProfileLevel capabilities : getProfileLevels()) { + if (capabilities.profile == CodecProfileLevel.VP9Profile2HDR10Plus) { + return true; + } + } + } return false; } + /** + * Returns whether it may be possible to adapt to playing a different format when the codec is + * configured to play media in the specified {@code format}. For adaptation to succeed, the codec + * must also be configured with appropriate maximum values and {@link + * #isSeamlessAdaptationSupported(Format, Format, boolean)} must return {@code true} for the + * old/new formats. + * + * @param format The format of media for which the decoder will be configured. + * @return Whether adaptation may be possible + */ + public boolean isSeamlessAdaptationSupported(Format format) { + if (isVideo) { + return adaptive; + } else { + Pair codecProfileLevel = MediaCodecUtil.getCodecProfileAndLevel(format); + return codecProfileLevel != null && codecProfileLevel.first == CodecProfileLevel.AACObjectXHE; + } + } + + /** + * Returns whether it is possible to adapt the decoder seamlessly from {@code oldFormat} to {@code + * newFormat}. If {@code newFormat} may not be completely populated, pass {@code false} for {@code + * isNewFormatComplete}. + * + * @param oldFormat The format being decoded. + * @param newFormat The new format. + * @param isNewFormatComplete Whether {@code newFormat} is populated with format-specific + * metadata. + * @return Whether it is possible to adapt the decoder seamlessly. + */ + public boolean isSeamlessAdaptationSupported( + Format oldFormat, Format newFormat, boolean isNewFormatComplete) { + if (isVideo) { + return oldFormat.sampleMimeType.equals(newFormat.sampleMimeType) + && oldFormat.rotationDegrees == newFormat.rotationDegrees + && (adaptive + || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height)) + && ((!isNewFormatComplete && newFormat.colorInfo == null) + || Util.areEqual(oldFormat.colorInfo, newFormat.colorInfo)); + } else { + if (!MimeTypes.AUDIO_AAC.equals(mimeType) + || !oldFormat.sampleMimeType.equals(newFormat.sampleMimeType) + || oldFormat.channelCount != newFormat.channelCount + || oldFormat.sampleRate != newFormat.sampleRate) { + return false; + } + // Check the codec profile levels support adaptation. + Pair oldCodecProfileLevel = + MediaCodecUtil.getCodecProfileAndLevel(oldFormat); + Pair newCodecProfileLevel = + MediaCodecUtil.getCodecProfileAndLevel(newFormat); + if (oldCodecProfileLevel == null || newCodecProfileLevel == null) { + return false; + } + int oldProfile = oldCodecProfileLevel.first; + int newProfile = newCodecProfileLevel.first; + return oldProfile == CodecProfileLevel.AACObjectXHE + && newProfile == CodecProfileLevel.AACObjectXHE; + } + } + /** * Whether the decoder supports video with a given width, height and frame rate. - *

- * Must not be called if the device SDK version is less than 21. + * + *

Must not be called if the device SDK version is less than 21. * * @param width Width in pixels. * @param height Height in pixels. - * @param frameRate Optional frame rate in frames per second. Ignored if set to - * {@link Format#NO_VALUE} or any value less than or equal to 0. + * @param frameRate Optional frame rate in frames per second. Ignored if set to {@link + * Format#NO_VALUE} or any value less than or equal to 0. * @return Whether the decoder supports video with the given width, height and frame rate. */ @TargetApi(21) @@ -165,12 +404,10 @@ public final class MediaCodecInfo { logNoSupport("sizeAndRate.vCaps"); return false; } - if (!areSizeAndRateSupported(videoCapabilities, width, height, frameRate)) { - // Capabilities are known to be inaccurately reported for vertical resolutions on some devices - // (b/31387661). If the video is vertical and the capabilities indicate support if the width - // and height are swapped, we assume that the vertical resolution is also supported. + if (!areSizeAndRateSupportedV21(videoCapabilities, width, height, frameRate)) { if (width >= height - || !areSizeAndRateSupported(videoCapabilities, height, width, frameRate)) { + || !enableRotatedVerticalResolutionWorkaround(name) + || !areSizeAndRateSupportedV21(videoCapabilities, height, width, frameRate)) { logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate); return false; } @@ -194,18 +431,13 @@ public final class MediaCodecInfo { @TargetApi(21) public Point alignVideoSizeV21(int width, int height) { if (capabilities == null) { - logNoSupport("align.caps"); return null; } VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); if (videoCapabilities == null) { - logNoSupport("align.vCaps"); return null; } - int widthAlignment = videoCapabilities.getWidthAlignment(); - int heightAlignment = videoCapabilities.getHeightAlignment(); - return new Point(Util.ceilDivide(width, widthAlignment) * widthAlignment, - Util.ceilDivide(height, heightAlignment) * heightAlignment); + return alignVideoSizeV21(videoCapabilities, width, height); } /** @@ -253,7 +485,9 @@ public final class MediaCodecInfo { logNoSupport("channelCount.aCaps"); return false; } - if (audioCapabilities.getMaxInputChannelCount() < channelCount) { + int maxInputChannelCount = adjustMaxInputChannelCount(name, mimeType, + audioCapabilities.getMaxInputChannelCount()); + if (maxInputChannelCount < channelCount) { logNoSupport("channelCount.support, " + channelCount); return false; } @@ -270,6 +504,40 @@ public final class MediaCodecInfo { + Util.DEVICE_DEBUG_INFO + "]"); } + private static int adjustMaxInputChannelCount(String name, String mimeType, int maxChannelCount) { + if (maxChannelCount > 1 || (Util.SDK_INT >= 26 && maxChannelCount > 0)) { + // The maximum channel count looks like it's been set correctly. + return maxChannelCount; + } + if (MimeTypes.AUDIO_MPEG.equals(mimeType) + || MimeTypes.AUDIO_AMR_NB.equals(mimeType) + || MimeTypes.AUDIO_AMR_WB.equals(mimeType) + || MimeTypes.AUDIO_AAC.equals(mimeType) + || MimeTypes.AUDIO_VORBIS.equals(mimeType) + || MimeTypes.AUDIO_OPUS.equals(mimeType) + || MimeTypes.AUDIO_RAW.equals(mimeType) + || MimeTypes.AUDIO_FLAC.equals(mimeType) + || MimeTypes.AUDIO_ALAW.equals(mimeType) + || MimeTypes.AUDIO_MLAW.equals(mimeType) + || MimeTypes.AUDIO_MSGSM.equals(mimeType)) { + // Platform code should have set a default. + return maxChannelCount; + } + // The maximum channel count looks incorrect. Adjust it to an assumed default. + int assumedMaxChannelCount; + if (MimeTypes.AUDIO_AC3.equals(mimeType)) { + assumedMaxChannelCount = 6; + } else if (MimeTypes.AUDIO_E_AC3.equals(mimeType)) { + assumedMaxChannelCount = 16; + } else { + // Default to the platform limit, which is 30. + assumedMaxChannelCount = 30; + } + Log.w(TAG, "AssumedMaxChannelAdjustment: " + name + ", [" + maxChannelCount + " to " + + assumedMaxChannelCount + "]"); + return assumedMaxChannelCount; + } + private static boolean isAdaptive(CodecCapabilities capabilities) { return Util.SDK_INT >= 19 && isAdaptiveV19(capabilities); } @@ -279,14 +547,6 @@ public final class MediaCodecInfo { return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback); } - @TargetApi(21) - private static boolean areSizeAndRateSupported(VideoCapabilities capabilities, int width, - int height, double frameRate) { - return frameRate == Format.NO_VALUE || frameRate <= 0 - ? capabilities.isSizeSupported(width, height) - : capabilities.areSizeAndRateSupported(width, height, frameRate); - } - private static boolean isTunneling(CodecCapabilities capabilities) { return Util.SDK_INT >= 21 && isTunnelingV21(capabilities); } @@ -296,4 +556,62 @@ public final class MediaCodecInfo { return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback); } + private static boolean isSecure(CodecCapabilities capabilities) { + return Util.SDK_INT >= 21 && isSecureV21(capabilities); + } + + @TargetApi(21) + private static boolean isSecureV21(CodecCapabilities capabilities) { + return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback); + } + + @TargetApi(21) + private static boolean areSizeAndRateSupportedV21(VideoCapabilities capabilities, int width, + int height, double frameRate) { + // Don't ever fail due to alignment. See: https://github.com/google/ExoPlayer/issues/6551. + Point alignedSize = alignVideoSizeV21(capabilities, width, height); + width = alignedSize.x; + height = alignedSize.y; + + if (frameRate == Format.NO_VALUE || frameRate <= 0) { + return capabilities.isSizeSupported(width, height); + } else { + // The signaled frame rate may be slightly higher than the actual frame rate, so we take the + // floor to avoid situations where a range check in areSizeAndRateSupported fails due to + // slightly exceeding the limits for a standard format (e.g., 1080p at 30 fps). + double floorFrameRate = Math.floor(frameRate); + return capabilities.areSizeAndRateSupported(width, height, floorFrameRate); + } + } + + @TargetApi(21) + private static Point alignVideoSizeV21(VideoCapabilities capabilities, int width, int height) { + int widthAlignment = capabilities.getWidthAlignment(); + int heightAlignment = capabilities.getHeightAlignment(); + return new Point( + Util.ceilDivide(width, widthAlignment) * widthAlignment, + Util.ceilDivide(height, heightAlignment) * heightAlignment); + } + + @TargetApi(23) + private static int getMaxSupportedInstancesV23(CodecCapabilities capabilities) { + return capabilities.getMaxSupportedInstances(); + } + + /** + * Capabilities are known to be inaccurately reported for vertical resolutions on some devices. + * [Internal ref: b/31387661]. When this workaround is enabled, we also check whether the + * capabilities indicate support if the width and height are swapped. If they do, we assume that + * the vertical resolution is also supported. + * + * @param name The name of the codec. + * @return Whether to enable the workaround. + */ + private static final boolean enableRotatedVerticalResolutionWorkaround(String name) { + if ("OMX.MTK.VIDEO.DECODER.HEVC".equals(name) && "mcv5a".equals(Util.DEVICE)) { + // See https://github.com/google/ExoPlayer/issues/6612. + return false; + } + return true; + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 4d5ed8709a83..8d2f4574fd78 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -20,10 +20,13 @@ import android.media.MediaCodec; import android.media.MediaCodec.CodecException; import android.media.MediaCodec.CryptoException; import android.media.MediaCrypto; +import android.media.MediaCryptoException; import android.media.MediaFormat; -import android.os.Looper; +import android.os.Bundle; import android.os.SystemClock; -import android.util.Log; +import androidx.annotation.CheckResult; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.BaseRenderer; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; @@ -32,27 +35,31 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderCounters; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaPeriod; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimedValueQueue; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TraceUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; /** * An abstract renderer that uses {@link MediaCodec} to decode samples for rendering. */ -@TargetApi(16) public abstract class MediaCodecRenderer extends BaseRenderer { - /** - * Thrown when a failure occurs instantiating a decoder. - */ + /** Thrown when a failure occurs instantiating a decoder. */ public static class DecoderInitializationException extends Exception { private static final int CUSTOM_ERROR_CODE_BASE = -50000; @@ -70,31 +77,75 @@ public abstract class MediaCodecRenderer extends BaseRenderer { public final boolean secureDecoderRequired; /** - * The name of the decoder that failed to initialize. Null if no suitable decoder was found. + * The {@link MediaCodecInfo} of the decoder that failed to initialize. Null if no suitable + * decoder was found. */ - public final String decoderName; + @Nullable public final MediaCodecInfo codecInfo; + + /** An optional developer-readable diagnostic information string. May be null. */ + @Nullable public final String diagnosticInfo; /** - * An optional developer-readable diagnostic information string. May be null. + * If the decoder failed to initialize and another decoder being used as a fallback also failed + * to initialize, the {@link DecoderInitializationException} for the fallback decoder. Null if + * there was no fallback decoder or no suitable decoders were found. */ - public final String diagnosticInfo; + @Nullable public final DecoderInitializationException fallbackDecoderInitializationException; public DecoderInitializationException(Format format, Throwable cause, boolean secureDecoderRequired, int errorCode) { - super("Decoder init failed: [" + errorCode + "], " + format, cause); - this.mimeType = format.sampleMimeType; - this.secureDecoderRequired = secureDecoderRequired; - this.decoderName = null; - this.diagnosticInfo = buildCustomDiagnosticInfo(errorCode); + this( + "Decoder init failed: [" + errorCode + "], " + format, + cause, + format.sampleMimeType, + secureDecoderRequired, + /* mediaCodecInfo= */ null, + buildCustomDiagnosticInfo(errorCode), + /* fallbackDecoderInitializationException= */ null); } - public DecoderInitializationException(Format format, Throwable cause, - boolean secureDecoderRequired, String decoderName) { - super("Decoder init failed: " + decoderName + ", " + format, cause); - this.mimeType = format.sampleMimeType; + public DecoderInitializationException( + Format format, + Throwable cause, + boolean secureDecoderRequired, + MediaCodecInfo mediaCodecInfo) { + this( + "Decoder init failed: " + mediaCodecInfo.name + ", " + format, + cause, + format.sampleMimeType, + secureDecoderRequired, + mediaCodecInfo, + Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null, + /* fallbackDecoderInitializationException= */ null); + } + + private DecoderInitializationException( + String message, + Throwable cause, + String mimeType, + boolean secureDecoderRequired, + @Nullable MediaCodecInfo mediaCodecInfo, + @Nullable String diagnosticInfo, + @Nullable DecoderInitializationException fallbackDecoderInitializationException) { + super(message, cause); + this.mimeType = mimeType; this.secureDecoderRequired = secureDecoderRequired; - this.decoderName = decoderName; - this.diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null; + this.codecInfo = mediaCodecInfo; + this.diagnosticInfo = diagnosticInfo; + this.fallbackDecoderInitializationException = fallbackDecoderInitializationException; + } + + @CheckResult + private DecoderInitializationException copyWithFallbackException( + DecoderInitializationException fallbackException) { + return new DecoderInitializationException( + getMessage(), + getCause(), + mimeType, + secureDecoderRequired, + codecInfo, + diagnosticInfo, + fallbackException); } @TargetApi(21) @@ -107,11 +158,39 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private static String buildCustomDiagnosticInfo(int errorCode) { String sign = errorCode < 0 ? "neg_" : ""; - return "com.google.android.exoplayer.MediaCodecTrackRenderer_" + sign + Math.abs(errorCode); + return "com.google.android.exoplayer2.mediacodec.MediaCodecRenderer_" + + sign + + Math.abs(errorCode); + } + } + + /** Thrown when a failure occurs in the decoder. */ + public static class DecoderException extends Exception { + + /** The {@link MediaCodecInfo} of the decoder that failed. Null if unknown. */ + @Nullable public final MediaCodecInfo codecInfo; + + /** An optional developer-readable diagnostic information string. May be null. */ + @Nullable public final String diagnosticInfo; + + public DecoderException(Throwable cause, @Nullable MediaCodecInfo codecInfo) { + super("Decoder failed: " + (codecInfo == null ? null : codecInfo.name), cause); + this.codecInfo = codecInfo; + diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null; } + @TargetApi(21) + private static String getDiagnosticInfoV21(Throwable cause) { + if (cause instanceof CodecException) { + return ((CodecException) cause).getDiagnosticInfo(); + } + return null; + } } + /** Indicates no codec operating rate should be set. */ + protected static final float CODEC_OPERATING_RATE_UNSET = -1; + private static final String TAG = "MediaCodecRenderer"; /** @@ -124,6 +203,39 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private static final long MAX_CODEC_HOTSWAP_TIME_MS = 1000; + /** + * The possible return values for {@link #canKeepCodec(MediaCodec, MediaCodecInfo, Format, + * Format)}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + KEEP_CODEC_RESULT_NO, + KEEP_CODEC_RESULT_YES_WITH_FLUSH, + KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION, + KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION + }) + protected @interface KeepCodecResult {} + /** The codec cannot be kept. */ + protected static final int KEEP_CODEC_RESULT_NO = 0; + /** The codec can be kept, but must be flushed. */ + protected static final int KEEP_CODEC_RESULT_YES_WITH_FLUSH = 1; + /** + * The codec can be kept. It does not need to be flushed, but must be reconfigured by prefixing + * the next input buffer with the new format's configuration data. + */ + protected static final int KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION = 2; + /** The codec can be kept. It does not need to be flushed and no reconfiguration is required. */ + protected static final int KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION = 3; + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + RECONFIGURATION_STATE_NONE, + RECONFIGURATION_STATE_WRITE_PENDING, + RECONFIGURATION_STATE_QUEUE_PENDING + }) + private @interface ReconfigurationState {} /** * There is no pending adaptive reconfiguration work. */ @@ -138,72 +250,131 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private static final int RECONFIGURATION_STATE_QUEUE_PENDING = 2; + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({DRAIN_STATE_NONE, DRAIN_STATE_SIGNAL_END_OF_STREAM, DRAIN_STATE_WAIT_END_OF_STREAM}) + private @interface DrainState {} + /** The codec is not being drained. */ + private static final int DRAIN_STATE_NONE = 0; + /** The codec needs to be drained, but we haven't signaled an end of stream to it yet. */ + private static final int DRAIN_STATE_SIGNAL_END_OF_STREAM = 1; + /** The codec needs to be drained, and we're waiting for it to output an end of stream. */ + private static final int DRAIN_STATE_WAIT_END_OF_STREAM = 2; + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + DRAIN_ACTION_NONE, + DRAIN_ACTION_FLUSH, + DRAIN_ACTION_UPDATE_DRM_SESSION, + DRAIN_ACTION_REINITIALIZE + }) + private @interface DrainAction {} + /** No special action should be taken. */ + private static final int DRAIN_ACTION_NONE = 0; + /** The codec should be flushed. */ + private static final int DRAIN_ACTION_FLUSH = 1; + /** The codec should be flushed and updated to use the pending DRM session. */ + private static final int DRAIN_ACTION_UPDATE_DRM_SESSION = 2; + /** The codec should be reinitialized. */ + private static final int DRAIN_ACTION_REINITIALIZE = 3; + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + ADAPTATION_WORKAROUND_MODE_NEVER, + ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION, + ADAPTATION_WORKAROUND_MODE_ALWAYS + }) + private @interface AdaptationWorkaroundMode {} /** - * The codec does not need to be re-initialized. + * The adaptation workaround is never used. */ - private static final int REINITIALIZATION_STATE_NONE = 0; + private static final int ADAPTATION_WORKAROUND_MODE_NEVER = 0; /** - * The input format has changed in a way that requires the codec to be re-initialized, but we - * haven't yet signaled an end of stream to the existing codec. We need to do so in order to - * ensure that it outputs any remaining buffers before we release it. + * The adaptation workaround is used when adapting between formats of the same resolution only. */ - private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1; + private static final int ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION = 1; /** - * The input format has changed in a way that requires the codec to be re-initialized, and we've - * signaled an end of stream to the existing codec. We're waiting for the codec to output an end - * of stream signal to indicate that it has output any remaining buffers before we release it. + * The adaptation workaround is always used when adapting between formats. */ - private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2; + private static final int ADAPTATION_WORKAROUND_MODE_ALWAYS = 2; /** - * H.264/AVC buffer to queue when using the adaptation workaround (see - * {@link #codecNeedsAdaptationWorkaround(String)}. Consists of three NAL units with start codes: - * Baseline sequence/picture parameter sets and a 32 * 32 pixel IDR slice. This stream can be - * queued to force a resolution change when adapting to a new format. + * H.264/AVC buffer to queue when using the adaptation workaround (see {@link + * #codecAdaptationWorkaroundMode(String)}. Consists of three NAL units with start codes: Baseline + * sequence/picture parameter sets and a 32 * 32 pixel IDR slice. This stream can be queued to + * force a resolution change when adapting to a new format. */ - private static final byte[] ADAPTATION_WORKAROUND_BUFFER = Util.getBytesFromHexString( - "0000016742C00BDA259000000168CE0F13200000016588840DCE7118A0002FBF1C31C3275D78"); + private static final byte[] ADAPTATION_WORKAROUND_BUFFER = + new byte[] { + 0, 0, 1, 103, 66, -64, 11, -38, 37, -112, 0, 0, 1, 104, -50, 15, 19, 32, 0, 0, 1, 101, -120, + -124, 13, -50, 113, 24, -96, 0, 47, -65, 28, 49, -61, 39, 93, 120 + }; + private static final int ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT = 32; private final MediaCodecSelector mediaCodecSelector; - private final DrmSessionManager drmSessionManager; + @Nullable private final DrmSessionManager drmSessionManager; private final boolean playClearSamplesWithoutKeys; + private final boolean enableDecoderFallback; + private final float assumedMinimumCodecOperatingRate; private final DecoderInputBuffer buffer; private final DecoderInputBuffer flagsOnlyBuffer; - private final FormatHolder formatHolder; - private final List decodeOnlyPresentationTimestamps; + private final TimedValueQueue formatQueue; + private final ArrayList decodeOnlyPresentationTimestamps; private final MediaCodec.BufferInfo outputBufferInfo; - private Format format; - private MediaCodec codec; - private DrmSession drmSession; - private DrmSession pendingDrmSession; - private boolean codecIsAdaptive; + private boolean drmResourcesAcquired; + @Nullable private Format inputFormat; + private Format outputFormat; + @Nullable private DrmSession codecDrmSession; + @Nullable private DrmSession sourceDrmSession; + @Nullable private MediaCrypto mediaCrypto; + private boolean mediaCryptoRequiresSecureDecoder; + private long renderTimeLimitMs; + private float rendererOperatingRate; + @Nullable private MediaCodec codec; + @Nullable private Format codecFormat; + private float codecOperatingRate; + @Nullable private ArrayDeque availableCodecInfos; + @Nullable private DecoderInitializationException preferredDecoderInitializationException; + @Nullable private MediaCodecInfo codecInfo; + @AdaptationWorkaroundMode private int codecAdaptationWorkaroundMode; + private boolean codecNeedsReconfigureWorkaround; private boolean codecNeedsDiscardToSpsWorkaround; private boolean codecNeedsFlushWorkaround; - private boolean codecNeedsAdaptationWorkaround; - private boolean codecNeedsEosPropagationWorkaround; + private boolean codecNeedsSosFlushWorkaround; private boolean codecNeedsEosFlushWorkaround; private boolean codecNeedsEosOutputExceptionWorkaround; private boolean codecNeedsMonoChannelCountWorkaround; private boolean codecNeedsAdaptationWorkaroundBuffer; private boolean shouldSkipAdaptationWorkaroundOutputBuffer; + private boolean codecNeedsEosPropagation; private ByteBuffer[] inputBuffers; private ByteBuffer[] outputBuffers; private long codecHotswapDeadlineMs; private int inputIndex; private int outputIndex; - private boolean shouldSkipOutputBuffer; + private ByteBuffer outputBuffer; + private boolean isDecodeOnlyOutputBuffer; + private boolean isLastOutputBuffer; private boolean codecReconfigured; - private int codecReconfigurationState; - private int codecReinitializationState; + @ReconfigurationState private int codecReconfigurationState; + @DrainState private int codecDrainState; + @DrainAction private int codecDrainAction; private boolean codecReceivedBuffers; private boolean codecReceivedEos; - + private boolean codecHasOutputMediaFormat; + private long largestQueuedPresentationTimeUs; + private long lastBufferInStreamPresentationTimeUs; private boolean inputStreamEnded; private boolean outputStreamEnded; private boolean waitingForKeys; - private boolean waitingForFirstSyncFrame; + private boolean waitingForFirstSyncSample; + private boolean waitingForFirstSampleInFormat; + private boolean skipMediaCodecStopOnRelease; + private boolean pendingOutputEndOfStream; protected DecoderCounters decoderCounters; @@ -218,178 +389,221 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * begin in parallel with key acquisition. This parameter specifies whether the renderer is * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is less efficient or slower + * than the primary decoder. + * @param assumedMinimumCodecOperatingRate A codec operating rate that all codecs instantiated by + * this renderer are assumed to meet implicitly (i.e. without the operating rate being set + * explicitly using {@link MediaFormat#KEY_OPERATING_RATE}). */ - public MediaCodecRenderer(int trackType, MediaCodecSelector mediaCodecSelector, - DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys) { + public MediaCodecRenderer( + int trackType, + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, + float assumedMinimumCodecOperatingRate) { super(trackType); - Assertions.checkState(Util.SDK_INT >= 16); this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector); this.drmSessionManager = drmSessionManager; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + this.enableDecoderFallback = enableDecoderFallback; + this.assumedMinimumCodecOperatingRate = assumedMinimumCodecOperatingRate; buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); - formatHolder = new FormatHolder(); + formatQueue = new TimedValueQueue<>(); decodeOnlyPresentationTimestamps = new ArrayList<>(); outputBufferInfo = new MediaCodec.BufferInfo(); codecReconfigurationState = RECONFIGURATION_STATE_NONE; - codecReinitializationState = REINITIALIZATION_STATE_NONE; + codecDrainState = DRAIN_STATE_NONE; + codecDrainAction = DRAIN_ACTION_NONE; + codecOperatingRate = CODEC_OPERATING_RATE_UNSET; + rendererOperatingRate = 1f; + renderTimeLimitMs = C.TIME_UNSET; + } + + /** + * Set a limit on the time a single {@link #render(long, long)} call can spend draining and + * filling the decoder. + * + *

This method is experimental, and will be renamed or removed in a future release. It should + * only be called before the renderer is used. + * + * @param renderTimeLimitMs The render time limit in milliseconds, or {@link C#TIME_UNSET} for no + * limit. + */ + public void experimental_setRenderTimeLimitMs(long renderTimeLimitMs) { + this.renderTimeLimitMs = renderTimeLimitMs; + } + + /** + * Skip calling {@link MediaCodec#stop()} when the underlying MediaCodec is going to be released. + * + *

By default, when the MediaCodecRenderer is releasing the underlying {@link MediaCodec}, it + * first calls {@link MediaCodec#stop()} and then calls {@link MediaCodec#release()}. If this + * feature is enabled, the MediaCodecRenderer will skip the call to {@link MediaCodec#stop()}. + * + *

This method is experimental, and will be renamed or removed in a future release. It should + * only be called before the renderer is used. + * + * @param enabled enable or disable the feature. + */ + public void experimental_setSkipMediaCodecStopOnRelease(boolean enabled) { + skipMediaCodecStopOnRelease = enabled; } @Override - public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { + @AdaptiveSupport + public final int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_NOT_SEAMLESS; } @Override + @Capabilities public final int supportsFormat(Format format) throws ExoPlaybackException { try { - return supportsFormat(mediaCodecSelector, format); + return supportsFormat(mediaCodecSelector, drmSessionManager, format); } catch (DecoderQueryException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, format); } } /** - * Returns the extent to which the renderer is capable of supporting a given format. + * Returns the {@link Capabilities} for the given {@link Format}. * * @param mediaCodecSelector The decoder selector. - * @param format The format. - * @return The extent to which the renderer is capable of supporting the given format. See - * {@link #supportsFormat(Format)} for more detail. + * @param drmSessionManager The renderer's {@link DrmSessionManager}. + * @param format The {@link Format}. + * @return The {@link Capabilities} for this {@link Format}. * @throws DecoderQueryException If there was an error querying decoders. */ - protected abstract int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) + @Capabilities + protected abstract int supportsFormat( + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + Format format) throws DecoderQueryException; /** - * Returns a {@link MediaCodecInfo} for a given format. + * Returns a list of decoders that can decode media in the specified format, in priority order. * * @param mediaCodecSelector The decoder selector. - * @param format The format for which a decoder is required. + * @param format The {@link Format} for which a decoder is required. * @param requiresSecureDecoder Whether a secure decoder is required. - * @return A {@link MediaCodecInfo} describing the decoder to instantiate, or null if no - * suitable decoder exists. + * @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty. * @throws DecoderQueryException Thrown if there was an error querying decoders. */ - protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector, - Format format, boolean requiresSecureDecoder) throws DecoderQueryException { - return mediaCodecSelector.getDecoderInfo(format.sampleMimeType, requiresSecureDecoder); - } + protected abstract List getDecoderInfos( + MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder) + throws DecoderQueryException; /** * Configures a newly created {@link MediaCodec}. * * @param codecInfo Information about the {@link MediaCodec} being configured. * @param codec The {@link MediaCodec} to configure. - * @param format The format for which the codec is being configured. + * @param format The {@link Format} for which the codec is being configured. * @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption. - * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. + * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if + * no codec operating rate should be set. */ - protected abstract void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, - MediaCrypto crypto) throws DecoderQueryException; + protected abstract void configureCodec( + MediaCodecInfo codecInfo, + MediaCodec codec, + Format format, + @Nullable MediaCrypto crypto, + float codecOperatingRate); - @SuppressWarnings("deprecation") protected final void maybeInitCodec() throws ExoPlaybackException { - if (!shouldInitCodec()) { + if (codec != null || inputFormat == null) { + // We have a codec already, or we don't have a format with which to instantiate one. return; } - drmSession = pendingDrmSession; - String mimeType = format.sampleMimeType; - MediaCrypto mediaCrypto = null; - boolean drmSessionRequiresSecureDecoder = false; - if (drmSession != null) { - @DrmSession.State int drmSessionState = drmSession.getState(); - if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); - } else if (drmSessionState == DrmSession.STATE_OPENED - || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) { - mediaCrypto = drmSession.getMediaCrypto().getWrappedMediaCrypto(); - drmSessionRequiresSecureDecoder = drmSession.requiresSecureDecoderComponent(mimeType); - } else { - // The drm session isn't open yet. - return; - } - } + setCodecDrmSession(sourceDrmSession); - MediaCodecInfo decoderInfo = null; - try { - decoderInfo = getDecoderInfo(mediaCodecSelector, format, drmSessionRequiresSecureDecoder); - if (decoderInfo == null && drmSessionRequiresSecureDecoder) { - // The drm session indicates that a secure decoder is required, but the device does not have - // one. Assuming that supportsFormat indicated support for the media being played, we know - // that it does not require a secure output path. Most CDM implementations allow playback to - // proceed with a non-secure decoder in this case, so we try our luck. - decoderInfo = getDecoderInfo(mediaCodecSelector, format, false); - if (decoderInfo != null) { - Log.w(TAG, "Drm session requires secure decoder for " + mimeType + ", but " - + "no secure decoder available. Trying to proceed with " + decoderInfo.name + "."); + String mimeType = inputFormat.sampleMimeType; + if (codecDrmSession != null) { + if (mediaCrypto == null) { + FrameworkMediaCrypto sessionMediaCrypto = codecDrmSession.getMediaCrypto(); + if (sessionMediaCrypto == null) { + DrmSessionException drmError = codecDrmSession.getError(); + if (drmError != null) { + // Continue for now. We may be able to avoid failure if the session recovers, or if a + // new input format causes the session to be replaced before it's used. + } else { + // The drm session isn't open yet. + return; + } + } else { + try { + mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId); + } catch (MediaCryptoException e) { + throw createRendererException(e, inputFormat); + } + mediaCryptoRequiresSecureDecoder = + !sessionMediaCrypto.forceAllowInsecureDecoderComponents + && mediaCrypto.requiresSecureDecoderComponent(mimeType); + } + } + if (FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC) { + @DrmSession.State int drmSessionState = codecDrmSession.getState(); + if (drmSessionState == DrmSession.STATE_ERROR) { + throw createRendererException(codecDrmSession.getError(), inputFormat); + } else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) { + // Wait for keys. + return; } } - } catch (DecoderQueryException e) { - throwDecoderInitError(new DecoderInitializationException(format, e, - drmSessionRequiresSecureDecoder, DecoderInitializationException.DECODER_QUERY_ERROR)); } - if (decoderInfo == null) { - throwDecoderInitError(new DecoderInitializationException(format, null, - drmSessionRequiresSecureDecoder, - DecoderInitializationException.NO_SUITABLE_DECODER_ERROR)); - } - - String codecName = decoderInfo.name; - codecIsAdaptive = decoderInfo.adaptive && !codecNeedsDisableAdaptationWorkaround(codecName); - codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format); - codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); - codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName); - codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName); - codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); - codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); - codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format); try { - long codecInitializingTimestamp = SystemClock.elapsedRealtime(); - TraceUtil.beginSection("createCodec:" + codecName); - codec = MediaCodec.createByCodecName(codecName); - TraceUtil.endSection(); - TraceUtil.beginSection("configureCodec"); - configureCodec(decoderInfo, codec, format, mediaCrypto); - TraceUtil.endSection(); - TraceUtil.beginSection("startCodec"); - codec.start(); - TraceUtil.endSection(); - long codecInitializedTimestamp = SystemClock.elapsedRealtime(); - onCodecInitialized(codecName, codecInitializedTimestamp, - codecInitializedTimestamp - codecInitializingTimestamp); - inputBuffers = codec.getInputBuffers(); - outputBuffers = codec.getOutputBuffers(); - } catch (Exception e) { - throwDecoderInitError(new DecoderInitializationException(format, e, - drmSessionRequiresSecureDecoder, codecName)); + maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder); + } catch (DecoderInitializationException e) { + throw createRendererException(e, inputFormat); } - codecHotswapDeadlineMs = getState() == STATE_STARTED - ? (SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS) : C.TIME_UNSET; - inputIndex = C.INDEX_UNSET; - outputIndex = C.INDEX_UNSET; - waitingForFirstSyncFrame = true; - decoderCounters.decoderInitCount++; } - private void throwDecoderInitError(DecoderInitializationException e) - throws ExoPlaybackException { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + protected boolean shouldInitCodec(MediaCodecInfo codecInfo) { + return true; } - protected boolean shouldInitCodec() { - return codec == null && format != null; + /** + * Returns whether the codec needs the renderer to propagate the end-of-stream signal directly, + * rather than by using an end-of-stream buffer queued to the codec. + */ + protected boolean getCodecNeedsEosPropagation() { + return false; + } + + /** + * Polls the pending output format queue for a given buffer timestamp. If a format is present, it + * is removed and returned. Otherwise returns {@code null}. Subclasses should only call this + * method if they are taking over responsibility for output format propagation (e.g., when using + * video tunneling). + */ + protected final @Nullable Format updateOutputFormatForTime(long presentationTimeUs) { + Format format = formatQueue.pollFloor(presentationTimeUs); + if (format != null) { + outputFormat = format; + } + return format; } protected final MediaCodec getCodec() { return codec; } + protected final @Nullable MediaCodecInfo getCodecInfo() { + return codecInfo; + } + @Override protected void onEnabled(boolean joining) throws ExoPlaybackException { + if (drmSessionManager != null && !drmResourcesAcquired) { + drmResourcesAcquired = true; + drmSessionManager.prepare(); + } decoderCounters = new DecoderCounters(); } @@ -397,76 +611,80 @@ public abstract class MediaCodecRenderer extends BaseRenderer { protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { inputStreamEnded = false; outputStreamEnded = false; - if (codec != null) { - flushCodec(); + pendingOutputEndOfStream = false; + flushOrReinitializeCodec(); + formatQueue.clear(); + } + + @Override + public final void setOperatingRate(float operatingRate) throws ExoPlaybackException { + rendererOperatingRate = operatingRate; + if (codec != null + && codecDrainAction != DRAIN_ACTION_REINITIALIZE + && getState() != STATE_DISABLED) { + updateCodecOperatingRate(); } } @Override protected void onDisabled() { - format = null; + inputFormat = null; + if (sourceDrmSession != null || codecDrmSession != null) { + // TODO: Do something better with this case. + onReset(); + } else { + flushOrReleaseCodec(); + } + } + + @Override + protected void onReset() { try { releaseCodec(); } finally { - try { - if (drmSession != null) { - drmSessionManager.releaseSession(drmSession); - } - } finally { - try { - if (pendingDrmSession != null && pendingDrmSession != drmSession) { - drmSessionManager.releaseSession(pendingDrmSession); - } - } finally { - drmSession = null; - pendingDrmSession = null; - } - } + setSourceDrmSession(null); + } + if (drmSessionManager != null && drmResourcesAcquired) { + drmResourcesAcquired = false; + drmSessionManager.release(); } } protected void releaseCodec() { - if (codec != null) { - codecHotswapDeadlineMs = C.TIME_UNSET; - inputIndex = C.INDEX_UNSET; - outputIndex = C.INDEX_UNSET; - waitingForKeys = false; - shouldSkipOutputBuffer = false; - decodeOnlyPresentationTimestamps.clear(); - inputBuffers = null; - outputBuffers = null; - codecReconfigured = false; - codecReceivedBuffers = false; - codecIsAdaptive = false; - codecNeedsDiscardToSpsWorkaround = false; - codecNeedsFlushWorkaround = false; - codecNeedsAdaptationWorkaround = false; - codecNeedsEosPropagationWorkaround = false; - codecNeedsEosFlushWorkaround = false; - codecNeedsMonoChannelCountWorkaround = false; - codecNeedsAdaptationWorkaroundBuffer = false; - shouldSkipAdaptationWorkaroundOutputBuffer = false; - codecReceivedEos = false; - codecReconfigurationState = RECONFIGURATION_STATE_NONE; - codecReinitializationState = REINITIALIZATION_STATE_NONE; - decoderCounters.decoderReleaseCount++; - buffer.data = null; - try { - codec.stop(); - } finally { + availableCodecInfos = null; + codecInfo = null; + codecFormat = null; + codecHasOutputMediaFormat = false; + resetInputBuffer(); + resetOutputBuffer(); + resetCodecBuffers(); + waitingForKeys = false; + codecHotswapDeadlineMs = C.TIME_UNSET; + decodeOnlyPresentationTimestamps.clear(); + largestQueuedPresentationTimeUs = C.TIME_UNSET; + lastBufferInStreamPresentationTimeUs = C.TIME_UNSET; + try { + if (codec != null) { + decoderCounters.decoderReleaseCount++; try { - codec.release(); - } finally { - codec = null; - if (drmSession != null && pendingDrmSession != drmSession) { - try { - drmSessionManager.releaseSession(drmSession); - } finally { - drmSession = null; - } + if (!skipMediaCodecStopOnRelease) { + codec.stop(); } + } finally { + codec.release(); } } + } finally { + codec = null; + try { + if (mediaCrypto != null) { + mediaCrypto.release(); + } + } finally { + mediaCrypto = null; + mediaCryptoRequiresSecureDecoder = false; + setCodecDrmSession(null); + } } } @@ -482,91 +700,352 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (outputStreamEnded) { - renderToEndOfStream(); - return; + if (pendingOutputEndOfStream) { + pendingOutputEndOfStream = false; + processEndOfStream(); } - if (format == null) { - // We don't have a format yet, so try and read one. - buffer.clear(); - int result = readSource(formatHolder, flagsOnlyBuffer, true); - if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); - } else if (result == C.RESULT_BUFFER_READ) { - // End of stream read having not read a format. - Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); - inputStreamEnded = true; - processEndOfStream(); + try { + if (outputStreamEnded) { + renderToEndOfStream(); return; - } else { + } + if (inputFormat == null && !readToFlagsOnlyBuffer(/* requireFormat= */ true)) { // We still don't have a format and can't make progress without one. return; } - } - // We have a format. - maybeInitCodec(); - if (codec != null) { - TraceUtil.beginSection("drainAndFeed"); - while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} - while (feedInputBuffer()) {} - TraceUtil.endSection(); - } else { - skipSource(positionUs); - // We need to read any format changes despite not having a codec so that drmSession can be - // updated, and so that we have the most recent format should the codec be initialized. We may - // also reach the end of the stream. Note that readSource will not read a sample into a - // flags-only buffer. - flagsOnlyBuffer.clear(); - int result = readSource(formatHolder, flagsOnlyBuffer, false); - if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); - } else if (result == C.RESULT_BUFFER_READ) { - Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); - inputStreamEnded = true; - processEndOfStream(); + // We have a format. + maybeInitCodec(); + if (codec != null) { + long drainStartTimeMs = SystemClock.elapsedRealtime(); + TraceUtil.beginSection("drainAndFeed"); + while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} + while (feedInputBuffer() && shouldContinueFeeding(drainStartTimeMs)) {} + TraceUtil.endSection(); + } else { + decoderCounters.skippedInputBufferCount += skipSource(positionUs); + // We need to read any format changes despite not having a codec so that drmSession can be + // updated, and so that we have the most recent format should the codec be initialized. We + // may also reach the end of the stream. Note that readSource will not read a sample into a + // flags-only buffer. + readToFlagsOnlyBuffer(/* requireFormat= */ false); } + decoderCounters.ensureUpdated(); + } catch (IllegalStateException e) { + if (isMediaCodecException(e)) { + throw createRendererException(e, inputFormat); + } + throw e; } - decoderCounters.ensureUpdated(); } - protected void flushCodec() throws ExoPlaybackException { + /** + * Flushes the codec. If flushing is not possible, the codec will be released and re-instantiated. + * This method is a no-op if the codec is {@code null}. + * + *

The implementation of this method calls {@link #flushOrReleaseCodec()}, and {@link + * #maybeInitCodec()} if the codec needs to be re-instantiated. + * + * @return Whether the codec was released and reinitialized, rather than being flushed. + * @throws ExoPlaybackException If an error occurs re-instantiating the codec. + */ + protected final boolean flushOrReinitializeCodec() throws ExoPlaybackException { + boolean released = flushOrReleaseCodec(); + if (released) { + maybeInitCodec(); + } + return released; + } + + /** + * Flushes the codec. If flushing is not possible, the codec will be released. This method is a + * no-op if the codec is {@code null}. + * + * @return Whether the codec was released. + */ + protected boolean flushOrReleaseCodec() { + if (codec == null) { + return false; + } + if (codecDrainAction == DRAIN_ACTION_REINITIALIZE + || codecNeedsFlushWorkaround + || (codecNeedsSosFlushWorkaround && !codecHasOutputMediaFormat) + || (codecNeedsEosFlushWorkaround && codecReceivedEos)) { + releaseCodec(); + return true; + } + + codec.flush(); + resetInputBuffer(); + resetOutputBuffer(); codecHotswapDeadlineMs = C.TIME_UNSET; - inputIndex = C.INDEX_UNSET; - outputIndex = C.INDEX_UNSET; - waitingForFirstSyncFrame = true; - waitingForKeys = false; - shouldSkipOutputBuffer = false; - decodeOnlyPresentationTimestamps.clear(); + codecReceivedEos = false; + codecReceivedBuffers = false; + waitingForFirstSyncSample = true; codecNeedsAdaptationWorkaroundBuffer = false; shouldSkipAdaptationWorkaroundOutputBuffer = false; - if (codecNeedsFlushWorkaround || (codecNeedsEosFlushWorkaround && codecReceivedEos)) { - releaseCodec(); - maybeInitCodec(); - } else if (codecReinitializationState != REINITIALIZATION_STATE_NONE) { - // We're already waiting to release and re-initialize the codec. Since we're now flushing, - // there's no need to wait any longer. - releaseCodec(); - maybeInitCodec(); + isDecodeOnlyOutputBuffer = false; + isLastOutputBuffer = false; + + waitingForKeys = false; + decodeOnlyPresentationTimestamps.clear(); + largestQueuedPresentationTimeUs = C.TIME_UNSET; + lastBufferInStreamPresentationTimeUs = C.TIME_UNSET; + codecDrainState = DRAIN_STATE_NONE; + codecDrainAction = DRAIN_ACTION_NONE; + // Reconfiguration data sent shortly before the flush may not have been processed by the + // decoder. If the codec has been reconfigured we always send reconfiguration data again to + // guarantee that it's processed. + codecReconfigurationState = + codecReconfigured ? RECONFIGURATION_STATE_WRITE_PENDING : RECONFIGURATION_STATE_NONE; + return false; + } + + protected DecoderException createDecoderException( + Throwable cause, @Nullable MediaCodecInfo codecInfo) { + return new DecoderException(cause, codecInfo); + } + + /** Reads into {@link #flagsOnlyBuffer} and returns whether a {@link Format} was read. */ + private boolean readToFlagsOnlyBuffer(boolean requireFormat) throws ExoPlaybackException { + FormatHolder formatHolder = getFormatHolder(); + flagsOnlyBuffer.clear(); + int result = readSource(formatHolder, flagsOnlyBuffer, requireFormat); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder); + return true; + } else if (result == C.RESULT_BUFFER_READ && flagsOnlyBuffer.isEndOfStream()) { + inputStreamEnded = true; + processEndOfStream(); + } + return false; + } + + private void maybeInitCodecWithFallback( + MediaCrypto crypto, boolean mediaCryptoRequiresSecureDecoder) + throws DecoderInitializationException { + if (availableCodecInfos == null) { + try { + List allAvailableCodecInfos = + getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder); + availableCodecInfos = new ArrayDeque<>(); + if (enableDecoderFallback) { + availableCodecInfos.addAll(allAvailableCodecInfos); + } else if (!allAvailableCodecInfos.isEmpty()) { + availableCodecInfos.add(allAvailableCodecInfos.get(0)); + } + preferredDecoderInitializationException = null; + } catch (DecoderQueryException e) { + throw new DecoderInitializationException( + inputFormat, + e, + mediaCryptoRequiresSecureDecoder, + DecoderInitializationException.DECODER_QUERY_ERROR); + } + } + + if (availableCodecInfos.isEmpty()) { + throw new DecoderInitializationException( + inputFormat, + /* cause= */ null, + mediaCryptoRequiresSecureDecoder, + DecoderInitializationException.NO_SUITABLE_DECODER_ERROR); + } + + while (codec == null) { + MediaCodecInfo codecInfo = availableCodecInfos.peekFirst(); + if (!shouldInitCodec(codecInfo)) { + return; + } + try { + initCodec(codecInfo, crypto); + } catch (Exception e) { + Log.w(TAG, "Failed to initialize decoder: " + codecInfo, e); + // This codec failed to initialize, so fall back to the next codec in the list (if any). We + // won't try to use this codec again unless there's a format change or the renderer is + // disabled and re-enabled. + availableCodecInfos.removeFirst(); + DecoderInitializationException exception = + new DecoderInitializationException( + inputFormat, e, mediaCryptoRequiresSecureDecoder, codecInfo); + if (preferredDecoderInitializationException == null) { + preferredDecoderInitializationException = exception; + } else { + preferredDecoderInitializationException = + preferredDecoderInitializationException.copyWithFallbackException(exception); + } + if (availableCodecInfos.isEmpty()) { + throw preferredDecoderInitializationException; + } + } + } + + availableCodecInfos = null; + } + + private List getAvailableCodecInfos(boolean mediaCryptoRequiresSecureDecoder) + throws DecoderQueryException { + List codecInfos = + getDecoderInfos(mediaCodecSelector, inputFormat, mediaCryptoRequiresSecureDecoder); + if (codecInfos.isEmpty() && mediaCryptoRequiresSecureDecoder) { + // The drm session indicates that a secure decoder is required, but the device does not + // have one. Assuming that supportsFormat indicated support for the media being played, we + // know that it does not require a secure output path. Most CDM implementations allow + // playback to proceed with a non-secure decoder in this case, so we try our luck. + codecInfos = + getDecoderInfos(mediaCodecSelector, inputFormat, /* requiresSecureDecoder= */ false); + if (!codecInfos.isEmpty()) { + Log.w( + TAG, + "Drm session requires secure decoder for " + + inputFormat.sampleMimeType + + ", but no secure decoder available. Trying to proceed with " + + codecInfos + + "."); + } + } + return codecInfos; + } + + private void initCodec(MediaCodecInfo codecInfo, MediaCrypto crypto) throws Exception { + long codecInitializingTimestamp; + long codecInitializedTimestamp; + MediaCodec codec = null; + String codecName = codecInfo.name; + + float codecOperatingRate = + Util.SDK_INT < 23 + ? CODEC_OPERATING_RATE_UNSET + : getCodecOperatingRateV23(rendererOperatingRate, inputFormat, getStreamFormats()); + if (codecOperatingRate <= assumedMinimumCodecOperatingRate) { + codecOperatingRate = CODEC_OPERATING_RATE_UNSET; + } + try { + codecInitializingTimestamp = SystemClock.elapsedRealtime(); + TraceUtil.beginSection("createCodec:" + codecName); + codec = MediaCodec.createByCodecName(codecName); + TraceUtil.endSection(); + TraceUtil.beginSection("configureCodec"); + configureCodec(codecInfo, codec, inputFormat, crypto, codecOperatingRate); + TraceUtil.endSection(); + TraceUtil.beginSection("startCodec"); + codec.start(); + TraceUtil.endSection(); + codecInitializedTimestamp = SystemClock.elapsedRealtime(); + getCodecBuffers(codec); + } catch (Exception e) { + if (codec != null) { + resetCodecBuffers(); + codec.release(); + } + throw e; + } + + this.codec = codec; + this.codecInfo = codecInfo; + this.codecOperatingRate = codecOperatingRate; + codecFormat = inputFormat; + codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName); + codecNeedsReconfigureWorkaround = codecNeedsReconfigureWorkaround(codecName); + codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, codecFormat); + codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); + codecNeedsSosFlushWorkaround = codecNeedsSosFlushWorkaround(codecName); + codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); + codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); + codecNeedsMonoChannelCountWorkaround = + codecNeedsMonoChannelCountWorkaround(codecName, codecFormat); + codecNeedsEosPropagation = + codecNeedsEosPropagationWorkaround(codecInfo) || getCodecNeedsEosPropagation(); + + resetInputBuffer(); + resetOutputBuffer(); + codecHotswapDeadlineMs = + getState() == STATE_STARTED + ? (SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS) + : C.TIME_UNSET; + codecReconfigured = false; + codecReconfigurationState = RECONFIGURATION_STATE_NONE; + codecReceivedEos = false; + codecReceivedBuffers = false; + largestQueuedPresentationTimeUs = C.TIME_UNSET; + lastBufferInStreamPresentationTimeUs = C.TIME_UNSET; + codecDrainState = DRAIN_STATE_NONE; + codecDrainAction = DRAIN_ACTION_NONE; + codecNeedsAdaptationWorkaroundBuffer = false; + shouldSkipAdaptationWorkaroundOutputBuffer = false; + isDecodeOnlyOutputBuffer = false; + isLastOutputBuffer = false; + waitingForFirstSyncSample = true; + + decoderCounters.decoderInitCount++; + long elapsed = codecInitializedTimestamp - codecInitializingTimestamp; + onCodecInitialized(codecName, codecInitializedTimestamp, elapsed); + } + + private boolean shouldContinueFeeding(long drainStartTimeMs) { + return renderTimeLimitMs == C.TIME_UNSET + || SystemClock.elapsedRealtime() - drainStartTimeMs < renderTimeLimitMs; + } + + private void getCodecBuffers(MediaCodec codec) { + if (Util.SDK_INT < 21) { + inputBuffers = codec.getInputBuffers(); + outputBuffers = codec.getOutputBuffers(); + } + } + + private void resetCodecBuffers() { + if (Util.SDK_INT < 21) { + inputBuffers = null; + outputBuffers = null; + } + } + + private ByteBuffer getInputBuffer(int inputIndex) { + if (Util.SDK_INT >= 21) { + return codec.getInputBuffer(inputIndex); } else { - // We can flush and re-use the existing decoder. - codec.flush(); - codecReceivedBuffers = false; + return inputBuffers[inputIndex]; } - if (codecReconfigured && format != null) { - // Any reconfiguration data that we send shortly before the flush may be discarded. We - // avoid this issue by sending reconfiguration data following every flush. - codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; + } + + private ByteBuffer getOutputBuffer(int outputIndex) { + if (Util.SDK_INT >= 21) { + return codec.getOutputBuffer(outputIndex); + } else { + return outputBuffers[outputIndex]; } } + private boolean hasOutputBuffer() { + return outputIndex >= 0; + } + + private void resetInputBuffer() { + inputIndex = C.INDEX_UNSET; + buffer.data = null; + } + + private void resetOutputBuffer() { + outputIndex = C.INDEX_UNSET; + outputBuffer = null; + } + + private void setSourceDrmSession(@Nullable DrmSession session) { + DrmSession.replaceSession(sourceDrmSession, session); + sourceDrmSession = session; + } + + private void setCodecDrmSession(@Nullable DrmSession session) { + DrmSession.replaceSession(codecDrmSession, session); + codecDrmSession = session; + } + /** * @return Whether it may be possible to feed more input data. * @throws ExoPlaybackException If an error occurs feeding the input buffer. */ private boolean feedInputBuffer() throws ExoPlaybackException { - if (codec == null || codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM - || inputStreamEnded) { - // We need to reinitialize the codec or the input stream has ended. + if (codec == null || codecDrainState == DRAIN_STATE_WAIT_END_OF_STREAM || inputStreamEnded) { return false; } @@ -575,21 +1054,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (inputIndex < 0) { return false; } - buffer.data = inputBuffers[inputIndex]; + buffer.data = getInputBuffer(inputIndex); buffer.clear(); } - if (codecReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) { + if (codecDrainState == DRAIN_STATE_SIGNAL_END_OF_STREAM) { // We need to re-initialize the codec. Send an end of stream signal to the existing codec so // that it outputs any remaining buffers before we release it. - if (codecNeedsEosPropagationWorkaround) { + if (codecNeedsEosPropagation) { // Do nothing. } else { codecReceivedEos = true; codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); - inputIndex = C.INDEX_UNSET; + resetInputBuffer(); } - codecReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM; + codecDrainState = DRAIN_STATE_WAIT_END_OF_STREAM; return false; } @@ -597,12 +1076,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecNeedsAdaptationWorkaroundBuffer = false; buffer.data.put(ADAPTATION_WORKAROUND_BUFFER); codec.queueInputBuffer(inputIndex, 0, ADAPTATION_WORKAROUND_BUFFER.length, 0, 0); - inputIndex = C.INDEX_UNSET; + resetInputBuffer(); codecReceivedBuffers = true; return true; } int result; + FormatHolder formatHolder = getFormatHolder(); int adaptiveReconfigurationBytes = 0; if (waitingForKeys) { // We've already read an encrypted sample into buffer, and are waiting for keys. @@ -611,8 +1091,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { // For adaptive reconfiguration OMX decoders expect all reconfiguration data to be supplied // at the start of the buffer that also contains the first frame in the new format. if (codecReconfigurationState == RECONFIGURATION_STATE_WRITE_PENDING) { - for (int i = 0; i < format.initializationData.size(); i++) { - byte[] data = format.initializationData.get(i); + for (int i = 0; i < codecFormat.initializationData.size(); i++) { + byte[] data = codecFormat.initializationData.get(i); buffer.data.put(data); } codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING; @@ -621,6 +1101,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { result = readSource(formatHolder, buffer, false); } + if (hasReadStreamToEnd()) { + // Notify output queue of the last buffer's timestamp. + lastBufferInStreamPresentationTimeUs = largestQueuedPresentationTimeUs; + } + if (result == C.RESULT_NOTHING_READ) { return false; } @@ -631,7 +1116,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { buffer.clear(); codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; } - onInputFormatChanged(formatHolder.format); + onInputFormatChanged(formatHolder); return true; } @@ -650,19 +1135,19 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } try { - if (codecNeedsEosPropagationWorkaround) { + if (codecNeedsEosPropagation) { // Do nothing. } else { codecReceivedEos = true; codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); - inputIndex = C.INDEX_UNSET; + resetInputBuffer(); } } catch (CryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return false; } - if (waitingForFirstSyncFrame && !buffer.isKeyFrame()) { + if (waitingForFirstSyncSample && !buffer.isKeyFrame()) { buffer.clear(); if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { // The buffer we just cleared contained reconfiguration data. We need to re-write this @@ -671,7 +1156,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } return true; } - waitingForFirstSyncFrame = false; + waitingForFirstSyncSample = false; boolean bufferEncrypted = buffer.isEncrypted(); waitingForKeys = shouldWaitForKeys(bufferEncrypted); if (waitingForKeys) { @@ -689,8 +1174,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (buffer.isDecodeOnly()) { decodeOnlyPresentationTimestamps.add(presentationTimeUs); } + if (waitingForFirstSampleInFormat) { + formatQueue.add(presentationTimeUs, inputFormat); + waitingForFirstSampleInFormat = false; + } + largestQueuedPresentationTimeUs = + Math.max(largestQueuedPresentationTimeUs, presentationTimeUs); buffer.flip(); + if (buffer.hasSupplementalData()) { + handleInputBufferSupplementalData(buffer); + } onQueueInputBuffer(buffer); if (bufferEncrypted) { @@ -700,42 +1194,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } else { codec.queueInputBuffer(inputIndex, 0, buffer.data.limit(), presentationTimeUs, 0); } - inputIndex = C.INDEX_UNSET; + resetInputBuffer(); codecReceivedBuffers = true; codecReconfigurationState = RECONFIGURATION_STATE_NONE; decoderCounters.inputBufferCount++; } catch (CryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return true; } - private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(DecoderInputBuffer buffer, - int adaptiveReconfigurationBytes) { - MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfoV16(); - if (adaptiveReconfigurationBytes == 0) { - return cryptoInfo; - } - // There must be at least one sub-sample, although numBytesOfClearData is permitted to be - // null if it contains no clear data. Instantiate it if needed, and add the reconfiguration - // bytes to the clear byte count of the first sub-sample. - if (cryptoInfo.numBytesOfClearData == null) { - cryptoInfo.numBytesOfClearData = new int[1]; - } - cryptoInfo.numBytesOfClearData[0] += adaptiveReconfigurationBytes; - return cryptoInfo; - } - private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (drmSession == null) { + if (codecDrmSession == null + || (!bufferEncrypted + && (playClearSamplesWithoutKeys || codecDrmSession.playClearSamplesWithoutKeys()))) { return false; } - @DrmSession.State int drmSessionState = drmSession.getState(); + @DrmSession.State int drmSessionState = codecDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + throw createRendererException(codecDrmSession.getError(), inputFormat); } - return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS - && (bufferEncrypted || !playClearSamplesWithoutKeys); + return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } /** @@ -754,68 +1233,118 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Called when a new format is read from the upstream {@link MediaPeriod}. + * Called when a new {@link Format} is read from the upstream {@link MediaPeriod}. * - * @param newFormat The new format. - * @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}. + * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}. + * @throws ExoPlaybackException If an error occurs re-initializing the {@link MediaCodec}. */ - protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { - Format oldFormat = format; - format = newFormat; + @SuppressWarnings("unchecked") + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + waitingForFirstSampleInFormat = true; + Format newFormat = Assertions.checkNotNull(formatHolder.format); + if (formatHolder.includesDrmSession) { + setSourceDrmSession((DrmSession) formatHolder.drmSession); + } else { + sourceDrmSession = + getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession); + } + inputFormat = newFormat; - boolean drmInitDataChanged = !Util.areEqual(format.drmInitData, oldFormat == null ? null - : oldFormat.drmInitData); - if (drmInitDataChanged) { - if (format.drmInitData != null) { - if (drmSessionManager == null) { - throw ExoPlaybackException.createForRenderer( - new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); - } - pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData); - if (pendingDrmSession == drmSession) { - drmSessionManager.releaseSession(pendingDrmSession); - } - } else { - pendingDrmSession = null; - } + if (codec == null) { + maybeInitCodec(); + return; } - if (pendingDrmSession == drmSession && codec != null - && canReconfigureCodec(codec, codecIsAdaptive, oldFormat, format)) { - codecReconfigured = true; - codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; - codecNeedsAdaptationWorkaroundBuffer = codecNeedsAdaptationWorkaround - && format.width == oldFormat.width && format.height == oldFormat.height; - } else { - if (codecReceivedBuffers) { - // Signal end of stream and wait for any final output buffers before re-initialization. - codecReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; - } else { - // There aren't any final output buffers, so perform re-initialization immediately. - releaseCodec(); - maybeInitCodec(); - } + // We have an existing codec that we may need to reconfigure or re-initialize. If the existing + // codec instance is being kept then its operating rate may need to be updated. + + if ((sourceDrmSession == null && codecDrmSession != null) + || (sourceDrmSession != null && codecDrmSession == null) + || (sourceDrmSession != codecDrmSession + && !codecInfo.secure + && maybeRequiresSecureDecoder(sourceDrmSession, newFormat)) + || (Util.SDK_INT < 23 && sourceDrmSession != codecDrmSession)) { + // We might need to switch between the clear and protected output paths, or we're using DRM + // prior to API level 23 where the codec needs to be re-initialized to switch to the new DRM + // session. + drainAndReinitializeCodec(); + return; + } + + switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) { + case KEEP_CODEC_RESULT_NO: + drainAndReinitializeCodec(); + break; + case KEEP_CODEC_RESULT_YES_WITH_FLUSH: + codecFormat = newFormat; + updateCodecOperatingRate(); + if (sourceDrmSession != codecDrmSession) { + drainAndUpdateCodecDrmSession(); + } else { + drainAndFlushCodec(); + } + break; + case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION: + if (codecNeedsReconfigureWorkaround) { + drainAndReinitializeCodec(); + } else { + codecReconfigured = true; + codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; + codecNeedsAdaptationWorkaroundBuffer = + codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS + || (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION + && newFormat.width == codecFormat.width + && newFormat.height == codecFormat.height); + codecFormat = newFormat; + updateCodecOperatingRate(); + if (sourceDrmSession != codecDrmSession) { + drainAndUpdateCodecDrmSession(); + } + } + break; + case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION: + codecFormat = newFormat; + updateCodecOperatingRate(); + if (sourceDrmSession != codecDrmSession) { + drainAndUpdateCodecDrmSession(); + } + break; + default: + throw new IllegalStateException(); // Never happens. } } /** - * Called when the output format of the {@link MediaCodec} changes. - *

- * The default implementation is a no-op. + * Called when the output {@link MediaFormat} of the {@link MediaCodec} changes. + * + *

The default implementation is a no-op. * * @param codec The {@link MediaCodec} instance. - * @param outputFormat The new output format. - * @throws ExoPlaybackException Thrown if an error occurs handling the new output format. + * @param outputMediaFormat The new output {@link MediaFormat}. + * @throws ExoPlaybackException Thrown if an error occurs handling the new output media format. */ - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputMediaFormat) + throws ExoPlaybackException { + // Do nothing. + } + + /** + * Handles supplemental data associated with an input buffer. + * + *

The default implementation is a no-op. + * + * @param buffer The input buffer that is about to be queued. + * @throws ExoPlaybackException Thrown if an error occurs handling supplemental data. + */ + protected void handleInputBufferSupplementalData(DecoderInputBuffer buffer) throws ExoPlaybackException { // Do nothing. } /** * Called immediately before an input buffer is queued into the codec. - *

- * The default implementation is a no-op. + * + *

The default implementation is a no-op. * * @param buffer The buffer to be queued. */ @@ -835,23 +1364,20 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Determines whether the existing {@link MediaCodec} should be reconfigured for a new format by - * sending codec specific initialization data at the start of the next input buffer. If true is - * returned then the {@link MediaCodec} instance will be reconfigured in this way. If false is - * returned then the instance will be released, and a new instance will be created for the new - * format. - *

- * The default implementation returns false. + * Determines whether the existing {@link MediaCodec} can be kept for a new {@link Format}, and if + * it can whether it requires reconfiguration. + * + *

The default implementation returns {@link #KEEP_CODEC_RESULT_NO}. * * @param codec The existing {@link MediaCodec} instance. - * @param codecIsAdaptive Whether the codec is adaptive. - * @param oldFormat The format for which the existing instance is configured. - * @param newFormat The new format. - * @return Whether the existing instance can be reconfigured. + * @param codecInfo A {@link MediaCodecInfo} describing the decoder. + * @param oldFormat The {@link Format} for which the existing instance is configured. + * @param newFormat The new {@link Format}. + * @return Whether the instance can be kept, and if it can whether it requires reconfiguration. */ - protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive, Format oldFormat, - Format newFormat) { - return false; + protected @KeepCodecResult int canKeepCodec( + MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + return KEEP_CODEC_RESULT_NO; } @Override @@ -861,9 +1387,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override public boolean isReady() { - return format != null && !waitingForKeys && (isSourceReady() || outputIndex >= 0 - || (codecHotswapDeadlineMs != C.TIME_UNSET - && SystemClock.elapsedRealtime() < codecHotswapDeadlineMs)); + return inputFormat != null + && !waitingForKeys + && (isSourceReady() + || hasOutputBuffer() + || (codecHotswapDeadlineMs != C.TIME_UNSET + && SystemClock.elapsedRealtime() < codecHotswapDeadlineMs)); } /** @@ -875,18 +1404,109 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return 0; } + /** + * Returns the {@link MediaFormat#KEY_OPERATING_RATE} value for a given renderer operating rate, + * current {@link Format} and set of possible stream formats. + * + *

The default implementation returns {@link #CODEC_OPERATING_RATE_UNSET}. + * + * @param operatingRate The renderer operating rate. + * @param format The {@link Format} for which the codec is being configured. + * @param streamFormats The possible stream formats. + * @return The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if no codec operating + * rate should be set. + */ + protected float getCodecOperatingRateV23( + float operatingRate, Format format, Format[] streamFormats) { + return CODEC_OPERATING_RATE_UNSET; + } + + /** + * Updates the codec operating rate. + * + * @throws ExoPlaybackException If an error occurs releasing or initializing a codec. + */ + private void updateCodecOperatingRate() throws ExoPlaybackException { + if (Util.SDK_INT < 23) { + return; + } + + float newCodecOperatingRate = + getCodecOperatingRateV23(rendererOperatingRate, codecFormat, getStreamFormats()); + if (codecOperatingRate == newCodecOperatingRate) { + // No change. + } else if (newCodecOperatingRate == CODEC_OPERATING_RATE_UNSET) { + // The only way to clear the operating rate is to instantiate a new codec instance. See + // [Internal ref: b/71987865]. + drainAndReinitializeCodec(); + } else if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET + || newCodecOperatingRate > assumedMinimumCodecOperatingRate) { + // We need to set the operating rate, either because we've set it previously or because it's + // above the assumed minimum rate. + Bundle codecParameters = new Bundle(); + codecParameters.putFloat(MediaFormat.KEY_OPERATING_RATE, newCodecOperatingRate); + codec.setParameters(codecParameters); + codecOperatingRate = newCodecOperatingRate; + } + } + + /** Starts draining the codec for flush. */ + private void drainAndFlushCodec() { + if (codecReceivedBuffers) { + codecDrainState = DRAIN_STATE_SIGNAL_END_OF_STREAM; + codecDrainAction = DRAIN_ACTION_FLUSH; + } + } + + /** + * Starts draining the codec to update its DRM session. The update may occur immediately if no + * buffers have been queued to the codec. + * + * @throws ExoPlaybackException If an error occurs updating the codec's DRM session. + */ + private void drainAndUpdateCodecDrmSession() throws ExoPlaybackException { + if (Util.SDK_INT < 23) { + // The codec needs to be re-initialized to switch to the source DRM session. + drainAndReinitializeCodec(); + return; + } + if (codecReceivedBuffers) { + codecDrainState = DRAIN_STATE_SIGNAL_END_OF_STREAM; + codecDrainAction = DRAIN_ACTION_UPDATE_DRM_SESSION; + } else { + // Nothing has been queued to the decoder, so we can do the update immediately. + updateDrmSessionOrReinitializeCodecV23(); + } + } + + /** + * Starts draining the codec for re-initialization. Re-initialization may occur immediately if no + * buffers have been queued to the codec. + * + * @throws ExoPlaybackException If an error occurs re-initializing a codec. + */ + private void drainAndReinitializeCodec() throws ExoPlaybackException { + if (codecReceivedBuffers) { + codecDrainState = DRAIN_STATE_SIGNAL_END_OF_STREAM; + codecDrainAction = DRAIN_ACTION_REINITIALIZE; + } else { + // Nothing has been queued to the decoder, so we can re-initialize immediately. + reinitializeCodec(); + } + } + /** * @return Whether it may be possible to drain more output data. * @throws ExoPlaybackException If an error occurs draining the output buffer. */ - @SuppressWarnings("deprecation") private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (outputIndex < 0) { + if (!hasOutputBuffer()) { + int outputIndex; if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) { try { - outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, - getDequeueOutputBufferTimeoutUs()); + outputIndex = + codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs()); } catch (IllegalStateException e) { processEndOfStream(); if (outputStreamEnded) { @@ -896,53 +1516,67 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } } else { - outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, - getDequeueOutputBufferTimeoutUs()); + outputIndex = + codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs()); } - if (outputIndex >= 0) { - // We've dequeued a buffer. - if (shouldSkipAdaptationWorkaroundOutputBuffer) { - shouldSkipAdaptationWorkaroundOutputBuffer = false; - codec.releaseOutputBuffer(outputIndex, false); - outputIndex = C.INDEX_UNSET; + + if (outputIndex < 0) { + if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED /* (-2) */) { + processOutputFormat(); + return true; + } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED /* (-3) */) { + processOutputBuffersChanged(); return true; } - if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { - // The dequeued buffer indicates the end of the stream. Process it immediately. - processEndOfStream(); - outputIndex = C.INDEX_UNSET; - return false; - } else { - // The dequeued buffer is a media buffer. Do some initial setup. The buffer will be - // processed by calling processOutputBuffer (possibly multiple times) below. - ByteBuffer outputBuffer = outputBuffers[outputIndex]; - if (outputBuffer != null) { - outputBuffer.position(outputBufferInfo.offset); - outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size); - } - shouldSkipOutputBuffer = shouldSkipOutputBuffer(outputBufferInfo.presentationTimeUs); - } - } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED /* (-2) */) { - processOutputFormat(); - return true; - } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED /* (-3) */) { - processOutputBuffersChanged(); - return true; - } else /* MediaCodec.INFO_TRY_AGAIN_LATER (-1) or unknown negative return value */ { - if (codecNeedsEosPropagationWorkaround && (inputStreamEnded - || codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM)) { + /* MediaCodec.INFO_TRY_AGAIN_LATER (-1) or unknown negative return value */ + if (codecNeedsEosPropagation + && (inputStreamEnded || codecDrainState == DRAIN_STATE_WAIT_END_OF_STREAM)) { processEndOfStream(); } return false; } + + // We've dequeued a buffer. + if (shouldSkipAdaptationWorkaroundOutputBuffer) { + shouldSkipAdaptationWorkaroundOutputBuffer = false; + codec.releaseOutputBuffer(outputIndex, false); + return true; + } else if (outputBufferInfo.size == 0 + && (outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + // The dequeued buffer indicates the end of the stream. Process it immediately. + processEndOfStream(); + return false; + } + + this.outputIndex = outputIndex; + outputBuffer = getOutputBuffer(outputIndex); + // The dequeued buffer is a media buffer. Do some initial setup. + // It will be processed by calling processOutputBuffer (possibly multiple times). + if (outputBuffer != null) { + outputBuffer.position(outputBufferInfo.offset); + outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size); + } + isDecodeOnlyOutputBuffer = isDecodeOnlyBuffer(outputBufferInfo.presentationTimeUs); + isLastOutputBuffer = + lastBufferInStreamPresentationTimeUs == outputBufferInfo.presentationTimeUs; + updateOutputFormatForTime(outputBufferInfo.presentationTimeUs); } boolean processedOutputBuffer; if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) { try { - processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec, - outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags, - outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer); + processedOutputBuffer = + processOutputBuffer( + positionUs, + elapsedRealtimeUs, + codec, + outputBuffer, + outputIndex, + outputBufferInfo.flags, + outputBufferInfo.presentationTimeUs, + isDecodeOnlyOutputBuffer, + isLastOutputBuffer, + outputFormat); } catch (IllegalStateException e) { processEndOfStream(); if (outputStreamEnded) { @@ -952,78 +1586,102 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } } else { - processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec, - outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags, - outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer); + processedOutputBuffer = + processOutputBuffer( + positionUs, + elapsedRealtimeUs, + codec, + outputBuffer, + outputIndex, + outputBufferInfo.flags, + outputBufferInfo.presentationTimeUs, + isDecodeOnlyOutputBuffer, + isLastOutputBuffer, + outputFormat); } if (processedOutputBuffer) { onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs); - outputIndex = C.INDEX_UNSET; - return true; + boolean isEndOfStream = (outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; + resetOutputBuffer(); + if (!isEndOfStream) { + return true; + } + processEndOfStream(); } return false; } - /** - * Processes a new output format. - */ + /** Processes a new output {@link MediaFormat}. */ private void processOutputFormat() throws ExoPlaybackException { - MediaFormat format = codec.getOutputFormat(); - if (codecNeedsAdaptationWorkaround - && format.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT - && format.getInteger(MediaFormat.KEY_HEIGHT) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT) { + codecHasOutputMediaFormat = true; + MediaFormat mediaFormat = codec.getOutputFormat(); + if (codecAdaptationWorkaroundMode != ADAPTATION_WORKAROUND_MODE_NEVER + && mediaFormat.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT + && mediaFormat.getInteger(MediaFormat.KEY_HEIGHT) + == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT) { // We assume this format changed event was caused by the adaptation workaround. shouldSkipAdaptationWorkaroundOutputBuffer = true; return; } if (codecNeedsMonoChannelCountWorkaround) { - format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); + mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); } - onOutputFormatChanged(codec, format); + onOutputFormatChanged(codec, mediaFormat); } /** * Processes a change in the output buffers. */ - @SuppressWarnings("deprecation") private void processOutputBuffersChanged() { - outputBuffers = codec.getOutputBuffers(); + if (Util.SDK_INT < 21) { + outputBuffers = codec.getOutputBuffers(); + } } /** * Processes an output media buffer. - *

- * When a new {@link ByteBuffer} is passed to this method its position and limit delineate the + * + *

When a new {@link ByteBuffer} is passed to this method its position and limit delineate the * data to be processed. The return value indicates whether the buffer was processed in full. If * true is returned then the next call to this method will receive a new buffer to be processed. * If false is returned then the same buffer will be passed to the next call. An implementation of * this method is free to modify the buffer and can assume that the buffer will not be externally * modified between successive calls. Hence an implementation can, for example, modify the * buffer's position to keep track of how much of the data it has processed. - *

- * Note that the first call to this method following a call to - * {@link #onPositionReset(long, boolean)} will always receive a new {@link ByteBuffer} to be - * processed. * - * @param positionUs The current media time in microseconds, measured at the start of the - * current iteration of the rendering loop. - * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, - * measured at the start of the current iteration of the rendering loop. + *

Note that the first call to this method following a call to {@link #onPositionReset(long, + * boolean)} will always receive a new {@link ByteBuffer} to be processed. + * + * @param positionUs The current media time in microseconds, measured at the start of the current + * iteration of the rendering loop. + * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, measured at the + * start of the current iteration of the rendering loop. * @param codec The {@link MediaCodec} instance. * @param buffer The output buffer to process. * @param bufferIndex The index of the output buffer. * @param bufferFlags The flags attached to the output buffer. * @param bufferPresentationTimeUs The presentation time of the output buffer in microseconds. - * @param shouldSkip Whether the buffer should be skipped (i.e. not rendered). - * + * @param isDecodeOnlyBuffer Whether the buffer was marked with {@link C#BUFFER_FLAG_DECODE_ONLY} + * by the source. + * @param isLastBuffer Whether the buffer is the last sample of the current stream. + * @param format The {@link Format} associated with the buffer. * @return Whether the output buffer was fully processed (e.g. rendered or skipped). * @throws ExoPlaybackException If an error occurs processing the output buffer. */ - protected abstract boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, - MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, - long bufferPresentationTimeUs, boolean shouldSkip) throws ExoPlaybackException; + protected abstract boolean processOutputBuffer( + long positionUs, + long elapsedRealtimeUs, + MediaCodec codec, + ByteBuffer buffer, + int bufferIndex, + int bufferFlags, + long bufferPresentationTimeUs, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, + Format format) + throws ExoPlaybackException; /** * Incrementally renders any remaining output. @@ -1042,17 +1700,38 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @throws ExoPlaybackException If an error occurs processing the signal. */ private void processEndOfStream() throws ExoPlaybackException { - if (codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) { - // We're waiting to re-initialize the codec, and have now processed all final buffers. - releaseCodec(); - maybeInitCodec(); - } else { - outputStreamEnded = true; - renderToEndOfStream(); + switch (codecDrainAction) { + case DRAIN_ACTION_REINITIALIZE: + reinitializeCodec(); + break; + case DRAIN_ACTION_UPDATE_DRM_SESSION: + updateDrmSessionOrReinitializeCodecV23(); + break; + case DRAIN_ACTION_FLUSH: + flushOrReinitializeCodec(); + break; + case DRAIN_ACTION_NONE: + default: + outputStreamEnded = true; + renderToEndOfStream(); + break; } } - private boolean shouldSkipOutputBuffer(long presentationTimeUs) { + /** + * Notifies the renderer that output end of stream is pending and should be handled on the next + * render. + */ + protected final void setPendingOutputEndOfStream() { + pendingOutputEndOfStream = true; + } + + private void reinitializeCodec() throws ExoPlaybackException { + releaseCodec(); + maybeInitCodec(); + } + + private boolean isDecodeOnlyBuffer(long presentationTimeUs) { // We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would // box presentationTimeUs, creating a Long object that would need to be garbage collected. int size = decodeOnlyPresentationTimestamps.size(); @@ -1065,6 +1744,105 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } + @TargetApi(23) + private void updateDrmSessionOrReinitializeCodecV23() throws ExoPlaybackException { + @Nullable FrameworkMediaCrypto sessionMediaCrypto = sourceDrmSession.getMediaCrypto(); + if (sessionMediaCrypto == null) { + // We'd only expect this to happen if the CDM from which the pending session is obtained needs + // provisioning. This is unlikely to happen (it probably requires a switch from one DRM scheme + // to another, where the new CDM hasn't been used before and needs provisioning). It would be + // possible to handle this case more efficiently (i.e. with a new renderer state that waits + // for provisioning to finish and then calls mediaCrypto.setMediaDrmSession), but the extra + // complexity is not warranted given how unlikely the case is to occur. + reinitializeCodec(); + return; + } + if (C.PLAYREADY_UUID.equals(sessionMediaCrypto.uuid)) { + // The PlayReady CDM does not implement setMediaDrmSession. + // TODO: Add API check once [Internal ref: b/128835874] is fixed. + reinitializeCodec(); + return; + } + + if (flushOrReinitializeCodec()) { + // The codec was reinitialized. The new codec will be using the new DRM session, so there's + // nothing more to do. + return; + } + + try { + mediaCrypto.setMediaDrmSession(sessionMediaCrypto.sessionId); + } catch (MediaCryptoException e) { + throw createRendererException(e, inputFormat); + } + setCodecDrmSession(sourceDrmSession); + codecDrainState = DRAIN_STATE_NONE; + codecDrainAction = DRAIN_ACTION_NONE; + } + + /** + * Returns whether a {@link DrmSession} may require a secure decoder for a given {@link Format}. + * + * @param drmSession The {@link DrmSession}. + * @param format The {@link Format}. + * @return Whether a secure decoder may be required. + */ + private static boolean maybeRequiresSecureDecoder( + DrmSession drmSession, Format format) { + @Nullable FrameworkMediaCrypto sessionMediaCrypto = drmSession.getMediaCrypto(); + if (sessionMediaCrypto == null) { + // We'd only expect this to happen if the CDM from which the pending session is obtained needs + // provisioning. This is unlikely to happen (it probably requires a switch from one DRM scheme + // to another, where the new CDM hasn't been used before and needs provisioning). Assume that + // a secure decoder may be required. + return true; + } + if (sessionMediaCrypto.forceAllowInsecureDecoderComponents) { + return false; + } + MediaCrypto mediaCrypto; + try { + mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId); + } catch (MediaCryptoException e) { + // This shouldn't happen, but if it does then assume that a secure decoder may be required. + return true; + } + try { + return mediaCrypto.requiresSecureDecoderComponent(format.sampleMimeType); + } finally { + mediaCrypto.release(); + } + } + + private static MediaCodec.CryptoInfo getFrameworkCryptoInfo( + DecoderInputBuffer buffer, int adaptiveReconfigurationBytes) { + MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfo(); + if (adaptiveReconfigurationBytes == 0) { + return cryptoInfo; + } + // There must be at least one sub-sample, although numBytesOfClearData is permitted to be + // null if it contains no clear data. Instantiate it if needed, and add the reconfiguration + // bytes to the clear byte count of the first sub-sample. + if (cryptoInfo.numBytesOfClearData == null) { + cryptoInfo.numBytesOfClearData = new int[1]; + } + cryptoInfo.numBytesOfClearData[0] += adaptiveReconfigurationBytes; + return cryptoInfo; + } + + private static boolean isMediaCodecException(IllegalStateException error) { + if (Util.SDK_INT >= 21 && isMediaCodecExceptionV21(error)) { + return true; + } + StackTraceElement[] stackTrace = error.getStackTrace(); + return stackTrace.length > 0 && stackTrace[0].getClassName().equals("android.media.MediaCodec"); + } + + @TargetApi(21) + private static boolean isMediaCodecExceptionV21(IllegalStateException error) { + return error instanceof MediaCodec.CodecException; + } + /** * Returns whether the decoder is known to fail when flushed. *

@@ -1085,32 +1863,57 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Returns whether the decoder is known to get stuck during some adaptations where the resolution - * does not change. - *

- * If true is returned, the renderer will work around the issue by queueing and discarding a blank - * frame at a different resolution, which resets the codec's internal state. - *

- * See [Internal: b/27807182]. + * Returns a mode that specifies when the adaptation workaround should be enabled. + * + *

When enabled, the workaround queues and discards a blank frame with a resolution whose width + * and height both equal {@link #ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT}, to reset the decoder's + * internal state when a format change occurs. + * + *

See [Internal: b/27807182]. See GitHub issue #3257. * * @param name The name of the decoder. - * @return True if the decoder is known to get stuck during some adaptations. + * @return The mode specifying when the adaptation workaround should be enabled. */ - private static boolean codecNeedsAdaptationWorkaround(String name) { - return Util.SDK_INT < 24 + private @AdaptationWorkaroundMode int codecAdaptationWorkaroundMode(String name) { + if (Util.SDK_INT <= 25 && "OMX.Exynos.avc.dec.secure".equals(name) + && (Util.MODEL.startsWith("SM-T585") || Util.MODEL.startsWith("SM-A510") + || Util.MODEL.startsWith("SM-A520") || Util.MODEL.startsWith("SM-J700"))) { + return ADAPTATION_WORKAROUND_MODE_ALWAYS; + } else if (Util.SDK_INT < 24 && ("OMX.Nvidia.h264.decode".equals(name) || "OMX.Nvidia.h264.decode.secure".equals(name)) && ("flounder".equals(Util.DEVICE) || "flounder_lte".equals(Util.DEVICE) - || "grouper".equals(Util.DEVICE) || "tilapia".equals(Util.DEVICE)); + || "grouper".equals(Util.DEVICE) || "tilapia".equals(Util.DEVICE))) { + return ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION; + } else { + return ADAPTATION_WORKAROUND_MODE_NEVER; + } + } + + /** + * Returns whether the decoder is known to fail when an attempt is made to reconfigure it with a + * new format's configuration data. + * + *

When enabled, the workaround will always release and recreate the decoder, rather than + * attempting to reconfigure the existing instance. + * + * @param name The name of the decoder. + * @return True if the decoder is known to fail when an attempt is made to reconfigure it with a + * new format's configuration data. + */ + private static boolean codecNeedsReconfigureWorkaround(String name) { + return Util.MODEL.startsWith("SM-T230") && "OMX.MARVELL.VIDEO.HW.CODA7542DECODER".equals(name); } /** * Returns whether the decoder is an H.264/AVC decoder known to fail if NAL units are queued * before the codec specific data. - *

- * If true is returned, the renderer will work around the issue by discarding data up to the SPS. + * + *

If true is returned, the renderer will work around the issue by discarding data up to the + * SPS. * * @param name The name of the decoder. - * @param format The format used to configure the decoder. + * @param format The {@link Format} used to configure the decoder. * @return True if the decoder is known to fail if NAL units are queued before CSD. */ private static boolean codecNeedsDiscardToSpsWorkaround(String name, Format format) { @@ -1119,20 +1922,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Returns whether the decoder is known to handle the propagation of the - * {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device. - *

- * If true is returned, the renderer will work around the issue by approximating end of stream + * Returns whether the decoder is known to handle the propagation of the {@link + * MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device. + * + *

If true is returned, the renderer will work around the issue by approximating end of stream * behavior without relying on the flag being propagated through to an output buffer by the * underlying decoder. * - * @param name The name of the decoder. + * @param codecInfo Information about the {@link MediaCodec}. * @return True if the decoder is known to handle {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} * propagation incorrectly on the host device. False otherwise. */ - private static boolean codecNeedsEosPropagationWorkaround(String name) { - return Util.SDK_INT <= 17 && ("OMX.rk.video_decoder.avc".equals(name) - || "OMX.allwinner.video.decoder.avc".equals(name)); + private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) { + String name = codecInfo.name; + return (Util.SDK_INT <= 25 && "OMX.rk.video_decoder.avc".equals(name)) + || (Util.SDK_INT <= 17 && "OMX.allwinner.video.decoder.avc".equals(name)) + || ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure); } /** @@ -1150,7 +1955,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private static boolean codecNeedsEosFlushWorkaround(String name) { return (Util.SDK_INT <= 23 && "OMX.google.vorbis.decoder".equals(name)) - || (Util.SDK_INT <= 19 && "hb2000".equals(Util.DEVICE) + || (Util.SDK_INT <= 19 + && ("hb2000".equals(Util.DEVICE) || "stvm8".equals(Util.DEVICE)) && ("OMX.amlogic.avc.decoder.awesome".equals(name) || "OMX.amlogic.avc.decoder.awesome.secure".equals(name))); } @@ -1171,17 +1977,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Returns whether the decoder is known to set the number of audio channels in the output format - * to 2 for the given input format, whilst only actually outputting a single channel. - *

- * If true is returned then we explicitly override the number of channels in the output format, - * setting it to 1. + * Returns whether the decoder is known to set the number of audio channels in the output {@link + * Format} to 2 for the given input {@link Format}, whilst only actually outputting a single + * channel. + * + *

If true is returned then we explicitly override the number of channels in the output {@link + * Format}, setting it to 1. * * @param name The decoder name. - * @param format The input format. - * @return True if the decoder is known to set the number of audio channels in the output format - * to 2 for the given input format, whilst only actually outputting a single channel. False - * otherwise. + * @param format The input {@link Format}. + * @return True if the decoder is known to set the number of audio channels in the output {@link + * Format} to 2 for the given input {@link Format}, whilst only actually outputting a single + * channel. False otherwise. */ private static boolean codecNeedsMonoChannelCountWorkaround(String name, Format format) { return Util.SDK_INT <= 18 && format.channelCount == 1 @@ -1189,17 +1996,19 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Returns whether the decoder is known to fail when adapting, despite advertising itself as an - * adaptive decoder. - *

- * If true is returned then we explicitly disable adaptation for the decoder. + * Returns whether the decoder is known to behave incorrectly if flushed prior to having output a + * {@link MediaFormat}. * - * @param name The decoder name. - * @return True if the decoder is known to fail when adapting. + *

If true is returned, the renderer will work around the issue by instantiating a new decoder + * when this case occurs. + * + *

See [Internal: b/141097367]. + * + * @param name The name of the decoder. + * @return True if the decoder is known to behave incorrectly if flushed prior to having output a + * {@link MediaFormat}. False otherwise. */ - private static boolean codecNeedsDisableAdaptationWorkaround(String name) { - return Util.SDK_INT <= 19 && Util.MODEL.equals("ODROID-XU3") - && ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name)); + private static boolean codecNeedsSosFlushWorkaround(String name) { + return Util.SDK_INT == 29 && "c2.android.aac.decoder".equals(name); } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java index a6e24cda7c60..3f90c3a10531 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java @@ -16,7 +16,9 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec; import android.media.MediaCodec; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import java.util.List; /** * Selector of {@link MediaCodec} instances. @@ -24,41 +26,46 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCode public interface MediaCodecSelector { /** - * Default implementation of {@link MediaCodecSelector}. + * Default implementation of {@link MediaCodecSelector}, which returns the preferred decoder for + * the given format. */ - MediaCodecSelector DEFAULT = new MediaCodecSelector() { + MediaCodecSelector DEFAULT = + new MediaCodecSelector() { + @Override + public List getDecoderInfos( + String mimeType, boolean requiresSecureDecoder, boolean requiresTunnelingDecoder) + throws DecoderQueryException { + return MediaCodecUtil.getDecoderInfos( + mimeType, requiresSecureDecoder, requiresTunnelingDecoder); + } - @Override - public MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder) - throws DecoderQueryException { - return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder); - } - - @Override - public MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { - return MediaCodecUtil.getPassthroughDecoderInfo(); - } - - }; + @Override + @Nullable + public MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { + return MediaCodecUtil.getPassthroughDecoderInfo(); + } + }; /** - * Selects a decoder to instantiate for a given mime type. + * Returns a list of decoders that can decode media in the specified MIME type, in priority order. * - * @param mimeType The mime type for which a decoder is required. + * @param mimeType The MIME type for which a decoder is required. * @param requiresSecureDecoder Whether a secure decoder is required. - * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists. + * @param requiresTunnelingDecoder Whether a tunneling decoder is required. + * @return An unmodifiable list of {@link MediaCodecInfo}s corresponding to decoders. May be + * empty. * @throws DecoderQueryException Thrown if there was an error querying decoders. */ - MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder) + List getDecoderInfos( + String mimeType, boolean requiresSecureDecoder, boolean requiresTunnelingDecoder) throws DecoderQueryException; /** * Selects a decoder to instantiate for audio passthrough. * - * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder - * exists. + * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists. * @throws DecoderQueryException Thrown if there was an error querying decoders. */ + @Nullable MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException; - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index fcc2e94ad7bd..11fe93130570 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -21,11 +21,17 @@ import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecList; import android.text.TextUtils; -import android.util.Log; import android.util.Pair; import android.util.SparseIntArray; +import androidx.annotation.CheckResult; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.ColorInfo; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -33,11 +39,11 @@ import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; /** * A utility class for querying the available codecs. */ -@TargetApi(16) @SuppressLint("InlinedApi") public final class MediaCodecUtil { @@ -56,8 +62,6 @@ public final class MediaCodecUtil { } private static final String TAG = "MediaCodecUtil"; - private static final MediaCodecInfo PASSTHROUGH_DECODER_INFO = - MediaCodecInfo.newPassthroughInstance("OMX.google.raw.decoder"); private static final Pattern PROFILE_PATTERN = Pattern.compile("^\\D?(\\d+)$"); private static final HashMap> decoderInfosCache = new HashMap<>(); @@ -68,10 +72,23 @@ public final class MediaCodecUtil { private static final SparseIntArray AVC_LEVEL_NUMBER_TO_CONST; private static final String CODEC_ID_AVC1 = "avc1"; private static final String CODEC_ID_AVC2 = "avc2"; + // VP9 + private static final SparseIntArray VP9_PROFILE_NUMBER_TO_CONST; + private static final SparseIntArray VP9_LEVEL_NUMBER_TO_CONST; + private static final String CODEC_ID_VP09 = "vp09"; // HEVC. private static final Map HEVC_CODEC_STRING_TO_PROFILE_LEVEL; private static final String CODEC_ID_HEV1 = "hev1"; private static final String CODEC_ID_HVC1 = "hvc1"; + // Dolby Vision. + private static final Map DOLBY_VISION_STRING_TO_PROFILE; + private static final Map DOLBY_VISION_STRING_TO_LEVEL; + // AV1. + private static final SparseIntArray AV1_LEVEL_NUMBER_TO_CONST; + private static final String CODEC_ID_AV01 = "av01"; + // MP4A AAC. + private static final SparseIntArray MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE; + private static final String CODEC_ID_MP4A = "mp4a"; // Lazily initialized. private static int maxH264DecodableFrameSize = -1; @@ -80,17 +97,19 @@ public final class MediaCodecUtil { /** * Optional call to warm the codec cache for a given mime type. - *

- * Calling this method may speed up subsequent calls to {@link #getDecoderInfo(String, boolean)} - * and {@link #getDecoderInfos(String, boolean)}. + * + *

Calling this method may speed up subsequent calls to {@link #getDecoderInfo(String, boolean, + * boolean)} and {@link #getDecoderInfos(String, boolean, boolean)}. * * @param mimeType The mime type. * @param secure Whether the decoder is required to support secure decryption. Always pass false * unless secure decryption really is required. + * @param tunneling Whether the decoder is required to support tunneling. Always pass false unless + * tunneling really is required. */ - public static void warmDecoderInfoCache(String mimeType, boolean secure) { + public static void warmDecoderInfoCache(String mimeType, boolean secure, boolean tunneling) { try { - getDecoderInfos(mimeType, secure); + getDecoderInfos(mimeType, secure, tunneling); } catch (DecoderQueryException e) { // Codec warming is best effort, so we can swallow the exception. Log.e(TAG, "Codec warming failed", e); @@ -99,52 +118,61 @@ public final class MediaCodecUtil { /** * Returns information about a decoder suitable for audio passthrough. - ** - * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder - * exists. + * + * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists. + * @throws DecoderQueryException If there was an error querying the available decoders. */ - public static MediaCodecInfo getPassthroughDecoderInfo() { - // TODO: Return null if the raw decoder doesn't exist. - return PASSTHROUGH_DECODER_INFO; + @Nullable + public static MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { + @Nullable + MediaCodecInfo decoderInfo = + getDecoderInfo(MimeTypes.AUDIO_RAW, /* secure= */ false, /* tunneling= */ false); + return decoderInfo == null ? null : MediaCodecInfo.newPassthroughInstance(decoderInfo.name); } /** * Returns information about the preferred decoder for a given mime type. * - * @param mimeType The mime type. + * @param mimeType The MIME type. * @param secure Whether the decoder is required to support secure decryption. Always pass false * unless secure decryption really is required. - * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder - * exists. + * @param tunneling Whether the decoder is required to support tunneling. Always pass false unless + * tunneling really is required. + * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists. * @throws DecoderQueryException If there was an error querying the available decoders. */ - public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure) + @Nullable + public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure, boolean tunneling) throws DecoderQueryException { - List decoderInfos = getDecoderInfos(mimeType, secure); + List decoderInfos = getDecoderInfos(mimeType, secure, tunneling); return decoderInfos.isEmpty() ? null : decoderInfos.get(0); } /** - * Returns all {@link MediaCodecInfo}s for the given mime type, in the order given by - * {@link MediaCodecList}. + * Returns all {@link MediaCodecInfo}s for the given mime type, in the order given by {@link + * MediaCodecList}. * - * @param mimeType The mime type. + * @param mimeType The MIME type. * @param secure Whether the decoder is required to support secure decryption. Always pass false * unless secure decryption really is required. - * @return A list of all @{link MediaCodecInfo}s for the given mime type, in the order - * given by {@link MediaCodecList}. + * @param tunneling Whether the decoder is required to support tunneling. Always pass false unless + * tunneling really is required. + * @return An unmodifiable list of all {@link MediaCodecInfo}s for the given mime type, in the + * order given by {@link MediaCodecList}. * @throws DecoderQueryException If there was an error querying the available decoders. */ - public static synchronized List getDecoderInfos(String mimeType, - boolean secure) throws DecoderQueryException { - CodecKey key = new CodecKey(mimeType, secure); - List decoderInfos = decoderInfosCache.get(key); - if (decoderInfos != null) { - return decoderInfos; + public static synchronized List getDecoderInfos( + String mimeType, boolean secure, boolean tunneling) throws DecoderQueryException { + CodecKey key = new CodecKey(mimeType, secure, tunneling); + @Nullable List cachedDecoderInfos = decoderInfosCache.get(key); + if (cachedDecoderInfos != null) { + return cachedDecoderInfos; } - MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21 - ? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16(); - decoderInfos = getDecoderInfosInternal(key, mediaCodecList); + MediaCodecListCompat mediaCodecList = + Util.SDK_INT >= 21 + ? new MediaCodecListCompatV21(secure, tunneling) + : new MediaCodecListCompatV16(); + ArrayList decoderInfos = getDecoderInfosInternal(key, mediaCodecList); if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) { // Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the // legacy path. We also try this path on API levels 22 and 23 as a defensive measure. @@ -155,49 +183,187 @@ public final class MediaCodecUtil { + ". Assuming: " + decoderInfos.get(0).name); } } - decoderInfos = Collections.unmodifiableList(decoderInfos); - decoderInfosCache.put(key, decoderInfos); + applyWorkarounds(mimeType, decoderInfos); + List unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos); + decoderInfosCache.put(key, unmodifiableDecoderInfos); + return unmodifiableDecoderInfos; + } + + /** + * Returns a copy of the provided decoder list sorted such that decoders with format support are + * listed first. The returned list is modifiable for convenience. + */ + @CheckResult + public static List getDecoderInfosSortedByFormatSupport( + List decoderInfos, Format format) { + decoderInfos = new ArrayList<>(decoderInfos); + sortByScore( + decoderInfos, + decoderInfo -> { + try { + return decoderInfo.isFormatSupported(format) ? 1 : 0; + } catch (DecoderQueryException e) { + return -1; + } + }); return decoderInfos; } - private static List getDecoderInfosInternal( + /** + * Returns the maximum frame size supported by the default H264 decoder. + * + * @return The maximum frame size for an H264 stream that can be decoded on the device. + */ + public static int maxH264DecodableFrameSize() throws DecoderQueryException { + if (maxH264DecodableFrameSize == -1) { + int result = 0; + @Nullable + MediaCodecInfo decoderInfo = + getDecoderInfo(MimeTypes.VIDEO_H264, /* secure= */ false, /* tunneling= */ false); + if (decoderInfo != null) { + for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) { + result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result); + } + // We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are + // the levels mandated by the Android CDD. + result = Math.max(result, Util.SDK_INT >= 21 ? (720 * 480) : (480 * 360)); + } + maxH264DecodableFrameSize = result; + } + return maxH264DecodableFrameSize; + } + + /** + * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the codec + * description string (as defined by RFC 6381) of the given format. + * + * @param format Media format with a codec description string, as defined by RFC 6381. + * @return A pair (profile constant, level constant) if the codec of the {@code format} is + * well-formed and recognized, or null otherwise. + */ + @Nullable + public static Pair getCodecProfileAndLevel(Format format) { + if (format.codecs == null) { + return null; + } + String[] parts = format.codecs.split("\\."); + // Dolby Vision can use DV, AVC or HEVC codec IDs, so check the MIME type first. + if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) { + return getDolbyVisionProfileAndLevel(format.codecs, parts); + } + switch (parts[0]) { + case CODEC_ID_AVC1: + case CODEC_ID_AVC2: + return getAvcProfileAndLevel(format.codecs, parts); + case CODEC_ID_VP09: + return getVp9ProfileAndLevel(format.codecs, parts); + case CODEC_ID_HEV1: + case CODEC_ID_HVC1: + return getHevcProfileAndLevel(format.codecs, parts); + case CODEC_ID_AV01: + return getAv1ProfileAndLevel(format.codecs, parts, format.colorInfo); + case CODEC_ID_MP4A: + return getAacCodecProfileAndLevel(format.codecs, parts); + default: + return null; + } + } + + // Internal methods. + + /** + * Returns {@link MediaCodecInfo}s for the given codec {@link CodecKey} in the order given by + * {@code mediaCodecList}. + * + * @param key The codec key. + * @param mediaCodecList The codec list. + * @return The codec information for usable codecs matching the specified key. + * @throws DecoderQueryException If there was an error querying the available decoders. + */ + private static ArrayList getDecoderInfosInternal( CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException { try { - List decoderInfos = new ArrayList<>(); + ArrayList decoderInfos = new ArrayList<>(); String mimeType = key.mimeType; int numberOfCodecs = mediaCodecList.getCodecCount(); boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit(); // Note: MediaCodecList is sorted by the framework such that the best decoders come first. for (int i = 0; i < numberOfCodecs; i++) { android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); - String codecName = codecInfo.getName(); - if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit)) { - for (String supportedType : codecInfo.getSupportedTypes()) { - if (supportedType.equalsIgnoreCase(mimeType)) { - try { - CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(supportedType); - boolean secure = mediaCodecList.isSecurePlaybackSupported(mimeType, capabilities); - if ((secureDecodersExplicit && key.secure == secure) - || (!secureDecodersExplicit && !key.secure)) { - decoderInfos.add(MediaCodecInfo.newInstance(codecName, mimeType, capabilities)); - } else if (!secureDecodersExplicit && secure) { - decoderInfos.add(MediaCodecInfo.newInstance(codecName + ".secure", mimeType, - capabilities)); - // It only makes sense to have one synthesized secure decoder, return immediately. - return decoderInfos; - } - } catch (Exception e) { - if (Util.SDK_INT <= 23 && !decoderInfos.isEmpty()) { - // Suppress error querying secondary codec capabilities up to API level 23. - Log.e(TAG, "Skipping codec " + codecName + " (failed to query capabilities)"); - } else { - // Rethrow error querying primary codec capabilities, or secondary codec - // capabilities if API level is greater than 23. - Log.e(TAG, "Failed to query codec " + codecName + " (" + supportedType + ")"); - throw e; - } - } - } + if (isAlias(codecInfo)) { + // Skip aliases of other codecs, since they will also be listed under their canonical + // names. + continue; + } + String name = codecInfo.getName(); + if (!isCodecUsableDecoder(codecInfo, name, secureDecodersExplicit, mimeType)) { + continue; + } + @Nullable String codecMimeType = getCodecMimeType(codecInfo, name, mimeType); + if (codecMimeType == null) { + continue; + } + try { + CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(codecMimeType); + boolean tunnelingSupported = + mediaCodecList.isFeatureSupported( + CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities); + boolean tunnelingRequired = + mediaCodecList.isFeatureRequired( + CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities); + if ((!key.tunneling && tunnelingRequired) || (key.tunneling && !tunnelingSupported)) { + continue; + } + boolean secureSupported = + mediaCodecList.isFeatureSupported( + CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities); + boolean secureRequired = + mediaCodecList.isFeatureRequired( + CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities); + if ((!key.secure && secureRequired) || (key.secure && !secureSupported)) { + continue; + } + boolean hardwareAccelerated = isHardwareAccelerated(codecInfo); + boolean softwareOnly = isSoftwareOnly(codecInfo); + boolean vendor = isVendor(codecInfo); + boolean forceDisableAdaptive = codecNeedsDisableAdaptationWorkaround(name); + if ((secureDecodersExplicit && key.secure == secureSupported) + || (!secureDecodersExplicit && !key.secure)) { + decoderInfos.add( + MediaCodecInfo.newInstance( + name, + mimeType, + codecMimeType, + capabilities, + hardwareAccelerated, + softwareOnly, + vendor, + forceDisableAdaptive, + /* forceSecure= */ false)); + } else if (!secureDecodersExplicit && secureSupported) { + decoderInfos.add( + MediaCodecInfo.newInstance( + name + ".secure", + mimeType, + codecMimeType, + capabilities, + hardwareAccelerated, + softwareOnly, + vendor, + forceDisableAdaptive, + /* forceSecure= */ true)); + // It only makes sense to have one synthesized secure decoder, return immediately. + return decoderInfos; + } + } catch (Exception e) { + if (Util.SDK_INT <= 23 && !decoderInfos.isEmpty()) { + // Suppress error querying secondary codec capabilities up to API level 23. + Log.e(TAG, "Skipping codec " + name + " (failed to query capabilities)"); + } else { + // Rethrow error querying primary codec capabilities, or secondary codec + // capabilities if API level is greater than 23. + Log.e(TAG, "Failed to query codec " + name + " (" + codecMimeType + ")"); + throw e; } } } @@ -210,10 +376,60 @@ public final class MediaCodecUtil { } /** - * Returns whether the specified codec is usable for decoding on the current device. + * Returns the codec's supported MIME type for media of type {@code mimeType}, or {@code null} if + * the codec can't be used. + * + * @param info The codec information. + * @param name The name of the codec + * @param mimeType The MIME type. + * @return The codec's supported MIME type for media of type {@code mimeType}, or {@code null} if + * the codec can't be used. If non-null, the returned type will be equal to {@code mimeType} + * except in cases where the codec is known to use a non-standard MIME type alias. */ - private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, String name, - boolean secureDecodersExplicit) { + @Nullable + private static String getCodecMimeType( + android.media.MediaCodecInfo info, + String name, + String mimeType) { + String[] supportedTypes = info.getSupportedTypes(); + for (String supportedType : supportedTypes) { + if (supportedType.equalsIgnoreCase(mimeType)) { + return supportedType; + } + } + + if (mimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) { + // Handle decoders that declare support for DV via MIME types that aren't + // video/dolby-vision. + if ("OMX.MS.HEVCDV.Decoder".equals(name)) { + return "video/hevcdv"; + } else if ("OMX.RTK.video.decoder".equals(name) + || "OMX.realtek.video.decoder.tunneled".equals(name)) { + return "video/dv_hevc"; + } + } else if (mimeType.equals(MimeTypes.AUDIO_ALAC) && "OMX.lge.alac.decoder".equals(name)) { + return "audio/x-lg-alac"; + } else if (mimeType.equals(MimeTypes.AUDIO_FLAC) && "OMX.lge.flac.decoder".equals(name)) { + return "audio/x-lg-flac"; + } + + return null; + } + + /** + * Returns whether the specified codec is usable for decoding on the current device. + * + * @param info The codec information. + * @param name The name of the codec + * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. + * @param mimeType The MIME type. + * @return Whether the specified codec is usable for decoding on the current device. + */ + private static boolean isCodecUsableDecoder( + android.media.MediaCodecInfo info, + String name, + boolean secureDecodersExplicit, + String mimeType) { if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) { return false; } @@ -229,14 +445,12 @@ public final class MediaCodecUtil { return false; } - // Work around https://github.com/google/ExoPlayer/issues/398 - if (Util.SDK_INT < 18 && "OMX.SEC.MP3.Decoder".equals(name)) { - return false; - } - - // Work around https://github.com/google/ExoPlayer/issues/1528 - if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) - && "a70".equals(Util.DEVICE)) { + // Work around https://github.com/google/ExoPlayer/issues/1528 and + // https://github.com/google/ExoPlayer/issues/3171. + if (Util.SDK_INT < 18 + && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) + && ("a70".equals(Util.DEVICE) + || ("Xiaomi".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith("HM")))) { return false; } @@ -269,12 +483,30 @@ public final class MediaCodecUtil { return false; } - // Work around https://github.com/google/ExoPlayer/issues/548 + // Work around https://github.com/google/ExoPlayer/issues/3249. + if (Util.SDK_INT < 24 + && ("OMX.SEC.aac.dec".equals(name) || "OMX.Exynos.AAC.Decoder".equals(name)) + && "samsung".equals(Util.MANUFACTURER) + && (Util.DEVICE.startsWith("zeroflte") // Galaxy S6 + || Util.DEVICE.startsWith("zerolte") // Galaxy S6 Edge + || Util.DEVICE.startsWith("zenlte") // Galaxy S6 Edge+ + || "SC-05G".equals(Util.DEVICE) // Galaxy S6 + || "marinelteatt".equals(Util.DEVICE) // Galaxy S6 Active + || "404SC".equals(Util.DEVICE) // Galaxy S6 Edge + || "SC-04G".equals(Util.DEVICE) + || "SCV31".equals(Util.DEVICE))) { + return false; + } + + // Work around https://github.com/google/ExoPlayer/issues/548. // VP8 decoder on Samsung Galaxy S3/S4/S4 Mini/Tab 3/Note 2 does not render video. if (Util.SDK_INT <= 19 - && "OMX.SEC.vp8.dec".equals(name) && "samsung".equals(Util.MANUFACTURER) - && (Util.DEVICE.startsWith("d2") || Util.DEVICE.startsWith("serrano") - || Util.DEVICE.startsWith("jflte") || Util.DEVICE.startsWith("santos") + && "OMX.SEC.vp8.dec".equals(name) + && "samsung".equals(Util.MANUFACTURER) + && (Util.DEVICE.startsWith("d2") + || Util.DEVICE.startsWith("serrano") + || Util.DEVICE.startsWith("jflte") + || Util.DEVICE.startsWith("santos") || Util.DEVICE.startsWith("t0"))) { return false; } @@ -285,56 +517,197 @@ public final class MediaCodecUtil { return false; } + // MTK E-AC3 decoder doesn't support decoding JOC streams in 2-D. See [Internal: b/69400041]. + if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType) && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) { + return false; + } + return true; } /** - * Returns the maximum frame size supported by the default H264 decoder. + * Modifies a list of {@link MediaCodecInfo}s to apply workarounds where we know better than the + * platform. * - * @return The maximum frame size for an H264 stream that can be decoded on the device. + * @param mimeType The MIME type of input media. + * @param decoderInfos The list to modify. */ - public static int maxH264DecodableFrameSize() throws DecoderQueryException { - if (maxH264DecodableFrameSize == -1) { - int result = 0; - MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false); - if (decoderInfo != null) { - for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) { - result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result); - } - // We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are - // the levels mandated by the Android CDD. - result = Math.max(result, Util.SDK_INT >= 21 ? (720 * 480) : (480 * 360)); + private static void applyWorkarounds(String mimeType, List decoderInfos) { + if (MimeTypes.AUDIO_RAW.equals(mimeType)) { + if (Util.SDK_INT < 26 + && Util.DEVICE.equals("R9") + && decoderInfos.size() == 1 + && decoderInfos.get(0).name.equals("OMX.MTK.AUDIO.DECODER.RAW")) { + // This device does not list a generic raw audio decoder, yet it can be instantiated by + // name. See Issue #5782. + decoderInfos.add( + MediaCodecInfo.newInstance( + /* name= */ "OMX.google.raw.decoder", + /* mimeType= */ MimeTypes.AUDIO_RAW, + /* codecMimeType= */ MimeTypes.AUDIO_RAW, + /* capabilities= */ null, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ true, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false)); } - maxH264DecodableFrameSize = result; + // Work around inconsistent raw audio decoding behavior across different devices. + sortByScore( + decoderInfos, + decoderInfo -> { + String name = decoderInfo.name; + if (name.startsWith("OMX.google") || name.startsWith("c2.android")) { + // Prefer generic decoders over ones provided by the device. + return 1; + } + if (Util.SDK_INT < 26 && name.equals("OMX.MTK.AUDIO.DECODER.RAW")) { + // This decoder may modify the audio, so any other compatible decoders take + // precedence. See [Internal: b/62337687]. + return -1; + } + return 0; + }); } - return maxH264DecodableFrameSize; + + if (Util.SDK_INT < 21 && decoderInfos.size() > 1) { + String firstCodecName = decoderInfos.get(0).name; + if ("OMX.SEC.mp3.dec".equals(firstCodecName) + || "OMX.SEC.MP3.Decoder".equals(firstCodecName) + || "OMX.brcm.audio.mp3.decoder".equals(firstCodecName)) { + // Prefer OMX.google codecs over OMX.SEC.mp3.dec, OMX.SEC.MP3.Decoder and + // OMX.brcm.audio.mp3.decoder on older devices. See: + // https://github.com/google/ExoPlayer/issues/398 and + // https://github.com/google/ExoPlayer/issues/4519. + sortByScore(decoderInfos, decoderInfo -> decoderInfo.name.startsWith("OMX.google") ? 1 : 0); + } + } + + if (Util.SDK_INT < 30 && decoderInfos.size() > 1) { + String firstCodecName = decoderInfos.get(0).name; + // Prefer anything other than OMX.qti.audio.decoder.flac on older devices. See [Internal + // ref: b/147278539] and [Internal ref: b/147354613]. + if ("OMX.qti.audio.decoder.flac".equals(firstCodecName)) { + decoderInfos.add(decoderInfos.remove(0)); + } + } + } + + private static boolean isAlias(android.media.MediaCodecInfo info) { + return Util.SDK_INT >= 29 && isAliasV29(info); + } + + @RequiresApi(29) + private static boolean isAliasV29(android.media.MediaCodecInfo info) { + return info.isAlias(); } /** - * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the given - * codec description string (as defined by RFC 6381). - * - * @param codec A codec description string, as defined by RFC 6381. - * @return A pair (profile constant, level constant) if {@code codec} is well-formed and - * recognized, or null otherwise + * The result of {@link android.media.MediaCodecInfo#isHardwareAccelerated()} for API levels 29+, + * or a best-effort approximation for lower levels. */ - public static Pair getCodecProfileAndLevel(String codec) { - if (codec == null) { - return null; - } - String[] parts = codec.split("\\."); - switch (parts[0]) { - case CODEC_ID_HEV1: - case CODEC_ID_HVC1: - return getHevcProfileAndLevel(codec, parts); - case CODEC_ID_AVC1: - case CODEC_ID_AVC2: - return getAvcProfileAndLevel(codec, parts); - default: - return null; + private static boolean isHardwareAccelerated(android.media.MediaCodecInfo codecInfo) { + if (Util.SDK_INT >= 29) { + return isHardwareAcceleratedV29(codecInfo); } + // codecInfo.isHardwareAccelerated() != codecInfo.isSoftwareOnly() is not necessarily true. + // However, we assume this to be true as an approximation. + return !isSoftwareOnly(codecInfo); } + @TargetApi(29) + private static boolean isHardwareAcceleratedV29(android.media.MediaCodecInfo codecInfo) { + return codecInfo.isHardwareAccelerated(); + } + + /** + * The result of {@link android.media.MediaCodecInfo#isSoftwareOnly()} for API levels 29+, or a + * best-effort approximation for lower levels. + */ + private static boolean isSoftwareOnly(android.media.MediaCodecInfo codecInfo) { + if (Util.SDK_INT >= 29) { + return isSoftwareOnlyV29(codecInfo); + } + String codecName = Util.toLowerInvariant(codecInfo.getName()); + if (codecName.startsWith("arc.")) { // App Runtime for Chrome (ARC) codecs + return false; + } + return codecName.startsWith("omx.google.") + || codecName.startsWith("omx.ffmpeg.") + || (codecName.startsWith("omx.sec.") && codecName.contains(".sw.")) + || codecName.equals("omx.qcom.video.decoder.hevcswvdec") + || codecName.startsWith("c2.android.") + || codecName.startsWith("c2.google.") + || (!codecName.startsWith("omx.") && !codecName.startsWith("c2.")); + } + + @TargetApi(29) + private static boolean isSoftwareOnlyV29(android.media.MediaCodecInfo codecInfo) { + return codecInfo.isSoftwareOnly(); + } + + /** + * The result of {@link android.media.MediaCodecInfo#isVendor()} for API levels 29+, or a + * best-effort approximation for lower levels. + */ + private static boolean isVendor(android.media.MediaCodecInfo codecInfo) { + if (Util.SDK_INT >= 29) { + return isVendorV29(codecInfo); + } + String codecName = Util.toLowerInvariant(codecInfo.getName()); + return !codecName.startsWith("omx.google.") + && !codecName.startsWith("c2.android.") + && !codecName.startsWith("c2.google."); + } + + @TargetApi(29) + private static boolean isVendorV29(android.media.MediaCodecInfo codecInfo) { + return codecInfo.isVendor(); + } + + /** + * Returns whether the decoder is known to fail when adapting, despite advertising itself as an + * adaptive decoder. + * + * @param name The decoder name. + * @return True if the decoder is known to fail when adapting. + */ + private static boolean codecNeedsDisableAdaptationWorkaround(String name) { + return Util.SDK_INT <= 22 + && ("ODROID-XU3".equals(Util.MODEL) || "Nexus 10".equals(Util.MODEL)) + && ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name)); + } + + @Nullable + private static Pair getDolbyVisionProfileAndLevel( + String codec, String[] parts) { + if (parts.length < 3) { + // The codec has fewer parts than required by the Dolby Vision codec string format. + Log.w(TAG, "Ignoring malformed Dolby Vision codec string: " + codec); + return null; + } + // The profile_space gets ignored. + Matcher matcher = PROFILE_PATTERN.matcher(parts[1]); + if (!matcher.matches()) { + Log.w(TAG, "Ignoring malformed Dolby Vision codec string: " + codec); + return null; + } + @Nullable String profileString = matcher.group(1); + @Nullable Integer profile = DOLBY_VISION_STRING_TO_PROFILE.get(profileString); + if (profile == null) { + Log.w(TAG, "Unknown Dolby Vision profile string: " + profileString); + return null; + } + String levelString = parts[2]; + @Nullable Integer level = DOLBY_VISION_STRING_TO_LEVEL.get(levelString); + if (level == null) { + Log.w(TAG, "Unknown Dolby Vision level string: " + levelString); + return null; + } + return new Pair<>(profile, level); + } + + @Nullable private static Pair getHevcProfileAndLevel(String codec, String[] parts) { if (parts.length < 4) { // The codec has fewer parts than required by the HEVC codec string format. @@ -347,7 +720,7 @@ public final class MediaCodecUtil { Log.w(TAG, "Ignoring malformed HEVC codec string: " + codec); return null; } - String profileString = matcher.group(1); + @Nullable String profileString = matcher.group(1); int profile; if ("1".equals(profileString)) { profile = CodecProfileLevel.HEVCProfileMain; @@ -357,31 +730,33 @@ public final class MediaCodecUtil { Log.w(TAG, "Unknown HEVC profile string: " + profileString); return null; } - Integer level = HEVC_CODEC_STRING_TO_PROFILE_LEVEL.get(parts[3]); + @Nullable String levelString = parts[3]; + @Nullable Integer level = HEVC_CODEC_STRING_TO_PROFILE_LEVEL.get(levelString); if (level == null) { - Log.w(TAG, "Unknown HEVC level string: " + matcher.group(1)); + Log.w(TAG, "Unknown HEVC level string: " + levelString); return null; } return new Pair<>(profile, level); } - private static Pair getAvcProfileAndLevel(String codec, String[] codecsParts) { - if (codecsParts.length < 2) { + @Nullable + private static Pair getAvcProfileAndLevel(String codec, String[] parts) { + if (parts.length < 2) { // The codec has fewer parts than required by the AVC codec string format. Log.w(TAG, "Ignoring malformed AVC codec string: " + codec); return null; } - Integer profileInteger; - Integer levelInteger; + int profileInteger; + int levelInteger; try { - if (codecsParts[1].length() == 6) { + if (parts[1].length() == 6) { // Format: avc1.xxccyy, where xx is profile and yy level, both hexadecimal. - profileInteger = Integer.parseInt(codecsParts[1].substring(0, 2), 16); - levelInteger = Integer.parseInt(codecsParts[1].substring(4), 16); - } else if (codecsParts.length >= 3) { + profileInteger = Integer.parseInt(parts[1].substring(0, 2), 16); + levelInteger = Integer.parseInt(parts[1].substring(4), 16); + } else if (parts.length >= 3) { // Format: avc1.xx.[y]yy where xx is profile and [y]yy level, both decimal. - profileInteger = Integer.parseInt(codecsParts[1]); - levelInteger = Integer.parseInt(codecsParts[2]); + profileInteger = Integer.parseInt(parts[1]); + levelInteger = Integer.parseInt(parts[2]); } else { // We don't recognize the format. Log.w(TAG, "Ignoring malformed AVC codec string: " + codec); @@ -392,19 +767,95 @@ public final class MediaCodecUtil { return null; } - Integer profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger); - if (profile == null) { + int profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger, -1); + if (profile == -1) { Log.w(TAG, "Unknown AVC profile: " + profileInteger); return null; } - Integer level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger); - if (level == null) { + int level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1); + if (level == -1) { Log.w(TAG, "Unknown AVC level: " + levelInteger); return null; } return new Pair<>(profile, level); } + @Nullable + private static Pair getVp9ProfileAndLevel(String codec, String[] parts) { + if (parts.length < 3) { + Log.w(TAG, "Ignoring malformed VP9 codec string: " + codec); + return null; + } + int profileInteger; + int levelInteger; + try { + profileInteger = Integer.parseInt(parts[1]); + levelInteger = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed VP9 codec string: " + codec); + return null; + } + + int profile = VP9_PROFILE_NUMBER_TO_CONST.get(profileInteger, -1); + if (profile == -1) { + Log.w(TAG, "Unknown VP9 profile: " + profileInteger); + return null; + } + int level = VP9_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1); + if (level == -1) { + Log.w(TAG, "Unknown VP9 level: " + levelInteger); + return null; + } + return new Pair<>(profile, level); + } + + @Nullable + private static Pair getAv1ProfileAndLevel( + String codec, String[] parts, @Nullable ColorInfo colorInfo) { + if (parts.length < 4) { + Log.w(TAG, "Ignoring malformed AV1 codec string: " + codec); + return null; + } + int profileInteger; + int levelInteger; + int bitDepthInteger; + try { + profileInteger = Integer.parseInt(parts[1]); + levelInteger = Integer.parseInt(parts[2].substring(0, 2)); + bitDepthInteger = Integer.parseInt(parts[3]); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed AV1 codec string: " + codec); + return null; + } + + if (profileInteger != 0) { + Log.w(TAG, "Unknown AV1 profile: " + profileInteger); + return null; + } + if (bitDepthInteger != 8 && bitDepthInteger != 10) { + Log.w(TAG, "Unknown AV1 bit depth: " + bitDepthInteger); + return null; + } + int profile; + if (bitDepthInteger == 8) { + profile = CodecProfileLevel.AV1ProfileMain8; + } else if (colorInfo != null + && (colorInfo.hdrStaticInfo != null + || colorInfo.colorTransfer == C.COLOR_TRANSFER_HLG + || colorInfo.colorTransfer == C.COLOR_TRANSFER_ST2084)) { + profile = CodecProfileLevel.AV1ProfileMain10HDR10; + } else { + profile = CodecProfileLevel.AV1ProfileMain10; + } + + int level = AV1_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1); + if (level == -1) { + Log.w(TAG, "Unknown AV1 level: " + levelInteger); + return null; + } + return new Pair<>(profile, level); + } + /** * Conversion values taken from ISO 14496-10 Table A-1. * @@ -414,25 +865,73 @@ public final class MediaCodecUtil { */ private static int avcLevelToMaxFrameSize(int avcLevel) { switch (avcLevel) { - case CodecProfileLevel.AVCLevel1: return 99 * 16 * 16; - case CodecProfileLevel.AVCLevel1b: return 99 * 16 * 16; - case CodecProfileLevel.AVCLevel12: return 396 * 16 * 16; - case CodecProfileLevel.AVCLevel13: return 396 * 16 * 16; - case CodecProfileLevel.AVCLevel2: return 396 * 16 * 16; - case CodecProfileLevel.AVCLevel21: return 792 * 16 * 16; - case CodecProfileLevel.AVCLevel22: return 1620 * 16 * 16; - case CodecProfileLevel.AVCLevel3: return 1620 * 16 * 16; - case CodecProfileLevel.AVCLevel31: return 3600 * 16 * 16; - case CodecProfileLevel.AVCLevel32: return 5120 * 16 * 16; - case CodecProfileLevel.AVCLevel4: return 8192 * 16 * 16; - case CodecProfileLevel.AVCLevel41: return 8192 * 16 * 16; - case CodecProfileLevel.AVCLevel42: return 8704 * 16 * 16; - case CodecProfileLevel.AVCLevel5: return 22080 * 16 * 16; - case CodecProfileLevel.AVCLevel51: return 36864 * 16 * 16; - default: return -1; + case CodecProfileLevel.AVCLevel1: + case CodecProfileLevel.AVCLevel1b: + return 99 * 16 * 16; + case CodecProfileLevel.AVCLevel12: + case CodecProfileLevel.AVCLevel13: + case CodecProfileLevel.AVCLevel2: + return 396 * 16 * 16; + case CodecProfileLevel.AVCLevel21: + return 792 * 16 * 16; + case CodecProfileLevel.AVCLevel22: + case CodecProfileLevel.AVCLevel3: + return 1620 * 16 * 16; + case CodecProfileLevel.AVCLevel31: + return 3600 * 16 * 16; + case CodecProfileLevel.AVCLevel32: + return 5120 * 16 * 16; + case CodecProfileLevel.AVCLevel4: + case CodecProfileLevel.AVCLevel41: + return 8192 * 16 * 16; + case CodecProfileLevel.AVCLevel42: + return 8704 * 16 * 16; + case CodecProfileLevel.AVCLevel5: + return 22080 * 16 * 16; + case CodecProfileLevel.AVCLevel51: + case CodecProfileLevel.AVCLevel52: + return 36864 * 16 * 16; + default: + return -1; } } + @Nullable + private static Pair getAacCodecProfileAndLevel(String codec, String[] parts) { + if (parts.length != 3) { + Log.w(TAG, "Ignoring malformed MP4A codec string: " + codec); + return null; + } + try { + // Get the object type indication, which is a hexadecimal value (see RFC 6381/ISO 14496-1). + int objectTypeIndication = Integer.parseInt(parts[1], 16); + @Nullable String mimeType = MimeTypes.getMimeTypeFromMp4ObjectType(objectTypeIndication); + if (MimeTypes.AUDIO_AAC.equals(mimeType)) { + // For MPEG-4 audio this is followed by an audio object type indication as a decimal number. + int audioObjectTypeIndication = Integer.parseInt(parts[2]); + int profile = MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.get(audioObjectTypeIndication, -1); + if (profile != -1) { + // Level is set to zero in AAC decoder CodecProfileLevels. + return new Pair<>(profile, 0); + } + } + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed MP4A codec string: " + codec); + } + return null; + } + + /** Stably sorts the provided {@code list} in-place, in order of decreasing score. */ + private static void sortByScore(List list, ScoreProvider scoreProvider) { + Collections.sort(list, (a, b) -> scoreProvider.getScore(b) - scoreProvider.getScore(a)); + } + + /** Interface for providers of item scores. */ + private interface ScoreProvider { + /** Returns the score of the provided item. */ + int getScore(T t); + } + private interface MediaCodecListCompat { /** @@ -452,12 +951,11 @@ public final class MediaCodecUtil { */ boolean secureDecodersExplicit(); - /** - * Whether secure playback is supported for the given {@link CodecCapabilities}, which should - * have been obtained from a {@link android.media.MediaCodecInfo} obtained from this list. - */ - boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities); + /** Whether the specified {@link CodecCapabilities} {@code feature} is supported. */ + boolean isFeatureSupported(String feature, String mimeType, CodecCapabilities capabilities); + /** Whether the specified {@link CodecCapabilities} {@code feature} is required. */ + boolean isFeatureRequired(String feature, String mimeType, CodecCapabilities capabilities); } @TargetApi(21) @@ -465,10 +963,15 @@ public final class MediaCodecUtil { private final int codecKind; - private android.media.MediaCodecInfo[] mediaCodecInfos; + @Nullable private android.media.MediaCodecInfo[] mediaCodecInfos; - public MediaCodecListCompatV21(boolean includeSecure) { - codecKind = includeSecure ? MediaCodecList.ALL_CODECS : MediaCodecList.REGULAR_CODECS; + // the constructor does not initialize fields: mediaCodecInfos + @SuppressWarnings("nullness:initialization.fields.uninitialized") + public MediaCodecListCompatV21(boolean includeSecure, boolean includeTunneling) { + codecKind = + includeSecure || includeTunneling + ? MediaCodecList.ALL_CODECS + : MediaCodecList.REGULAR_CODECS; } @Override @@ -477,6 +980,8 @@ public final class MediaCodecUtil { return mediaCodecInfos.length; } + // incompatible types in return. + @SuppressWarnings("nullness:return.type.incompatible") @Override public android.media.MediaCodecInfo getCodecInfoAt(int index) { ensureMediaCodecInfosInitialized(); @@ -489,10 +994,18 @@ public final class MediaCodecUtil { } @Override - public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities) { - return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback); + public boolean isFeatureSupported( + String feature, String mimeType, CodecCapabilities capabilities) { + return capabilities.isFeatureSupported(feature); } + @Override + public boolean isFeatureRequired( + String feature, String mimeType, CodecCapabilities capabilities) { + return capabilities.isFeatureRequired(feature); + } + + @EnsuresNonNull({"mediaCodecInfos"}) private void ensureMediaCodecInfosInitialized() { if (mediaCodecInfos == null) { mediaCodecInfos = new MediaCodecList(codecKind).getCodecInfos(); @@ -501,7 +1014,6 @@ public final class MediaCodecUtil { } - @SuppressWarnings("deprecation") private static final class MediaCodecListCompatV16 implements MediaCodecListCompat { @Override @@ -520,10 +1032,18 @@ public final class MediaCodecUtil { } @Override - public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities) { + public boolean isFeatureSupported( + String feature, String mimeType, CodecCapabilities capabilities) { // Secure decoders weren't explicitly listed prior to API level 21. We assume that a secure // H264 decoder exists. - return MimeTypes.VIDEO_H264.equals(mimeType); + return CodecCapabilities.FEATURE_SecurePlayback.equals(feature) + && MimeTypes.VIDEO_H264.equals(mimeType); + } + + @Override + public boolean isFeatureRequired( + String feature, String mimeType, CodecCapabilities capabilities) { + return false; } } @@ -532,23 +1052,26 @@ public final class MediaCodecUtil { public final String mimeType; public final boolean secure; + public final boolean tunneling; - public CodecKey(String mimeType, boolean secure) { + public CodecKey(String mimeType, boolean secure, boolean tunneling) { this.mimeType = mimeType; this.secure = secure; + this.tunneling = tunneling; } @Override public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + ((mimeType == null) ? 0 : mimeType.hashCode()); + result = prime * result + mimeType.hashCode(); result = prime * result + (secure ? 1231 : 1237); + result = prime * result + (tunneling ? 1231 : 1237); return result; } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -556,7 +1079,9 @@ public final class MediaCodecUtil { return false; } CodecKey other = (CodecKey) obj; - return TextUtils.equals(mimeType, other.mimeType) && secure == other.secure; + return TextUtils.equals(mimeType, other.mimeType) + && secure == other.secure + && tunneling == other.tunneling; } } @@ -567,6 +1092,9 @@ public final class MediaCodecUtil { AVC_PROFILE_NUMBER_TO_CONST.put(77, CodecProfileLevel.AVCProfileMain); AVC_PROFILE_NUMBER_TO_CONST.put(88, CodecProfileLevel.AVCProfileExtended); AVC_PROFILE_NUMBER_TO_CONST.put(100, CodecProfileLevel.AVCProfileHigh); + AVC_PROFILE_NUMBER_TO_CONST.put(110, CodecProfileLevel.AVCProfileHigh10); + AVC_PROFILE_NUMBER_TO_CONST.put(122, CodecProfileLevel.AVCProfileHigh422); + AVC_PROFILE_NUMBER_TO_CONST.put(244, CodecProfileLevel.AVCProfileHigh444); AVC_LEVEL_NUMBER_TO_CONST = new SparseIntArray(); AVC_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.AVCLevel1); @@ -587,6 +1115,26 @@ public final class MediaCodecUtil { AVC_LEVEL_NUMBER_TO_CONST.put(51, CodecProfileLevel.AVCLevel51); AVC_LEVEL_NUMBER_TO_CONST.put(52, CodecProfileLevel.AVCLevel52); + VP9_PROFILE_NUMBER_TO_CONST = new SparseIntArray(); + VP9_PROFILE_NUMBER_TO_CONST.put(0, CodecProfileLevel.VP9Profile0); + VP9_PROFILE_NUMBER_TO_CONST.put(1, CodecProfileLevel.VP9Profile1); + VP9_PROFILE_NUMBER_TO_CONST.put(2, CodecProfileLevel.VP9Profile2); + VP9_PROFILE_NUMBER_TO_CONST.put(3, CodecProfileLevel.VP9Profile3); + VP9_LEVEL_NUMBER_TO_CONST = new SparseIntArray(); + VP9_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.VP9Level1); + VP9_LEVEL_NUMBER_TO_CONST.put(11, CodecProfileLevel.VP9Level11); + VP9_LEVEL_NUMBER_TO_CONST.put(20, CodecProfileLevel.VP9Level2); + VP9_LEVEL_NUMBER_TO_CONST.put(21, CodecProfileLevel.VP9Level21); + VP9_LEVEL_NUMBER_TO_CONST.put(30, CodecProfileLevel.VP9Level3); + VP9_LEVEL_NUMBER_TO_CONST.put(31, CodecProfileLevel.VP9Level31); + VP9_LEVEL_NUMBER_TO_CONST.put(40, CodecProfileLevel.VP9Level4); + VP9_LEVEL_NUMBER_TO_CONST.put(41, CodecProfileLevel.VP9Level41); + VP9_LEVEL_NUMBER_TO_CONST.put(50, CodecProfileLevel.VP9Level5); + VP9_LEVEL_NUMBER_TO_CONST.put(51, CodecProfileLevel.VP9Level51); + VP9_LEVEL_NUMBER_TO_CONST.put(60, CodecProfileLevel.VP9Level6); + VP9_LEVEL_NUMBER_TO_CONST.put(61, CodecProfileLevel.VP9Level61); + VP9_LEVEL_NUMBER_TO_CONST.put(62, CodecProfileLevel.VP9Level62); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL = new HashMap<>(); HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L30", CodecProfileLevel.HEVCMainTierLevel1); HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L60", CodecProfileLevel.HEVCMainTierLevel2); @@ -615,6 +1163,70 @@ public final class MediaCodecUtil { HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H180", CodecProfileLevel.HEVCHighTierLevel6); HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H183", CodecProfileLevel.HEVCHighTierLevel61); HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H186", CodecProfileLevel.HEVCHighTierLevel62); - } + DOLBY_VISION_STRING_TO_PROFILE = new HashMap<>(); + DOLBY_VISION_STRING_TO_PROFILE.put("00", CodecProfileLevel.DolbyVisionProfileDvavPer); + DOLBY_VISION_STRING_TO_PROFILE.put("01", CodecProfileLevel.DolbyVisionProfileDvavPen); + DOLBY_VISION_STRING_TO_PROFILE.put("02", CodecProfileLevel.DolbyVisionProfileDvheDer); + DOLBY_VISION_STRING_TO_PROFILE.put("03", CodecProfileLevel.DolbyVisionProfileDvheDen); + DOLBY_VISION_STRING_TO_PROFILE.put("04", CodecProfileLevel.DolbyVisionProfileDvheDtr); + DOLBY_VISION_STRING_TO_PROFILE.put("05", CodecProfileLevel.DolbyVisionProfileDvheStn); + DOLBY_VISION_STRING_TO_PROFILE.put("06", CodecProfileLevel.DolbyVisionProfileDvheDth); + DOLBY_VISION_STRING_TO_PROFILE.put("07", CodecProfileLevel.DolbyVisionProfileDvheDtb); + DOLBY_VISION_STRING_TO_PROFILE.put("08", CodecProfileLevel.DolbyVisionProfileDvheSt); + DOLBY_VISION_STRING_TO_PROFILE.put("09", CodecProfileLevel.DolbyVisionProfileDvavSe); + + DOLBY_VISION_STRING_TO_LEVEL = new HashMap<>(); + DOLBY_VISION_STRING_TO_LEVEL.put("01", CodecProfileLevel.DolbyVisionLevelHd24); + DOLBY_VISION_STRING_TO_LEVEL.put("02", CodecProfileLevel.DolbyVisionLevelHd30); + DOLBY_VISION_STRING_TO_LEVEL.put("03", CodecProfileLevel.DolbyVisionLevelFhd24); + DOLBY_VISION_STRING_TO_LEVEL.put("04", CodecProfileLevel.DolbyVisionLevelFhd30); + DOLBY_VISION_STRING_TO_LEVEL.put("05", CodecProfileLevel.DolbyVisionLevelFhd60); + DOLBY_VISION_STRING_TO_LEVEL.put("06", CodecProfileLevel.DolbyVisionLevelUhd24); + DOLBY_VISION_STRING_TO_LEVEL.put("07", CodecProfileLevel.DolbyVisionLevelUhd30); + DOLBY_VISION_STRING_TO_LEVEL.put("08", CodecProfileLevel.DolbyVisionLevelUhd48); + DOLBY_VISION_STRING_TO_LEVEL.put("09", CodecProfileLevel.DolbyVisionLevelUhd60); + + // See https://aomediacodec.github.io/av1-spec/av1-spec.pdf Annex A: Profiles and levels for + // more information on mapping AV1 codec strings to levels. + AV1_LEVEL_NUMBER_TO_CONST = new SparseIntArray(); + AV1_LEVEL_NUMBER_TO_CONST.put(0, CodecProfileLevel.AV1Level2); + AV1_LEVEL_NUMBER_TO_CONST.put(1, CodecProfileLevel.AV1Level21); + AV1_LEVEL_NUMBER_TO_CONST.put(2, CodecProfileLevel.AV1Level22); + AV1_LEVEL_NUMBER_TO_CONST.put(3, CodecProfileLevel.AV1Level23); + AV1_LEVEL_NUMBER_TO_CONST.put(4, CodecProfileLevel.AV1Level3); + AV1_LEVEL_NUMBER_TO_CONST.put(5, CodecProfileLevel.AV1Level31); + AV1_LEVEL_NUMBER_TO_CONST.put(6, CodecProfileLevel.AV1Level32); + AV1_LEVEL_NUMBER_TO_CONST.put(7, CodecProfileLevel.AV1Level33); + AV1_LEVEL_NUMBER_TO_CONST.put(8, CodecProfileLevel.AV1Level4); + AV1_LEVEL_NUMBER_TO_CONST.put(9, CodecProfileLevel.AV1Level41); + AV1_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.AV1Level42); + AV1_LEVEL_NUMBER_TO_CONST.put(11, CodecProfileLevel.AV1Level43); + AV1_LEVEL_NUMBER_TO_CONST.put(12, CodecProfileLevel.AV1Level5); + AV1_LEVEL_NUMBER_TO_CONST.put(13, CodecProfileLevel.AV1Level51); + AV1_LEVEL_NUMBER_TO_CONST.put(14, CodecProfileLevel.AV1Level52); + AV1_LEVEL_NUMBER_TO_CONST.put(15, CodecProfileLevel.AV1Level53); + AV1_LEVEL_NUMBER_TO_CONST.put(16, CodecProfileLevel.AV1Level6); + AV1_LEVEL_NUMBER_TO_CONST.put(17, CodecProfileLevel.AV1Level61); + AV1_LEVEL_NUMBER_TO_CONST.put(18, CodecProfileLevel.AV1Level62); + AV1_LEVEL_NUMBER_TO_CONST.put(19, CodecProfileLevel.AV1Level63); + AV1_LEVEL_NUMBER_TO_CONST.put(20, CodecProfileLevel.AV1Level7); + AV1_LEVEL_NUMBER_TO_CONST.put(21, CodecProfileLevel.AV1Level71); + AV1_LEVEL_NUMBER_TO_CONST.put(22, CodecProfileLevel.AV1Level72); + AV1_LEVEL_NUMBER_TO_CONST.put(23, CodecProfileLevel.AV1Level73); + + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE = new SparseIntArray(); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(1, CodecProfileLevel.AACObjectMain); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(2, CodecProfileLevel.AACObjectLC); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(3, CodecProfileLevel.AACObjectSSR); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(4, CodecProfileLevel.AACObjectLTP); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(5, CodecProfileLevel.AACObjectHE); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(6, CodecProfileLevel.AACObjectScalable); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(17, CodecProfileLevel.AACObjectERLC); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(20, CodecProfileLevel.AACObjectERScalable); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(23, CodecProfileLevel.AACObjectLD); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(29, CodecProfileLevel.AACObjectHE_PS); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(39, CodecProfileLevel.AACObjectELD); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(42, CodecProfileLevel.AACObjectXHE); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java new file mode 100644 index 000000000000..cafaaa7c83d9 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec; + +import android.media.MediaFormat; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.ColorInfo; +import java.nio.ByteBuffer; +import java.util.List; + +/** Helper class for configuring {@link MediaFormat} instances. */ +public final class MediaFormatUtil { + + private MediaFormatUtil() {} + + /** + * Sets a {@link MediaFormat} {@link String} value. + * + * @param format The {@link MediaFormat} being configured. + * @param key The key to set. + * @param value The value to set. + */ + public static void setString(MediaFormat format, String key, String value) { + format.setString(key, value); + } + + /** + * Sets a {@link MediaFormat}'s codec specific data buffers. + * + * @param format The {@link MediaFormat} being configured. + * @param csdBuffers The csd buffers to set. + */ + public static void setCsdBuffers(MediaFormat format, List csdBuffers) { + for (int i = 0; i < csdBuffers.size(); i++) { + format.setByteBuffer("csd-" + i, ByteBuffer.wrap(csdBuffers.get(i))); + } + } + + /** + * Sets a {@link MediaFormat} integer value. Does nothing if {@code value} is {@link + * Format#NO_VALUE}. + * + * @param format The {@link MediaFormat} being configured. + * @param key The key to set. + * @param value The value to set. + */ + public static void maybeSetInteger(MediaFormat format, String key, int value) { + if (value != Format.NO_VALUE) { + format.setInteger(key, value); + } + } + + /** + * Sets a {@link MediaFormat} float value. Does nothing if {@code value} is {@link + * Format#NO_VALUE}. + * + * @param format The {@link MediaFormat} being configured. + * @param key The key to set. + * @param value The value to set. + */ + public static void maybeSetFloat(MediaFormat format, String key, float value) { + if (value != Format.NO_VALUE) { + format.setFloat(key, value); + } + } + + /** + * Sets a {@link MediaFormat} {@link ByteBuffer} value. Does nothing if {@code value} is null. + * + * @param format The {@link MediaFormat} being configured. + * @param key The key to set. + * @param value The {@link byte[]} that will be wrapped to obtain the value. + */ + public static void maybeSetByteBuffer(MediaFormat format, String key, @Nullable byte[] value) { + if (value != null) { + format.setByteBuffer(key, ByteBuffer.wrap(value)); + } + } + + /** + * Sets a {@link MediaFormat}'s color information. Does nothing if {@code colorInfo} is null. + * + * @param format The {@link MediaFormat} being configured. + * @param colorInfo The color info to set. + */ + @SuppressWarnings("InlinedApi") + public static void maybeSetColorInfo(MediaFormat format, @Nullable ColorInfo colorInfo) { + if (colorInfo != null) { + maybeSetInteger(format, MediaFormat.KEY_COLOR_TRANSFER, colorInfo.colorTransfer); + maybeSetInteger(format, MediaFormat.KEY_COLOR_STANDARD, colorInfo.colorSpace); + maybeSetInteger(format, MediaFormat.KEY_COLOR_RANGE, colorInfo.colorRange); + maybeSetByteBuffer(format, MediaFormat.KEY_HDR_STATIC_INFO, colorInfo.hdrStaticInfo); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/package-info.java new file mode 100644 index 000000000000..c8dd17d0dff3 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/Metadata.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/Metadata.java index 6841c460888d..16f01c4627cc 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/Metadata.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/Metadata.java @@ -17,6 +17,9 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata; import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.List; @@ -25,10 +28,27 @@ import java.util.List; */ public final class Metadata implements Parcelable { - /** - * A metadata entry. - */ - public interface Entry extends Parcelable {} + /** A metadata entry. */ + public interface Entry extends Parcelable { + + /** + * Returns the {@link Format} that can be used to decode the wrapped metadata in {@link + * #getWrappedMetadataBytes()}, or null if this Entry doesn't contain wrapped metadata. + */ + @Nullable + default Format getWrappedMetadataFormat() { + return null; + } + + /** + * Returns the bytes of the wrapped metadata in this Entry, or null if it doesn't contain + * wrapped metadata. + */ + @Nullable + default byte[] getWrappedMetadataBytes() { + return null; + } + } private final Entry[] entries; @@ -36,19 +56,15 @@ public final class Metadata implements Parcelable { * @param entries The metadata entries. */ public Metadata(Entry... entries) { - this.entries = entries == null ? new Entry[0] : entries; + this.entries = entries; } /** * @param entries The metadata entries. */ public Metadata(List entries) { - if (entries != null) { - this.entries = new Entry[entries.size()]; - entries.toArray(this.entries); - } else { - this.entries = new Entry[0]; - } + this.entries = new Entry[entries.size()]; + entries.toArray(this.entries); } /* package */ Metadata(Parcel in) { @@ -75,8 +91,36 @@ public final class Metadata implements Parcelable { return entries[index]; } + /** + * Returns a copy of this metadata with the entries of the specified metadata appended. Returns + * this instance if {@code other} is null. + * + * @param other The metadata that holds the entries to append. If null, this methods returns this + * instance. + * @return The metadata instance with the appended entries. + */ + public Metadata copyWithAppendedEntriesFrom(@Nullable Metadata other) { + if (other == null) { + return this; + } + return copyWithAppendedEntries(other.entries); + } + + /** + * Returns a copy of this metadata with the specified entries appended. + * + * @param entriesToAppend The entries to append. + * @return The metadata instance with the appended entries. + */ + public Metadata copyWithAppendedEntries(Entry... entriesToAppend) { + if (entriesToAppend.length == 0) { + return this; + } + return new Metadata(Util.nullSafeArrayConcatenation(entries, entriesToAppend)); + } + @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -92,6 +136,13 @@ public final class Metadata implements Parcelable { return Arrays.hashCode(entries); } + @Override + public String toString() { + return "entries=" + Arrays.toString(entries); + } + + // Parcelable implementation. + @Override public int describeContents() { return 0; @@ -105,16 +156,16 @@ public final class Metadata implements Parcelable { } } - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public Metadata createFromParcel(Parcel in) { - return new Metadata(in); - } - - @Override - public Metadata[] newArray(int size) { - return new Metadata[0]; - } - }; + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public Metadata createFromParcel(Parcel in) { + return new Metadata(in); + } + @Override + public Metadata[] newArray(int size) { + return new Metadata[size]; + } + }; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoder.java index 8d8fd06ae48e..1bc1c7dc069f 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoder.java @@ -15,6 +15,8 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata; +import androidx.annotation.Nullable; + /** * Decodes metadata from binary data. */ @@ -24,9 +26,8 @@ public interface MetadataDecoder { * Decodes a {@link Metadata} element from the provided input buffer. * * @param inputBuffer The input buffer to decode. - * @return The decoded metadata object. - * @throws MetadataDecoderException If a problem occurred decoding the data. + * @return The decoded metadata object, or null if the metadata could not be decoded. */ - Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException; - + @Nullable + Metadata decode(MetadataInputBuffer inputBuffer); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java index d88baab8dd6a..30f6aad4a945 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java @@ -15,8 +15,10 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.icy.IcyDecoder; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.scte35.SpliceInfoDecoder; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; @@ -46,38 +48,47 @@ public interface MetadataDecoderFactory { /** * Default {@link MetadataDecoder} implementation. - *

- * The formats supported by this factory are: + * + *

The formats supported by this factory are: + * *

    - *
  • ID3 ({@link Id3Decoder})
  • - *
  • EMSG ({@link EventMessageDecoder})
  • - *
  • SCTE-35 ({@link SpliceInfoDecoder})
  • + *
  • ID3 ({@link Id3Decoder}) + *
  • EMSG ({@link EventMessageDecoder}) + *
  • SCTE-35 ({@link SpliceInfoDecoder}) + *
  • ICY ({@link IcyDecoder}) *
*/ - MetadataDecoderFactory DEFAULT = new MetadataDecoderFactory() { + MetadataDecoderFactory DEFAULT = + new MetadataDecoderFactory() { - @Override - public boolean supportsFormat(Format format) { - String mimeType = format.sampleMimeType; - return MimeTypes.APPLICATION_ID3.equals(mimeType) - || MimeTypes.APPLICATION_EMSG.equals(mimeType) - || MimeTypes.APPLICATION_SCTE35.equals(mimeType); - } - - @Override - public MetadataDecoder createDecoder(Format format) { - switch (format.sampleMimeType) { - case MimeTypes.APPLICATION_ID3: - return new Id3Decoder(); - case MimeTypes.APPLICATION_EMSG: - return new EventMessageDecoder(); - case MimeTypes.APPLICATION_SCTE35: - return new SpliceInfoDecoder(); - default: - throw new IllegalArgumentException("Attempted to create decoder for unsupported format"); - } - } - - }; + @Override + public boolean supportsFormat(Format format) { + @Nullable String mimeType = format.sampleMimeType; + return MimeTypes.APPLICATION_ID3.equals(mimeType) + || MimeTypes.APPLICATION_EMSG.equals(mimeType) + || MimeTypes.APPLICATION_SCTE35.equals(mimeType) + || MimeTypes.APPLICATION_ICY.equals(mimeType); + } + @Override + public MetadataDecoder createDecoder(Format format) { + @Nullable String mimeType = format.sampleMimeType; + if (mimeType != null) { + switch (mimeType) { + case MimeTypes.APPLICATION_ID3: + return new Id3Decoder(); + case MimeTypes.APPLICATION_EMSG: + return new EventMessageDecoder(); + case MimeTypes.APPLICATION_SCTE35: + return new SpliceInfoDecoder(); + case MimeTypes.APPLICATION_ICY: + return new IcyDecoder(); + default: + break; + } + } + throw new IllegalArgumentException( + "Attempted to create decoder for unsupported MIME type: " + mimeType); + } + }; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderException.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataOutput.java similarity index 56% rename from mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderException.java rename to mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataOutput.java index 3a506d9df4c6..025f9f01bc34 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderException.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataOutput.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2017 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. @@ -16,23 +16,15 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata; /** - * Thrown when an error occurs decoding metadata. + * Receives metadata output. */ -public class MetadataDecoderException extends Exception { +public interface MetadataOutput { /** - * @param message The detail message for this exception. + * Called when there is metadata associated with current playback time. + * + * @param metadata The metadata. */ - public MetadataDecoderException(String message) { - super(message); - } - - /** - * @param message The detail message for this exception. - * @param cause The cause of this exception. - */ - public MetadataDecoderException(String message, Throwable cause) { - super(message, cause); - } + void onMetadata(Metadata metadata); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataRenderer.java index eaaba97d0050..329f9ffa7d73 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -15,65 +15,58 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Handler; import android.os.Handler.Callback; import android.os.Looper; import android.os.Message; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.BaseRenderer; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; -import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A renderer for metadata. */ public final class MetadataRenderer extends BaseRenderer implements Callback { - /** - * Receives output from a {@link MetadataRenderer}. - */ - public interface Output { - - /** - * Called each time there is a metadata associated with current playback time. - * - * @param metadata The metadata. - */ - void onMetadata(Metadata metadata); - - } - private static final int MSG_INVOKE_RENDERER = 0; // TODO: Holding multiple pending metadata objects is temporary mitigation against - // https://github.com/google/ExoPlayer/issues/1874 - // It should be removed once this issue has been addressed. + // https://github.com/google/ExoPlayer/issues/1874. It should be removed once this issue has been + // addressed. private static final int MAX_PENDING_METADATA_COUNT = 5; private final MetadataDecoderFactory decoderFactory; - private final Output output; - private final Handler outputHandler; - private final FormatHolder formatHolder; + private final MetadataOutput output; + @Nullable private final Handler outputHandler; private final MetadataInputBuffer buffer; - private final Metadata[] pendingMetadata; + private final @NullableType Metadata[] pendingMetadata; private final long[] pendingMetadataTimestamps; private int pendingMetadataIndex; private int pendingMetadataCount; - private MetadataDecoder decoder; + @Nullable private MetadataDecoder decoder; private boolean inputStreamEnded; + private long subsampleOffsetUs; /** * @param output The output. * @param outputLooper The looper associated with the thread on which the output should be called. * If the output makes use of standard Android UI components, then this should normally be the - * looper associated with the application's main thread, which can be obtained using - * {@link android.app.Activity#getMainLooper()}. Null may be passed if the output should be - * called directly on the player's internal rendering thread. + * looper associated with the application's main thread, which can be obtained using {@link + * android.app.Activity#getMainLooper()}. Null may be passed if the output should be called + * directly on the player's internal rendering thread. */ - public MetadataRenderer(Output output, Looper outputLooper) { + public MetadataRenderer(MetadataOutput output, @Nullable Looper outputLooper) { this(output, outputLooper, MetadataDecoderFactory.DEFAULT); } @@ -81,30 +74,36 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { * @param output The output. * @param outputLooper The looper associated with the thread on which the output should be called. * If the output makes use of standard Android UI components, then this should normally be the - * looper associated with the application's main thread, which can be obtained using - * {@link android.app.Activity#getMainLooper()}. Null may be passed if the output should be - * called directly on the player's internal rendering thread. + * looper associated with the application's main thread, which can be obtained using {@link + * android.app.Activity#getMainLooper()}. Null may be passed if the output should be called + * directly on the player's internal rendering thread. * @param decoderFactory A factory from which to obtain {@link MetadataDecoder} instances. */ - public MetadataRenderer(Output output, Looper outputLooper, - MetadataDecoderFactory decoderFactory) { + public MetadataRenderer( + MetadataOutput output, @Nullable Looper outputLooper, MetadataDecoderFactory decoderFactory) { super(C.TRACK_TYPE_METADATA); this.output = Assertions.checkNotNull(output); - this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this); + this.outputHandler = + outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this); this.decoderFactory = Assertions.checkNotNull(decoderFactory); - formatHolder = new FormatHolder(); buffer = new MetadataInputBuffer(); pendingMetadata = new Metadata[MAX_PENDING_METADATA_COUNT]; pendingMetadataTimestamps = new long[MAX_PENDING_METADATA_COUNT]; } @Override + @Capabilities public int supportsFormat(Format format) { - return decoderFactory.supportsFormat(format) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE; + if (decoderFactory.supportsFormat(format)) { + return RendererCapabilities.create( + supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM); + } else { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); + } } @Override - protected void onStreamChanged(Format[] formats) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) { decoder = decoderFactory.createDecoder(formats[0]); } @@ -115,9 +114,10 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + public void render(long positionUs, long elapsedRealtimeUs) { if (!inputStreamEnded && pendingMetadataCount < MAX_PENDING_METADATA_COUNT) { buffer.clear(); + FormatHolder formatHolder = getFormatHolder(); int result = readSource(formatHolder, buffer, false); if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { @@ -127,33 +127,70 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { // If we ever need to support a metadata format where this is not the case, we'll need to // pass the buffer to the decoder and discard the output. } else { - buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; + buffer.subsampleOffsetUs = subsampleOffsetUs; buffer.flip(); - try { - int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT; - pendingMetadata[index] = decoder.decode(buffer); - pendingMetadataTimestamps[index] = buffer.timeUs; - pendingMetadataCount++; - } catch (MetadataDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + @Nullable Metadata metadata = castNonNull(decoder).decode(buffer); + if (metadata != null) { + List entries = new ArrayList<>(metadata.length()); + decodeWrappedMetadata(metadata, entries); + if (!entries.isEmpty()) { + Metadata expandedMetadata = new Metadata(entries); + int index = + (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT; + pendingMetadata[index] = expandedMetadata; + pendingMetadataTimestamps[index] = buffer.timeUs; + pendingMetadataCount++; + } } } + } else if (result == C.RESULT_FORMAT_READ) { + subsampleOffsetUs = Assertions.checkNotNull(formatHolder.format).subsampleOffsetUs; } } if (pendingMetadataCount > 0 && pendingMetadataTimestamps[pendingMetadataIndex] <= positionUs) { - invokeRenderer(pendingMetadata[pendingMetadataIndex]); + Metadata metadata = castNonNull(pendingMetadata[pendingMetadataIndex]); + invokeRenderer(metadata); pendingMetadata[pendingMetadataIndex] = null; pendingMetadataIndex = (pendingMetadataIndex + 1) % MAX_PENDING_METADATA_COUNT; pendingMetadataCount--; } } + /** + * Iterates through {@code metadata.entries} and checks each one to see if contains wrapped + * metadata. If it does, then we recursively decode the wrapped metadata. If it doesn't (recursion + * base-case), we add the {@link Metadata.Entry} to {@code decodedEntries} (output parameter). + */ + private void decodeWrappedMetadata(Metadata metadata, List decodedEntries) { + for (int i = 0; i < metadata.length(); i++) { + @Nullable Format wrappedMetadataFormat = metadata.get(i).getWrappedMetadataFormat(); + if (wrappedMetadataFormat != null && decoderFactory.supportsFormat(wrappedMetadataFormat)) { + MetadataDecoder wrappedMetadataDecoder = + decoderFactory.createDecoder(wrappedMetadataFormat); + // wrappedMetadataFormat != null so wrappedMetadataBytes must be non-null too. + byte[] wrappedMetadataBytes = + Assertions.checkNotNull(metadata.get(i).getWrappedMetadataBytes()); + buffer.clear(); + buffer.ensureSpaceForWrite(wrappedMetadataBytes.length); + castNonNull(buffer.data).put(wrappedMetadataBytes); + buffer.flip(); + @Nullable Metadata innerMetadata = wrappedMetadataDecoder.decode(buffer); + if (innerMetadata != null) { + // The decoding succeeded, so we'll try another level of unwrapping. + decodeWrappedMetadata(innerMetadata, decodedEntries); + } + } else { + // Entry doesn't contain any wrapped metadata, so output it directly. + decodedEntries.add(metadata.get(i)); + } + } + } + @Override protected void onDisabled() { flushPendingMetadata(); decoder = null; - super.onDisabled(); } @Override @@ -180,7 +217,6 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { pendingMetadataCount = 0; } - @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { switch (msg.what) { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessage.java index 0b274ae0017d..01aac27a2700 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessage.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessage.java @@ -15,20 +15,48 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.util.Arrays; -/** - * An Event Message (emsg) as defined in ISO 23009-1. - */ +/** An Event Message (emsg) as defined in ISO 23009-1. */ public final class EventMessage implements Metadata.Entry { /** - * The message scheme. + * emsg scheme_id_uri from the CMAF + * spec. */ + @VisibleForTesting public static final String ID3_SCHEME_ID_AOM = "https://aomedia.org/emsg/ID3"; + + /** + * The Apple-hosted scheme_id equivalent to {@code ID3_SCHEME_ID_AOM} - used before AOM adoption. + */ + private static final String ID3_SCHEME_ID_APPLE = + "https://developer.apple.com/streaming/emsg-id3"; + + /** + * scheme_id_uri from section 7.3.2 of SCTE 214-3 + * 2015. + */ + @VisibleForTesting public static final String SCTE35_SCHEME_ID = "urn:scte:scte35:2014:bin"; + + private static final Format ID3_FORMAT = + Format.createSampleFormat( + /* id= */ null, MimeTypes.APPLICATION_ID3, Format.OFFSET_SAMPLE_RELATIVE); + private static final Format SCTE35_FORMAT = + Format.createSampleFormat( + /* id= */ null, MimeTypes.APPLICATION_SCTE35, Format.OFFSET_SAMPLE_RELATIVE); + + /** The message scheme. */ public final String schemeIdUri; /** @@ -55,15 +83,14 @@ public final class EventMessage implements Metadata.Entry { private int hashCode; /** - * * @param schemeIdUri The message scheme. * @param value The value for the event. * @param durationMs The duration of the event in milliseconds. * @param id The instance identifier. * @param messageData The body of the message. */ - public EventMessage(String schemeIdUri, String value, long durationMs, long id, - byte[] messageData) { + public EventMessage( + String schemeIdUri, String value, long durationMs, long id, byte[] messageData) { this.schemeIdUri = schemeIdUri; this.value = value; this.durationMs = durationMs; @@ -72,11 +99,31 @@ public final class EventMessage implements Metadata.Entry { } /* package */ EventMessage(Parcel in) { - schemeIdUri = in.readString(); - value = in.readString(); + schemeIdUri = castNonNull(in.readString()); + value = castNonNull(in.readString()); durationMs = in.readLong(); id = in.readLong(); - messageData = in.createByteArray(); + messageData = castNonNull(in.createByteArray()); + } + + @Override + @Nullable + public Format getWrappedMetadataFormat() { + switch (schemeIdUri) { + case ID3_SCHEME_ID_AOM: + case ID3_SCHEME_ID_APPLE: + return ID3_FORMAT; + case SCTE35_SCHEME_ID: + return SCTE35_FORMAT; + default: + return null; + } + } + + @Override + @Nullable + public byte[] getWrappedMetadataBytes() { + return getWrappedMetadataFormat() != null ? messageData : null; } @Override @@ -94,7 +141,7 @@ public final class EventMessage implements Metadata.Entry { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -102,11 +149,25 @@ public final class EventMessage implements Metadata.Entry { return false; } EventMessage other = (EventMessage) obj; - return durationMs == other.durationMs && id == other.id - && Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value) + return durationMs == other.durationMs + && id == other.id + && Util.areEqual(schemeIdUri, other.schemeIdUri) + && Util.areEqual(value, other.value) && Arrays.equals(messageData, other.messageData); } + @Override + public String toString() { + return "EMSG: scheme=" + + schemeIdUri + + ", id=" + + id + + ", durationMs=" + + durationMs + + ", value=" + + value; + } + // Parcelable implementation. @Override diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index 9fa6dcb2f351..09b0a69395cc 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -18,32 +18,30 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataDecoder; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import java.nio.ByteBuffer; import java.util.Arrays; -/** - * Decodes Event Message (emsg) atoms, as defined in ISO 23009-1. - *

- * Atom data should be provided to the decoder without the full atom header (i.e. starting from the - * first byte of the scheme_id_uri field). - */ +/** Decodes data encoded by {@link EventMessageEncoder}. */ public final class EventMessageDecoder implements MetadataDecoder { + @SuppressWarnings("ByteBufferBackingArray") @Override public Metadata decode(MetadataInputBuffer inputBuffer) { - ByteBuffer buffer = inputBuffer.data; + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); byte[] data = buffer.array(); int size = buffer.limit(); - ParsableByteArray emsgData = new ParsableByteArray(data, size); - String schemeIdUri = emsgData.readNullTerminatedString(); - String value = emsgData.readNullTerminatedString(); - long timescale = emsgData.readUnsignedInt(); - emsgData.skipBytes(4); // presentation_time_delta - long durationMs = (emsgData.readUnsignedInt() * 1000) / timescale; - long id = emsgData.readUnsignedInt(); - byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size); - return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData)); + return new Metadata(decode(new ParsableByteArray(data, size))); } + public EventMessage decode(ParsableByteArray emsgData) { + String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString()); + String value = Assertions.checkNotNull(emsgData.readNullTerminatedString()); + long durationMs = emsgData.readUnsignedInt(); + long id = emsgData.readUnsignedInt(); + byte[] messageData = + Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit()); + return new EventMessage(schemeIdUri, value, durationMs, id, messageData); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java new file mode 100644 index 000000000000..261e39ae7061 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * Encodes data that can be decoded by {@link EventMessageDecoder}. This class isn't thread safe. + */ +public final class EventMessageEncoder { + + private final ByteArrayOutputStream byteArrayOutputStream; + private final DataOutputStream dataOutputStream; + + public EventMessageEncoder() { + byteArrayOutputStream = new ByteArrayOutputStream(512); + dataOutputStream = new DataOutputStream(byteArrayOutputStream); + } + + /** + * Encodes an {@link EventMessage} to a byte array that can be decoded by {@link + * EventMessageDecoder}. + * + * @param eventMessage The event message to be encoded. + * @return The serialized byte array. + */ + public byte[] encode(EventMessage eventMessage) { + byteArrayOutputStream.reset(); + try { + writeNullTerminatedString(dataOutputStream, eventMessage.schemeIdUri); + String nonNullValue = eventMessage.value != null ? eventMessage.value : ""; + writeNullTerminatedString(dataOutputStream, nonNullValue); + writeUnsignedInt(dataOutputStream, eventMessage.durationMs); + writeUnsignedInt(dataOutputStream, eventMessage.id); + dataOutputStream.write(eventMessage.messageData); + dataOutputStream.flush(); + return byteArrayOutputStream.toByteArray(); + } catch (IOException e) { + // Should never happen. + throw new RuntimeException(e); + } + } + + private static void writeNullTerminatedString(DataOutputStream dataOutputStream, String value) + throws IOException { + dataOutputStream.writeBytes(value); + dataOutputStream.writeByte(0); + } + + private static void writeUnsignedInt(DataOutputStream outputStream, long value) + throws IOException { + outputStream.writeByte((int) (value >>> 24) & 0xFF); + outputStream.writeByte((int) (value >>> 16) & 0xFF); + outputStream.writeByte((int) (value >>> 8) & 0xFF); + outputStream.writeByte((int) value & 0xFF); + } + +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/package-info.java new file mode 100644 index 000000000000..3e54b59a8cfd --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/PictureFrame.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/PictureFrame.java new file mode 100644 index 000000000000..8a7ffbd976f9 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/PictureFrame.java @@ -0,0 +1,144 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.flac; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import java.util.Arrays; + +/** A picture parsed from a FLAC file. */ +public final class PictureFrame implements Metadata.Entry { + + /** The type of the picture. */ + public final int pictureType; + /** The mime type of the picture. */ + public final String mimeType; + /** A description of the picture. */ + public final String description; + /** The width of the picture in pixels. */ + public final int width; + /** The height of the picture in pixels. */ + public final int height; + /** The color depth of the picture in bits-per-pixel. */ + public final int depth; + /** For indexed-color pictures (e.g. GIF), the number of colors used. 0 otherwise. */ + public final int colors; + /** The encoded picture data. */ + public final byte[] pictureData; + + public PictureFrame( + int pictureType, + String mimeType, + String description, + int width, + int height, + int depth, + int colors, + byte[] pictureData) { + this.pictureType = pictureType; + this.mimeType = mimeType; + this.description = description; + this.width = width; + this.height = height; + this.depth = depth; + this.colors = colors; + this.pictureData = pictureData; + } + + /* package */ PictureFrame(Parcel in) { + this.pictureType = in.readInt(); + this.mimeType = castNonNull(in.readString()); + this.description = castNonNull(in.readString()); + this.width = in.readInt(); + this.height = in.readInt(); + this.depth = in.readInt(); + this.colors = in.readInt(); + this.pictureData = castNonNull(in.createByteArray()); + } + + @Override + public String toString() { + return "Picture: mimeType=" + mimeType + ", description=" + description; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PictureFrame other = (PictureFrame) obj; + return (pictureType == other.pictureType) + && mimeType.equals(other.mimeType) + && description.equals(other.description) + && (width == other.width) + && (height == other.height) + && (depth == other.depth) + && (colors == other.colors) + && Arrays.equals(pictureData, other.pictureData); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + pictureType; + result = 31 * result + mimeType.hashCode(); + result = 31 * result + description.hashCode(); + result = 31 * result + width; + result = 31 * result + height; + result = 31 * result + depth; + result = 31 * result + colors; + result = 31 * result + Arrays.hashCode(pictureData); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(pictureType); + dest.writeString(mimeType); + dest.writeString(description); + dest.writeInt(width); + dest.writeInt(height); + dest.writeInt(depth); + dest.writeInt(colors); + dest.writeByteArray(pictureData); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public PictureFrame createFromParcel(Parcel in) { + return new PictureFrame(in); + } + + @Override + public PictureFrame[] newArray(int size) { + return new PictureFrame[size]; + } + }; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/VorbisComment.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/VorbisComment.java new file mode 100644 index 000000000000..b777582b5df3 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/VorbisComment.java @@ -0,0 +1,99 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.flac; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; + +/** A vorbis comment. */ +public final class VorbisComment implements Metadata.Entry { + + /** The key. */ + public final String key; + + /** The value. */ + public final String value; + + /** + * @param key The key. + * @param value The value. + */ + public VorbisComment(String key, String value) { + this.key = key; + this.value = value; + } + + /* package */ VorbisComment(Parcel in) { + this.key = castNonNull(in.readString()); + this.value = castNonNull(in.readString()); + } + + @Override + public String toString() { + return "VC: " + key + "=" + value; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + VorbisComment other = (VorbisComment) obj; + return key.equals(other.key) && value.equals(other.value); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + key.hashCode(); + result = 31 * result + value.hashCode(); + return result; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(key); + dest.writeString(value); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public VorbisComment createFromParcel(Parcel in) { + return new VorbisComment(in); + } + + @Override + public VorbisComment[] newArray(int size) { + return new VorbisComment[size]; + } + }; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/package-info.java new file mode 100644 index 000000000000..02353ec303b8 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.flac; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java new file mode 100644 index 000000000000..1d44219eda17 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.icy; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataDecoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Decodes ICY stream information. */ +public final class IcyDecoder implements MetadataDecoder { + + private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.*?)';", Pattern.DOTALL); + private static final String STREAM_KEY_NAME = "streamtitle"; + private static final String STREAM_KEY_URL = "streamurl"; + + private final CharsetDecoder utf8Decoder; + private final CharsetDecoder iso88591Decoder; + + public IcyDecoder() { + utf8Decoder = Charset.forName(C.UTF8_NAME).newDecoder(); + iso88591Decoder = Charset.forName(C.ISO88591_NAME).newDecoder(); + } + + @Override + @SuppressWarnings("ByteBufferBackingArray") + public Metadata decode(MetadataInputBuffer inputBuffer) { + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); + @Nullable String icyString = decodeToString(buffer); + byte[] icyBytes = new byte[buffer.limit()]; + buffer.get(icyBytes); + + if (icyString == null) { + return new Metadata(new IcyInfo(icyBytes, /* title= */ null, /* url= */ null)); + } + + @Nullable String name = null; + @Nullable String url = null; + int index = 0; + Matcher matcher = METADATA_ELEMENT.matcher(icyString); + while (matcher.find(index)) { + @Nullable String key = Util.toLowerInvariant(matcher.group(1)); + @Nullable String value = matcher.group(2); + switch (key) { + case STREAM_KEY_NAME: + name = value; + break; + case STREAM_KEY_URL: + url = value; + break; + } + index = matcher.end(); + } + return new Metadata(new IcyInfo(icyBytes, name, url)); + } + + // The ICY spec doesn't specify a character encoding, and there's no way to communicate one + // either. So try decoding UTF-8 first, then fall back to ISO-8859-1. + // https://github.com/google/ExoPlayer/issues/6753 + @Nullable + private String decodeToString(ByteBuffer data) { + try { + return utf8Decoder.decode(data).toString(); + } catch (CharacterCodingException e) { + // Fall through to try ISO-8859-1 decoding. + } finally { + utf8Decoder.reset(); + data.rewind(); + } + try { + return iso88591Decoder.decode(data).toString(); + } catch (CharacterCodingException e) { + return null; + } finally { + iso88591Decoder.reset(); + data.rewind(); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java new file mode 100644 index 000000000000..638e7594eb4e --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.icy; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.List; +import java.util.Map; + +/** ICY headers. */ +public final class IcyHeaders implements Metadata.Entry { + + public static final String REQUEST_HEADER_ENABLE_METADATA_NAME = "Icy-MetaData"; + public static final String REQUEST_HEADER_ENABLE_METADATA_VALUE = "1"; + + private static final String TAG = "IcyHeaders"; + + private static final String RESPONSE_HEADER_BITRATE = "icy-br"; + private static final String RESPONSE_HEADER_GENRE = "icy-genre"; + private static final String RESPONSE_HEADER_NAME = "icy-name"; + private static final String RESPONSE_HEADER_URL = "icy-url"; + private static final String RESPONSE_HEADER_PUB = "icy-pub"; + private static final String RESPONSE_HEADER_METADATA_INTERVAL = "icy-metaint"; + + /** + * Parses {@link IcyHeaders} from response headers. + * + * @param responseHeaders The response headers. + * @return The parsed {@link IcyHeaders}, or {@code null} if no ICY headers were present. + */ + @Nullable + public static IcyHeaders parse(Map> responseHeaders) { + boolean icyHeadersPresent = false; + int bitrate = Format.NO_VALUE; + String genre = null; + String name = null; + String url = null; + boolean isPublic = false; + int metadataInterval = C.LENGTH_UNSET; + + List headers = responseHeaders.get(RESPONSE_HEADER_BITRATE); + if (headers != null) { + String bitrateHeader = headers.get(0); + try { + bitrate = Integer.parseInt(bitrateHeader) * 1000; + if (bitrate > 0) { + icyHeadersPresent = true; + } else { + Log.w(TAG, "Invalid bitrate: " + bitrateHeader); + bitrate = Format.NO_VALUE; + } + } catch (NumberFormatException e) { + Log.w(TAG, "Invalid bitrate header: " + bitrateHeader); + } + } + headers = responseHeaders.get(RESPONSE_HEADER_GENRE); + if (headers != null) { + genre = headers.get(0); + icyHeadersPresent = true; + } + headers = responseHeaders.get(RESPONSE_HEADER_NAME); + if (headers != null) { + name = headers.get(0); + icyHeadersPresent = true; + } + headers = responseHeaders.get(RESPONSE_HEADER_URL); + if (headers != null) { + url = headers.get(0); + icyHeadersPresent = true; + } + headers = responseHeaders.get(RESPONSE_HEADER_PUB); + if (headers != null) { + isPublic = headers.get(0).equals("1"); + icyHeadersPresent = true; + } + headers = responseHeaders.get(RESPONSE_HEADER_METADATA_INTERVAL); + if (headers != null) { + String metadataIntervalHeader = headers.get(0); + try { + metadataInterval = Integer.parseInt(metadataIntervalHeader); + if (metadataInterval > 0) { + icyHeadersPresent = true; + } else { + Log.w(TAG, "Invalid metadata interval: " + metadataIntervalHeader); + metadataInterval = C.LENGTH_UNSET; + } + } catch (NumberFormatException e) { + Log.w(TAG, "Invalid metadata interval: " + metadataIntervalHeader); + } + } + return icyHeadersPresent + ? new IcyHeaders(bitrate, genre, name, url, isPublic, metadataInterval) + : null; + } + + /** + * Bitrate in bits per second ({@code (icy-br * 1000)}), or {@link Format#NO_VALUE} if the header + * was not present. + */ + public final int bitrate; + /** The genre ({@code icy-genre}). */ + @Nullable public final String genre; + /** The stream name ({@code icy-name}). */ + @Nullable public final String name; + /** The URL of the radio station ({@code icy-url}). */ + @Nullable public final String url; + /** + * Whether the radio station is listed ({@code icy-pub}), or {@code false} if the header was not + * present. + */ + public final boolean isPublic; + + /** + * The interval in bytes between metadata chunks ({@code icy-metaint}), or {@link C#LENGTH_UNSET} + * if the header was not present. + */ + public final int metadataInterval; + + /** + * @param bitrate See {@link #bitrate}. + * @param genre See {@link #genre}. + * @param name See {@link #name See}. + * @param url See {@link #url}. + * @param isPublic See {@link #isPublic}. + * @param metadataInterval See {@link #metadataInterval}. + */ + public IcyHeaders( + int bitrate, + @Nullable String genre, + @Nullable String name, + @Nullable String url, + boolean isPublic, + int metadataInterval) { + Assertions.checkArgument(metadataInterval == C.LENGTH_UNSET || metadataInterval > 0); + this.bitrate = bitrate; + this.genre = genre; + this.name = name; + this.url = url; + this.isPublic = isPublic; + this.metadataInterval = metadataInterval; + } + + /* package */ IcyHeaders(Parcel in) { + bitrate = in.readInt(); + genre = in.readString(); + name = in.readString(); + url = in.readString(); + isPublic = Util.readBoolean(in); + metadataInterval = in.readInt(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IcyHeaders other = (IcyHeaders) obj; + return bitrate == other.bitrate + && Util.areEqual(genre, other.genre) + && Util.areEqual(name, other.name) + && Util.areEqual(url, other.url) + && isPublic == other.isPublic + && metadataInterval == other.metadataInterval; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + bitrate; + result = 31 * result + (genre != null ? genre.hashCode() : 0); + result = 31 * result + (name != null ? name.hashCode() : 0); + result = 31 * result + (url != null ? url.hashCode() : 0); + result = 31 * result + (isPublic ? 1 : 0); + result = 31 * result + metadataInterval; + return result; + } + + @Override + public String toString() { + return "IcyHeaders: name=\"" + + name + + "\", genre=\"" + + genre + + "\", bitrate=" + + bitrate + + ", metadataInterval=" + + metadataInterval; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(bitrate); + dest.writeString(genre); + dest.writeString(name); + dest.writeString(url); + Util.writeBoolean(dest, isPublic); + dest.writeInt(metadataInterval); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public IcyHeaders createFromParcel(Parcel in) { + return new IcyHeaders(in); + } + + @Override + public IcyHeaders[] newArray(int size) { + return new IcyHeaders[size]; + } + }; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyInfo.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyInfo.java new file mode 100644 index 000000000000..4104e41c644a --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyInfo.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.icy; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.util.Arrays; + +/** ICY in-stream information. */ +public final class IcyInfo implements Metadata.Entry { + + /** The complete metadata bytes used to construct this IcyInfo. */ + public final byte[] rawMetadata; + /** The stream title if present and decodable, or {@code null}. */ + @Nullable public final String title; + /** The stream URL if present and decodable, or {@code null}. */ + @Nullable public final String url; + + /** + * Construct a new IcyInfo from the source metadata, and optionally a StreamTitle and StreamUrl + * that have been extracted. + * + * @param rawMetadata See {@link #rawMetadata}. + * @param title See {@link #title}. + * @param url See {@link #url}. + */ + public IcyInfo(byte[] rawMetadata, @Nullable String title, @Nullable String url) { + this.rawMetadata = rawMetadata; + this.title = title; + this.url = url; + } + + /* package */ IcyInfo(Parcel in) { + rawMetadata = Assertions.checkNotNull(in.createByteArray()); + title = in.readString(); + url = in.readString(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IcyInfo other = (IcyInfo) obj; + // title & url are derived from rawMetadata, so no need to include them in the comparison. + return Arrays.equals(rawMetadata, other.rawMetadata); + } + + @Override + public int hashCode() { + // title & url are derived from rawMetadata, so no need to include them in the hash. + return Arrays.hashCode(rawMetadata); + } + + @Override + public String toString() { + return String.format( + "ICY: title=\"%s\", url=\"%s\", rawMetadata.length=\"%s\"", title, url, rawMetadata.length); + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeByteArray(rawMetadata); + dest.writeString(title); + dest.writeString(url); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public IcyInfo createFromParcel(Parcel in) { + return new IcyInfo(in); + } + + @Override + public IcyInfo[] newArray(int size) { + return new IcyInfo[size]; + } + }; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/package-info.java new file mode 100644 index 000000000000..a8a45e2ef152 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.icy; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.java index 29a8d288b264..f151707e4b64 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.java @@ -15,8 +15,11 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -28,11 +31,12 @@ public final class ApicFrame extends Id3Frame { public static final String ID = "APIC"; public final String mimeType; - public final String description; + @Nullable public final String description; public final int pictureType; public final byte[] pictureData; - public ApicFrame(String mimeType, String description, int pictureType, byte[] pictureData) { + public ApicFrame( + String mimeType, @Nullable String description, int pictureType, byte[] pictureData) { super(ID); this.mimeType = mimeType; this.description = description; @@ -42,14 +46,14 @@ public final class ApicFrame extends Id3Frame { /* package */ ApicFrame(Parcel in) { super(ID); - mimeType = in.readString(); + mimeType = castNonNull(in.readString()); description = in.readString(); pictureType = in.readInt(); - pictureData = in.createByteArray(); + pictureData = castNonNull(in.createByteArray()); } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -72,6 +76,13 @@ public final class ApicFrame extends Id3Frame { return result; } + @Override + public String toString() { + return id + ": mimeType=" + mimeType + ", description=" + description; + } + + // Parcelable implementation. + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mimeType); diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java index 69ee6132eccd..adc66ccdfe4c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java @@ -15,8 +15,11 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.Nullable; import java.util.Arrays; /** @@ -32,12 +35,12 @@ public final class BinaryFrame extends Id3Frame { } /* package */ BinaryFrame(Parcel in) { - super(in.readString()); - data = in.createByteArray(); + super(castNonNull(in.readString())); + data = castNonNull(in.createByteArray()); } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java index c891e4c2a19a..348781dddfe1 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java @@ -15,7 +15,10 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -53,7 +56,7 @@ public final class ChapterFrame extends Id3Frame { /* package */ ChapterFrame(Parcel in) { super(ID); - this.chapterId = in.readString(); + this.chapterId = castNonNull(in.readString()); this.startTimeMs = in.readInt(); this.endTimeMs = in.readInt(); this.startOffset = in.readLong(); @@ -80,7 +83,7 @@ public final class ChapterFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java index 9ea1f16bb050..9451151c1628 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java @@ -15,7 +15,10 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -42,12 +45,13 @@ public final class ChapterTocFrame extends Id3Frame { this.subFrames = subFrames; } - /* package */ ChapterTocFrame(Parcel in) { + /* package */ + ChapterTocFrame(Parcel in) { super(ID); - this.elementId = in.readString(); + this.elementId = castNonNull(in.readString()); this.isRoot = in.readByte() != 0; this.isOrdered = in.readByte() != 0; - this.children = in.createStringArray(); + this.children = castNonNull(in.createStringArray()); int subFrameCount = in.readInt(); subFrames = new Id3Frame[subFrameCount]; for (int i = 0; i < subFrameCount; i++) { @@ -70,7 +74,7 @@ public final class ChapterTocFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -101,8 +105,8 @@ public final class ChapterTocFrame extends Id3Frame { dest.writeByte((byte) (isOrdered ? 1 : 0)); dest.writeStringArray(children); dest.writeInt(subFrames.length); - for (int i = 0; i < subFrames.length; i++) { - dest.writeParcelable(subFrames[i], 0); + for (Id3Frame subFrame : subFrames) { + dest.writeParcelable(subFrame, 0); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.java index c1de4283977e..98b8c79a9647 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.java @@ -15,8 +15,11 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; /** @@ -39,13 +42,13 @@ public final class CommentFrame extends Id3Frame { /* package */ CommentFrame(Parcel in) { super(ID); - language = in.readString(); - description = in.readString(); - text = in.readString(); + language = castNonNull(in.readString()); + description = castNonNull(in.readString()); + text = castNonNull(in.readString()); } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -66,6 +69,13 @@ public final class CommentFrame extends Id3Frame { return result; } + @Override + public String toString() { + return id + ": language=" + language + ", description=" + description; + } + + // Parcelable implementation. + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(id); diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.java index e5fdc9114323..58a208a76a83 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.java @@ -15,8 +15,11 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -42,14 +45,14 @@ public final class GeobFrame extends Id3Frame { /* package */ GeobFrame(Parcel in) { super(ID); - mimeType = in.readString(); - filename = in.readString(); - description = in.readString(); - data = in.createByteArray(); + mimeType = castNonNull(in.readString()); + filename = castNonNull(in.readString()); + description = castNonNull(in.readString()); + data = castNonNull(in.createByteArray()); } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -71,6 +74,19 @@ public final class GeobFrame extends Id3Frame { return result; } + @Override + public String toString() { + return id + + ": mimeType=" + + mimeType + + ", filename=" + + filename + + ", description=" + + description; + } + + // Parcelable implementation. + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mimeType); diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index f36cf113ae25..36e004ed52c2 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -15,11 +15,14 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; -import android.util.Log; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataDecoder; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.UnsupportedEncodingException; @@ -53,12 +56,14 @@ public final class Id3Decoder implements MetadataDecoder { } + /** A predicate that indicates no frames should be decoded. */ + public static final FramePredicate NO_FRAMES_PREDICATE = + (majorVersion, id0, id1, id2, id3) -> false; + private static final String TAG = "Id3Decoder"; - /** - * The first three bytes of a well formed ID3 tag header. - */ - public static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + /** The first three bytes of a well formed ID3 tag header. */ + public static final int ID3_TAG = 0x00494433; /** * Length of an ID3 tag header. */ @@ -78,7 +83,7 @@ public final class Id3Decoder implements MetadataDecoder { private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; private static final int ID3_TEXT_ENCODING_UTF_8 = 3; - private final FramePredicate framePredicate; + @Nullable private final FramePredicate framePredicate; public Id3Decoder() { this(null); @@ -87,13 +92,15 @@ public final class Id3Decoder implements MetadataDecoder { /** * @param framePredicate Determines which frames are decoded. May be null to decode all frames. */ - public Id3Decoder(FramePredicate framePredicate) { + public Id3Decoder(@Nullable FramePredicate framePredicate) { this.framePredicate = framePredicate; } + @SuppressWarnings("ByteBufferBackingArray") @Override + @Nullable public Metadata decode(MetadataInputBuffer inputBuffer) { - ByteBuffer buffer = inputBuffer.data; + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); return decode(buffer.array(), buffer.limit()); } @@ -102,8 +109,10 @@ public final class Id3Decoder implements MetadataDecoder { * * @param data The bytes to decode ID3 tags from. * @param size Amount of bytes in {@code data} to read. - * @return A {@link Metadata} object containing the decoded ID3 tags. + * @return A {@link Metadata} object containing the decoded ID3 tags, or null if the data could + * not be decoded. */ + @Nullable public Metadata decode(byte[] data, int size) { List id3Frames = new ArrayList<>(); ParsableByteArray id3Data = new ParsableByteArray(data, size); @@ -146,6 +155,7 @@ public final class Id3Decoder implements MetadataDecoder { * @param data A {@link ParsableByteArray} from which the header should be read. * @return The parsed header, or null if the ID3 tag is unsupported. */ + @Nullable private static Id3Header decodeHeader(ParsableByteArray data) { if (data.bytesLeft() < ID3_HEADER_LENGTH) { Log.w(TAG, "Data too short to be an ID3 tag"); @@ -154,7 +164,7 @@ public final class Id3Decoder implements MetadataDecoder { int id = data.readUnsignedInt24(); if (id != ID3_TAG) { - Log.w(TAG, "Unexpected first three bytes of ID3 tag header: " + id); + Log.w(TAG, "Unexpected first three bytes of ID3 tag header: 0x" + String.format("%06X", id)); return null; } @@ -260,8 +270,13 @@ public final class Id3Decoder implements MetadataDecoder { } } - private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data, - boolean unsignedIntFrameSizeHack, int frameHeaderSize, FramePredicate framePredicate) { + @Nullable + private static Id3Frame decodeFrame( + int majorVersion, + ParsableByteArray id3Data, + boolean unsignedIntFrameSizeHack, + int frameHeaderSize, + @Nullable FramePredicate framePredicate) { int frameId0 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte(); @@ -371,6 +386,8 @@ public final class Id3Decoder implements MetadataDecoder { } else if (frameId0 == 'C' && frameId1 == 'T' && frameId2 == 'O' && frameId3 == 'C') { frame = decodeChapterTOCFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack, frameHeaderSize, framePredicate); + } else if (frameId0 == 'M' && frameId1 == 'L' && frameId2 == 'L' && frameId3 == 'T') { + frame = decodeMlltFrame(id3Data, frameSize); } else { String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3); frame = decodeBinaryFrame(id3Data, frameSize, id); @@ -389,6 +406,7 @@ public final class Id3Decoder implements MetadataDecoder { } } + @Nullable private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { if (frameSize < 1) { @@ -405,20 +423,16 @@ public final class Id3Decoder implements MetadataDecoder { int descriptionEndIndex = indexOfEos(data, 0, encoding); String description = new String(data, 0, descriptionEndIndex, charset); - String value; int valueStartIndex = descriptionEndIndex + delimiterLength(encoding); - if (valueStartIndex < data.length) { - int valueEndIndex = indexOfEos(data, valueStartIndex, encoding); - value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset); - } else { - value = ""; - } + int valueEndIndex = indexOfEos(data, valueStartIndex, encoding); + String value = decodeStringIfValid(data, valueStartIndex, valueEndIndex, charset); return new TextInformationFrame("TXXX", description, value); } - private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data, - int frameSize, String id) throws UnsupportedEncodingException { + @Nullable + private static TextInformationFrame decodeTextInformationFrame( + ParsableByteArray id3Data, int frameSize, String id) throws UnsupportedEncodingException { if (frameSize < 1) { // Frame is malformed. return null; @@ -436,6 +450,7 @@ public final class Id3Decoder implements MetadataDecoder { return new TextInformationFrame(id, null, value); } + @Nullable private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { if (frameSize < 1) { @@ -452,14 +467,9 @@ public final class Id3Decoder implements MetadataDecoder { int descriptionEndIndex = indexOfEos(data, 0, encoding); String description = new String(data, 0, descriptionEndIndex, charset); - String url; int urlStartIndex = descriptionEndIndex + delimiterLength(encoding); - if (urlStartIndex < data.length) { - int urlEndIndex = indexOfZeroByte(data, urlStartIndex); - url = new String(data, urlStartIndex, urlEndIndex - urlStartIndex, "ISO-8859-1"); - } else { - url = ""; - } + int urlEndIndex = indexOfZeroByte(data, urlStartIndex); + String url = decodeStringIfValid(data, urlStartIndex, urlEndIndex, "ISO-8859-1"); return new UrlLinkFrame("WXXX", description, url); } @@ -483,13 +493,8 @@ public final class Id3Decoder implements MetadataDecoder { int ownerEndIndex = indexOfZeroByte(data, 0); String owner = new String(data, 0, ownerEndIndex, "ISO-8859-1"); - byte[] privateData; int privateDataStartIndex = ownerEndIndex + 1; - if (privateDataStartIndex < data.length) { - privateData = Arrays.copyOfRange(data, privateDataStartIndex, data.length); - } else { - privateData = new byte[0]; - } + byte[] privateData = copyOfRangeIfValid(data, privateDataStartIndex, data.length); return new PrivFrame(owner, privateData); } @@ -507,16 +512,15 @@ public final class Id3Decoder implements MetadataDecoder { int filenameStartIndex = mimeTypeEndIndex + 1; int filenameEndIndex = indexOfEos(data, filenameStartIndex, encoding); - String filename = new String(data, filenameStartIndex, filenameEndIndex - filenameStartIndex, - charset); + String filename = decodeStringIfValid(data, filenameStartIndex, filenameEndIndex, charset); int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding); int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding); - String description = new String(data, descriptionStartIndex, - descriptionEndIndex - descriptionStartIndex, charset); + String description = + decodeStringIfValid(data, descriptionStartIndex, descriptionEndIndex, charset); int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding); - byte[] objectData = Arrays.copyOfRange(data, objectDataStartIndex, data.length); + byte[] objectData = copyOfRangeIfValid(data, objectDataStartIndex, data.length); return new GeobFrame(mimeType, filename, description, objectData); } @@ -534,7 +538,7 @@ public final class Id3Decoder implements MetadataDecoder { if (majorVersion == 2) { mimeTypeEndIndex = 2; mimeType = "image/" + Util.toLowerInvariant(new String(data, 0, 3, "ISO-8859-1")); - if (mimeType.equals("image/jpg")) { + if ("image/jpg".equals(mimeType)) { mimeType = "image/jpeg"; } } else { @@ -553,11 +557,12 @@ public final class Id3Decoder implements MetadataDecoder { descriptionEndIndex - descriptionStartIndex, charset); int pictureDataStartIndex = descriptionEndIndex + delimiterLength(encoding); - byte[] pictureData = Arrays.copyOfRange(data, pictureDataStartIndex, data.length); + byte[] pictureData = copyOfRangeIfValid(data, pictureDataStartIndex, data.length); return new ApicFrame(mimeType, description, pictureType, pictureData); } + @Nullable private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { if (frameSize < 4) { @@ -578,21 +583,21 @@ public final class Id3Decoder implements MetadataDecoder { int descriptionEndIndex = indexOfEos(data, 0, encoding); String description = new String(data, 0, descriptionEndIndex, charset); - String text; int textStartIndex = descriptionEndIndex + delimiterLength(encoding); - if (textStartIndex < data.length) { - int textEndIndex = indexOfEos(data, textStartIndex, encoding); - text = new String(data, textStartIndex, textEndIndex - textStartIndex, charset); - } else { - text = ""; - } + int textEndIndex = indexOfEos(data, textStartIndex, encoding); + String text = decodeStringIfValid(data, textStartIndex, textEndIndex, charset); return new CommentFrame(language, description, text); } - private static ChapterFrame decodeChapterFrame(ParsableByteArray id3Data, int frameSize, - int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize, - FramePredicate framePredicate) throws UnsupportedEncodingException { + private static ChapterFrame decodeChapterFrame( + ParsableByteArray id3Data, + int frameSize, + int majorVersion, + boolean unsignedIntFrameSizeHack, + int frameHeaderSize, + @Nullable FramePredicate framePredicate) + throws UnsupportedEncodingException { int framePosition = id3Data.getPosition(); int chapterIdEndIndex = indexOfZeroByte(id3Data.data, framePosition); String chapterId = new String(id3Data.data, framePosition, chapterIdEndIndex - framePosition, @@ -625,9 +630,14 @@ public final class Id3Decoder implements MetadataDecoder { return new ChapterFrame(chapterId, startTime, endTime, startOffset, endOffset, subFrameArray); } - private static ChapterTocFrame decodeChapterTOCFrame(ParsableByteArray id3Data, int frameSize, - int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize, - FramePredicate framePredicate) throws UnsupportedEncodingException { + private static ChapterTocFrame decodeChapterTOCFrame( + ParsableByteArray id3Data, + int frameSize, + int majorVersion, + boolean unsignedIntFrameSizeHack, + int frameHeaderSize, + @Nullable FramePredicate framePredicate) + throws UnsupportedEncodingException { int framePosition = id3Data.getPosition(); int elementIdEndIndex = indexOfZeroByte(id3Data.data, framePosition); String elementId = new String(id3Data.data, framePosition, elementIdEndIndex - framePosition, @@ -662,6 +672,36 @@ public final class Id3Decoder implements MetadataDecoder { return new ChapterTocFrame(elementId, isRoot, isOrdered, children, subFrameArray); } + private static MlltFrame decodeMlltFrame(ParsableByteArray id3Data, int frameSize) { + // See ID3v2.4.0 native frames subsection 4.6. + int mpegFramesBetweenReference = id3Data.readUnsignedShort(); + int bytesBetweenReference = id3Data.readUnsignedInt24(); + int millisecondsBetweenReference = id3Data.readUnsignedInt24(); + int bitsForBytesDeviation = id3Data.readUnsignedByte(); + int bitsForMillisecondsDeviation = id3Data.readUnsignedByte(); + + ParsableBitArray references = new ParsableBitArray(); + references.reset(id3Data); + int referencesBits = 8 * (frameSize - 10); + int bitsPerReference = bitsForBytesDeviation + bitsForMillisecondsDeviation; + int referencesCount = referencesBits / bitsPerReference; + int[] bytesDeviations = new int[referencesCount]; + int[] millisecondsDeviations = new int[referencesCount]; + for (int i = 0; i < referencesCount; i++) { + int bytesDeviation = references.readBits(bitsForBytesDeviation); + int millisecondsDeviation = references.readBits(bitsForMillisecondsDeviation); + bytesDeviations[i] = bytesDeviation; + millisecondsDeviations[i] = millisecondsDeviation; + } + + return new MlltFrame( + mpegFramesBetweenReference, + bytesBetweenReference, + millisecondsBetweenReference, + bytesDeviations, + millisecondsDeviations); + } + private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize, String id) { byte[] frame = new byte[frameSize]; @@ -680,9 +720,11 @@ public final class Id3Decoder implements MetadataDecoder { */ private static int removeUnsynchronization(ParsableByteArray data, int length) { byte[] bytes = data.data; - for (int i = data.getPosition(); i + 1 < length; i++) { + int startPosition = data.getPosition(); + for (int i = startPosition; i + 1 < startPosition + length; i++) { if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) { - System.arraycopy(bytes, i + 2, bytes, i + 1, length - i - 2); + int relativePosition = i - startPosition; + System.arraycopy(bytes, i + 2, bytes, i + 1, length - relativePosition - 2); length--; } } @@ -697,14 +739,13 @@ public final class Id3Decoder implements MetadataDecoder { */ private static String getCharsetName(int encodingByte) { switch (encodingByte) { - case ID3_TEXT_ENCODING_ISO_8859_1: - return "ISO-8859-1"; case ID3_TEXT_ENCODING_UTF_16: return "UTF-16"; case ID3_TEXT_ENCODING_UTF_16BE: return "UTF-16BE"; case ID3_TEXT_ENCODING_UTF_8: return "UTF-8"; + case ID3_TEXT_ENCODING_ISO_8859_1: default: return "ISO-8859-1"; } @@ -749,6 +790,41 @@ public final class Id3Decoder implements MetadataDecoder { ? 1 : 2; } + /** + * Copies the specified range of an array, or returns a zero length array if the range is invalid. + * + * @param data The array from which to copy. + * @param from The start of the range to copy (inclusive). + * @param to The end of the range to copy (exclusive). + * @return The copied data, or a zero length array if the range is invalid. + */ + private static byte[] copyOfRangeIfValid(byte[] data, int from, int to) { + if (to <= from) { + // Invalid or zero length range. + return Util.EMPTY_BYTE_ARRAY; + } + return Arrays.copyOfRange(data, from, to); + } + + /** + * Returns a string obtained by decoding the specified range of {@code data} using the specified + * {@code charsetName}. An empty string is returned if the range is invalid. + * + * @param data The array from which to decode the string. + * @param from The start of the range. + * @param to The end of the range (exclusive). + * @param charsetName The name of the Charset to use. + * @return The decoded string, or an empty string if the range is invalid. + * @throws UnsupportedEncodingException If the Charset is not supported. + */ + private static String decodeStringIfValid(byte[] data, int from, int to, String charsetName) + throws UnsupportedEncodingException { + if (to <= from || to > data.length) { + return ""; + } + return new String(data, from, to - from, charsetName); + } + private static final class Id3Header { private final int majorVersion; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java index f92c3bc10587..f96b5e752c9a 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java @@ -16,7 +16,6 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; /** * Base class for ID3 frames. @@ -29,7 +28,12 @@ public abstract class Id3Frame implements Metadata.Entry { public final String id; public Id3Frame(String id) { - this.id = Assertions.checkNotNull(id); + this.id = id; + } + + @Override + public String toString() { + return id; } @Override diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/InternalFrame.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/InternalFrame.java new file mode 100644 index 000000000000..ab8ccff343e7 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/InternalFrame.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** Internal ID3 frame that is intended for use by the player. */ +public final class InternalFrame extends Id3Frame { + + public static final String ID = "----"; + + public final String domain; + public final String description; + public final String text; + + public InternalFrame(String domain, String description, String text) { + super(ID); + this.domain = domain; + this.description = description; + this.text = text; + } + + /* package */ InternalFrame(Parcel in) { + super(ID); + domain = castNonNull(in.readString()); + description = castNonNull(in.readString()); + text = castNonNull(in.readString()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + InternalFrame other = (InternalFrame) obj; + return Util.areEqual(description, other.description) + && Util.areEqual(domain, other.domain) + && Util.areEqual(text, other.text); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (domain != null ? domain.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (text != null ? text.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return id + ": domain=" + domain + ", description=" + description; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(domain); + dest.writeString(text); + } + + public static final Creator CREATOR = + new Creator() { + + @Override + public InternalFrame createFromParcel(Parcel in) { + return new InternalFrame(in); + } + + @Override + public InternalFrame[] newArray(int size) { + return new InternalFrame[size]; + } + }; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/MlltFrame.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/MlltFrame.java new file mode 100644 index 000000000000..441235d7c928 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/MlltFrame.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import android.os.Parcel; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.Arrays; + +/** MPEG location lookup table frame. */ +public final class MlltFrame extends Id3Frame { + + public static final String ID = "MLLT"; + + public final int mpegFramesBetweenReference; + public final int bytesBetweenReference; + public final int millisecondsBetweenReference; + public final int[] bytesDeviations; + public final int[] millisecondsDeviations; + + public MlltFrame( + int mpegFramesBetweenReference, + int bytesBetweenReference, + int millisecondsBetweenReference, + int[] bytesDeviations, + int[] millisecondsDeviations) { + super(ID); + this.mpegFramesBetweenReference = mpegFramesBetweenReference; + this.bytesBetweenReference = bytesBetweenReference; + this.millisecondsBetweenReference = millisecondsBetweenReference; + this.bytesDeviations = bytesDeviations; + this.millisecondsDeviations = millisecondsDeviations; + } + + /* package */ + MlltFrame(Parcel in) { + super(ID); + this.mpegFramesBetweenReference = in.readInt(); + this.bytesBetweenReference = in.readInt(); + this.millisecondsBetweenReference = in.readInt(); + this.bytesDeviations = Util.castNonNull(in.createIntArray()); + this.millisecondsDeviations = Util.castNonNull(in.createIntArray()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + MlltFrame other = (MlltFrame) obj; + return mpegFramesBetweenReference == other.mpegFramesBetweenReference + && bytesBetweenReference == other.bytesBetweenReference + && millisecondsBetweenReference == other.millisecondsBetweenReference + && Arrays.equals(bytesDeviations, other.bytesDeviations) + && Arrays.equals(millisecondsDeviations, other.millisecondsDeviations); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + mpegFramesBetweenReference; + result = 31 * result + bytesBetweenReference; + result = 31 * result + millisecondsBetweenReference; + result = 31 * result + Arrays.hashCode(bytesDeviations); + result = 31 * result + Arrays.hashCode(millisecondsDeviations); + return result; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mpegFramesBetweenReference); + dest.writeInt(bytesBetweenReference); + dest.writeInt(millisecondsBetweenReference); + dest.writeIntArray(bytesDeviations); + dest.writeIntArray(millisecondsDeviations); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = + new Creator() { + + @Override + public MlltFrame createFromParcel(Parcel in) { + return new MlltFrame(in); + } + + @Override + public MlltFrame[] newArray(int size) { + return new MlltFrame[size]; + } + }; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.java index b8a3f7bfb33c..248d9996ddc5 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.java @@ -15,8 +15,11 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -38,12 +41,12 @@ public final class PrivFrame extends Id3Frame { /* package */ PrivFrame(Parcel in) { super(ID); - owner = in.readString(); - privateData = in.createByteArray(); + owner = castNonNull(in.readString()); + privateData = castNonNull(in.createByteArray()); } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -62,6 +65,12 @@ public final class PrivFrame extends Id3Frame { return result; } + @Override + public String toString() { + return id + ": owner=" + owner; + } + // Parcelable implementation. + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(owner); diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java index 562f1972953c..c0bd36ccf7c9 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java @@ -15,8 +15,11 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; /** @@ -24,23 +27,23 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; */ public final class TextInformationFrame extends Id3Frame { - public final String description; + @Nullable public final String description; public final String value; - public TextInformationFrame(String id, String description, String value) { + public TextInformationFrame(String id, @Nullable String description, String value) { super(id); this.description = description; this.value = value; } /* package */ TextInformationFrame(Parcel in) { - super(in.readString()); + super(castNonNull(in.readString())); description = in.readString(); - value = in.readString(); + value = castNonNull(in.readString()); } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -61,6 +64,13 @@ public final class TextInformationFrame extends Id3Frame { return result; } + @Override + public String toString() { + return id + ": description=" + description + ": value=" + value; + } + + // Parcelable implementation. + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(id); diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java index 5295dd6ce5a4..ced474960e28 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java @@ -15,8 +15,11 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; /** @@ -24,23 +27,23 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; */ public final class UrlLinkFrame extends Id3Frame { - public final String description; + @Nullable public final String description; public final String url; - public UrlLinkFrame(String id, String description, String url) { + public UrlLinkFrame(String id, @Nullable String description, String url) { super(id); this.description = description; this.url = url; } /* package */ UrlLinkFrame(Parcel in) { - super(in.readString()); + super(castNonNull(in.readString())); description = in.readString(); - url = in.readString(); + url = castNonNull(in.readString()); } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -61,6 +64,13 @@ public final class UrlLinkFrame extends Id3Frame { return result; } + @Override + public String toString() { + return id + ": url=" + url; + } + + // Parcelable implementation. + @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(id); diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/package-info.java new file mode 100644 index 000000000000..87b20161dfdd --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/package-info.java new file mode 100644 index 000000000000..e5775f7accf7 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java index c6a276391026..3437c8dd7348 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java @@ -18,14 +18,24 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.scte35; import android.os.Parcel; import android.os.Parcelable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; /** * Represents a private command as defined in SCTE35, Section 9.3.6. */ public final class PrivateCommand extends SpliceCommand { + /** + * The {@code pts_adjustment} as defined in SCTE35, Section 9.2. + */ public final long ptsAdjustment; + /** + * The identifier as defined in SCTE35, Section 9.3.6. + */ public final long identifier; + /** + * The private bytes as defined in SCTE35, Section 9.3.6. + */ public final byte[] commandBytes; private PrivateCommand(long identifier, byte[] commandBytes, long ptsAdjustment) { @@ -37,8 +47,7 @@ public final class PrivateCommand extends SpliceCommand { private PrivateCommand(Parcel in) { ptsAdjustment = in.readLong(); identifier = in.readLong(); - commandBytes = new byte[in.readInt()]; - in.readByteArray(commandBytes); + commandBytes = Util.castNonNull(in.createByteArray()); } /* package */ static PrivateCommand parseFromSection(ParsableByteArray sectionData, @@ -55,7 +64,6 @@ public final class PrivateCommand extends SpliceCommand { public void writeToParcel(Parcel dest, int flags) { dest.writeLong(ptsAdjustment); dest.writeLong(identifier); - dest.writeInt(commandBytes.length); dest.writeByteArray(commandBytes); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java index f2dd718e250b..866a7ec8bcf7 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java @@ -22,6 +22,13 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; */ public abstract class SpliceCommand implements Metadata.Entry { + @Override + public String toString() { + return "SCTE-35 splice command: type=" + getClass().getSimpleName(); + } + + // Parcelable implementation. + @Override public int describeContents() { return 0; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java index aa40f6e9f230..a90bddb07858 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java @@ -15,14 +15,16 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.scte35; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataDecoder; -import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataDecoderException; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; import java.nio.ByteBuffer; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Decodes splice info sections and produces splice commands. @@ -38,15 +40,18 @@ public final class SpliceInfoDecoder implements MetadataDecoder { private final ParsableByteArray sectionData; private final ParsableBitArray sectionHeader; - private TimestampAdjuster timestampAdjuster; + @MonotonicNonNull private TimestampAdjuster timestampAdjuster; public SpliceInfoDecoder() { sectionData = new ParsableByteArray(); sectionHeader = new ParsableBitArray(); } + @SuppressWarnings("ByteBufferBackingArray") @Override - public Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException { + public Metadata decode(MetadataInputBuffer inputBuffer) { + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); + // Internal timestamps adjustment. if (timestampAdjuster == null || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) { @@ -54,7 +59,6 @@ public final class SpliceInfoDecoder implements MetadataDecoder { timestampAdjuster.adjustSampleTimestamp(inputBuffer.timeUs - inputBuffer.subsampleOffsetUs); } - ByteBuffer buffer = inputBuffer.data; byte[] data = buffer.array(); int size = buffer.limit(); sectionData.reset(data, size); @@ -68,7 +72,7 @@ public final class SpliceInfoDecoder implements MetadataDecoder { sectionHeader.skipBits(20); int spliceCommandLength = sectionHeader.readBits(12); int spliceCommandType = sectionHeader.readBits(8); - SpliceCommand command = null; + @Nullable SpliceCommand command = null; // Go to the start of the command by skipping all fields up to command_type. sectionData.skipBytes(14); switch (spliceCommandType) { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java index d274f876dd59..5993efb10fde 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java @@ -29,24 +29,72 @@ import java.util.List; */ public final class SpliceInsertCommand extends SpliceCommand { + /** + * The splice event id. + */ public final long spliceEventId; + /** + * True if the event with id {@link #spliceEventId} has been canceled. + */ public final boolean spliceEventCancelIndicator; + /** + * If true, the splice event is an opportunity to exit from the network feed. If false, indicates + * an opportunity to return to the network feed. + */ public final boolean outOfNetworkIndicator; + /** + * Whether the splice mode is program splice mode, whereby all PIDs/components are to be spliced. + * If false, splicing is done per PID/component. + */ public final boolean programSpliceFlag; + /** + * Whether splicing should be done at the nearest opportunity. If false, splicing should be done + * at the moment indicated by {@link #programSplicePlaybackPositionUs} or + * {@link ComponentSplice#componentSplicePlaybackPositionUs}, depending on + * {@link #programSpliceFlag}. + */ public final boolean spliceImmediateFlag; + /** + * If {@link #programSpliceFlag} is true, the PTS at which the program splice should occur. + * {@link C#TIME_UNSET} otherwise. + */ public final long programSplicePts; + /** + * Equivalent to {@link #programSplicePts} but in the playback timebase. + */ public final long programSplicePlaybackPositionUs; + /** + * If {@link #programSpliceFlag} is false, a non-empty list containing the + * {@link ComponentSplice}s. Otherwise, an empty list. + */ public final List componentSpliceList; + /** + * If {@link #breakDurationUs} is not {@link C#TIME_UNSET}, defines whether + * {@link #breakDurationUs} should be used to know when to return to the network feed. If + * {@link #breakDurationUs} is {@link C#TIME_UNSET}, the value is undefined. + */ public final boolean autoReturn; - public final long breakDuration; + /** + * The duration of the splice in microseconds, or {@link C#TIME_UNSET} if no duration is present. + */ + public final long breakDurationUs; + /** + * The unique program id as defined in SCTE35, Section 9.3.3. + */ public final int uniqueProgramId; + /** + * Holds the value of {@code avail_num} as defined in SCTE35, Section 9.3.3. + */ public final int availNum; + /** + * Holds the value of {@code avails_expected} as defined in SCTE35, Section 9.3.3. + */ public final int availsExpected; private SpliceInsertCommand(long spliceEventId, boolean spliceEventCancelIndicator, boolean outOfNetworkIndicator, boolean programSpliceFlag, boolean spliceImmediateFlag, long programSplicePts, long programSplicePlaybackPositionUs, - List componentSpliceList, boolean autoReturn, long breakDuration, + List componentSpliceList, boolean autoReturn, long breakDurationUs, int uniqueProgramId, int availNum, int availsExpected) { this.spliceEventId = spliceEventId; this.spliceEventCancelIndicator = spliceEventCancelIndicator; @@ -57,7 +105,7 @@ public final class SpliceInsertCommand extends SpliceCommand { this.programSplicePlaybackPositionUs = programSplicePlaybackPositionUs; this.componentSpliceList = Collections.unmodifiableList(componentSpliceList); this.autoReturn = autoReturn; - this.breakDuration = breakDuration; + this.breakDurationUs = breakDurationUs; this.uniqueProgramId = uniqueProgramId; this.availNum = availNum; this.availsExpected = availsExpected; @@ -78,7 +126,7 @@ public final class SpliceInsertCommand extends SpliceCommand { } this.componentSpliceList = Collections.unmodifiableList(componentSpliceList); autoReturn = in.readByte() == 1; - breakDuration = in.readLong(); + breakDurationUs = in.readLong(); uniqueProgramId = in.readInt(); availNum = in.readInt(); availsExpected = in.readInt(); @@ -98,7 +146,7 @@ public final class SpliceInsertCommand extends SpliceCommand { int availNum = 0; int availsExpected = 0; boolean autoReturn = false; - long duration = C.TIME_UNSET; + long breakDurationUs = C.TIME_UNSET; if (!spliceEventCancelIndicator) { int headerByte = sectionData.readUnsignedByte(); outOfNetworkIndicator = (headerByte & 0x80) != 0; @@ -124,7 +172,8 @@ public final class SpliceInsertCommand extends SpliceCommand { if (durationFlag) { long firstByte = sectionData.readUnsignedByte(); autoReturn = (firstByte & 0x80) != 0; - duration = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt(); + long breakDuration90khz = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt(); + breakDurationUs = breakDuration90khz * 1000 / 90; } uniqueProgramId = sectionData.readUnsignedShort(); availNum = sectionData.readUnsignedByte(); @@ -133,7 +182,7 @@ public final class SpliceInsertCommand extends SpliceCommand { return new SpliceInsertCommand(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator, programSpliceFlag, spliceImmediateFlag, programSplicePts, timestampAdjuster.adjustTsTimestamp(programSplicePts), componentSplices, autoReturn, - duration, uniqueProgramId, availNum, availsExpected); + breakDurationUs, uniqueProgramId, availNum, availsExpected); } /** @@ -181,7 +230,7 @@ public final class SpliceInsertCommand extends SpliceCommand { componentSpliceList.get(i).writeToParcel(dest); } dest.writeByte((byte) (autoReturn ? 1 : 0)); - dest.writeLong(breakDuration); + dest.writeLong(breakDurationUs); dest.writeInt(uniqueProgramId); dest.writeInt(availNum); dest.writeInt(availsExpected); diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java index 1c38e526d79f..e1d369bc87b0 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java @@ -33,22 +33,62 @@ public final class SpliceScheduleCommand extends SpliceCommand { */ public static final class Event { + /** + * The splice event id. + */ public final long spliceEventId; + /** + * True if the event with id {@link #spliceEventId} has been canceled. + */ public final boolean spliceEventCancelIndicator; + /** + * If true, the splice event is an opportunity to exit from the network feed. If false, + * indicates an opportunity to return to the network feed. + */ public final boolean outOfNetworkIndicator; + /** + * Whether the splice mode is program splice mode, whereby all PIDs/components are to be + * spliced. If false, splicing is done per PID/component. + */ public final boolean programSpliceFlag; + /** + * Represents the time of the signaled splice event as the number of seconds since 00 hours UTC, + * January 6th, 1980, with the count of intervening leap seconds included. + */ public final long utcSpliceTime; + /** + * If {@link #programSpliceFlag} is false, a non-empty list containing the + * {@link ComponentSplice}s. Otherwise, an empty list. + */ public final List componentSpliceList; + /** + * If {@link #breakDurationUs} is not {@link C#TIME_UNSET}, defines whether + * {@link #breakDurationUs} should be used to know when to return to the network feed. If + * {@link #breakDurationUs} is {@link C#TIME_UNSET}, the value is undefined. + */ public final boolean autoReturn; - public final long breakDuration; + /** + * The duration of the splice in microseconds, or {@link C#TIME_UNSET} if no duration is + * present. + */ + public final long breakDurationUs; + /** + * The unique program id as defined in SCTE35, Section 9.3.2. + */ public final int uniqueProgramId; + /** + * Holds the value of {@code avail_num} as defined in SCTE35, Section 9.3.2. + */ public final int availNum; + /** + * Holds the value of {@code avails_expected} as defined in SCTE35, Section 9.3.2. + */ public final int availsExpected; private Event(long spliceEventId, boolean spliceEventCancelIndicator, boolean outOfNetworkIndicator, boolean programSpliceFlag, List componentSpliceList, long utcSpliceTime, boolean autoReturn, - long breakDuration, int uniqueProgramId, int availNum, int availsExpected) { + long breakDurationUs, int uniqueProgramId, int availNum, int availsExpected) { this.spliceEventId = spliceEventId; this.spliceEventCancelIndicator = spliceEventCancelIndicator; this.outOfNetworkIndicator = outOfNetworkIndicator; @@ -56,7 +96,7 @@ public final class SpliceScheduleCommand extends SpliceCommand { this.componentSpliceList = Collections.unmodifiableList(componentSpliceList); this.utcSpliceTime = utcSpliceTime; this.autoReturn = autoReturn; - this.breakDuration = breakDuration; + this.breakDurationUs = breakDurationUs; this.uniqueProgramId = uniqueProgramId; this.availNum = availNum; this.availsExpected = availsExpected; @@ -75,7 +115,7 @@ public final class SpliceScheduleCommand extends SpliceCommand { this.componentSpliceList = Collections.unmodifiableList(componentSpliceList); this.utcSpliceTime = in.readLong(); this.autoReturn = in.readByte() == 1; - this.breakDuration = in.readLong(); + this.breakDurationUs = in.readLong(); this.uniqueProgramId = in.readInt(); this.availNum = in.readInt(); this.availsExpected = in.readInt(); @@ -93,7 +133,7 @@ public final class SpliceScheduleCommand extends SpliceCommand { int availNum = 0; int availsExpected = 0; boolean autoReturn = false; - long duration = C.TIME_UNSET; + long breakDurationUs = C.TIME_UNSET; if (!spliceEventCancelIndicator) { int headerByte = sectionData.readUnsignedByte(); outOfNetworkIndicator = (headerByte & 0x80) != 0; @@ -114,15 +154,16 @@ public final class SpliceScheduleCommand extends SpliceCommand { if (durationFlag) { long firstByte = sectionData.readUnsignedByte(); autoReturn = (firstByte & 0x80) != 0; - duration = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt(); + long breakDuration90khz = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt(); + breakDurationUs = breakDuration90khz * 1000 / 90; } uniqueProgramId = sectionData.readUnsignedShort(); availNum = sectionData.readUnsignedByte(); availsExpected = sectionData.readUnsignedByte(); } return new Event(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator, - programSpliceFlag, componentSplices, utcSpliceTime, autoReturn, duration, uniqueProgramId, - availNum, availsExpected); + programSpliceFlag, componentSplices, utcSpliceTime, autoReturn, breakDurationUs, + uniqueProgramId, availNum, availsExpected); } private void writeToParcel(Parcel dest) { @@ -137,7 +178,7 @@ public final class SpliceScheduleCommand extends SpliceCommand { } dest.writeLong(utcSpliceTime); dest.writeByte((byte) (autoReturn ? 1 : 0)); - dest.writeLong(breakDuration); + dest.writeLong(breakDurationUs); dest.writeInt(uniqueProgramId); dest.writeInt(availNum); dest.writeInt(availsExpected); @@ -173,6 +214,9 @@ public final class SpliceScheduleCommand extends SpliceCommand { } + /** + * The list of scheduled events. + */ public final List events; private SpliceScheduleCommand(List events) { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java index c0dbdc463d77..f50a029f1bc6 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java @@ -25,7 +25,13 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjust */ public final class TimeSignalCommand extends SpliceCommand { + /** + * A PTS value, as defined in SCTE35, Section 9.3.4. + */ public final long ptsTime; + /** + * Equivalent to {@link #ptsTime} but in the playback timebase. + */ public final long playbackPositionUs; private TimeSignalCommand(long ptsTime, long playbackPositionUs) { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/package-info.java new file mode 100644 index 000000000000..17ce76bb9f36 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.scte35; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ActionFile.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ActionFile.java new file mode 100644 index 000000000000..5451ea5530d8 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ActionFile.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import android.net.Uri; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.DownloadRequest.UnsupportedRequestException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.AtomicFile; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.DataInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * Loads {@link DownloadRequest DownloadRequests} from legacy action files. + * + * @deprecated Legacy action files should be merged into download indices using {@link + * ActionFileUpgradeUtil}. + */ +@Deprecated +/* package */ final class ActionFile { + + private static final int VERSION = 0; + + private final AtomicFile atomicFile; + + /** + * @param actionFile The file from which {@link DownloadRequest DownloadRequests} will be loaded. + */ + public ActionFile(File actionFile) { + atomicFile = new AtomicFile(actionFile); + } + + /** Returns whether the file or its backup exists. */ + public boolean exists() { + return atomicFile.exists(); + } + + /** Deletes the action file and its backup. */ + public void delete() { + atomicFile.delete(); + } + + /** + * Loads {@link DownloadRequest DownloadRequests} from the file. + * + * @return The loaded {@link DownloadRequest DownloadRequests}, or an empty array if the file does + * not exist. + * @throws IOException If there is an error reading the file. + */ + public DownloadRequest[] load() throws IOException { + if (!exists()) { + return new DownloadRequest[0]; + } + @Nullable InputStream inputStream = null; + try { + inputStream = atomicFile.openRead(); + DataInputStream dataInputStream = new DataInputStream(inputStream); + int version = dataInputStream.readInt(); + if (version > VERSION) { + throw new IOException("Unsupported action file version: " + version); + } + int actionCount = dataInputStream.readInt(); + ArrayList actions = new ArrayList<>(); + for (int i = 0; i < actionCount; i++) { + try { + actions.add(readDownloadRequest(dataInputStream)); + } catch (UnsupportedRequestException e) { + // remove DownloadRequest is not supported. Ignore and continue loading rest. + } + } + return actions.toArray(new DownloadRequest[0]); + } finally { + Util.closeQuietly(inputStream); + } + } + + private static DownloadRequest readDownloadRequest(DataInputStream input) throws IOException { + String type = input.readUTF(); + int version = input.readInt(); + + Uri uri = Uri.parse(input.readUTF()); + boolean isRemoveAction = input.readBoolean(); + + int dataLength = input.readInt(); + @Nullable byte[] data; + if (dataLength != 0) { + data = new byte[dataLength]; + input.readFully(data); + } else { + data = null; + } + + // Serialized version 0 progressive actions did not contain keys. + boolean isLegacyProgressive = version == 0 && DownloadRequest.TYPE_PROGRESSIVE.equals(type); + List keys = new ArrayList<>(); + if (!isLegacyProgressive) { + int keyCount = input.readInt(); + for (int i = 0; i < keyCount; i++) { + keys.add(readKey(type, version, input)); + } + } + + // Serialized version 0 and 1 DASH/HLS/SS actions did not contain a custom cache key. + boolean isLegacySegmented = + version < 2 + && (DownloadRequest.TYPE_DASH.equals(type) + || DownloadRequest.TYPE_HLS.equals(type) + || DownloadRequest.TYPE_SS.equals(type)); + @Nullable String customCacheKey = null; + if (!isLegacySegmented) { + customCacheKey = input.readBoolean() ? input.readUTF() : null; + } + + // Serialized version 0, 1 and 2 did not contain an id. We need to generate one. + String id = version < 3 ? generateDownloadId(uri, customCacheKey) : input.readUTF(); + + if (isRemoveAction) { + // Remove actions are not supported anymore. + throw new UnsupportedRequestException(); + } + return new DownloadRequest(id, type, uri, keys, customCacheKey, data); + } + + private static StreamKey readKey(String type, int version, DataInputStream input) + throws IOException { + int periodIndex; + int groupIndex; + int trackIndex; + + // Serialized version 0 HLS/SS actions did not contain a period index. + if ((DownloadRequest.TYPE_HLS.equals(type) || DownloadRequest.TYPE_SS.equals(type)) + && version == 0) { + periodIndex = 0; + groupIndex = input.readInt(); + trackIndex = input.readInt(); + } else { + periodIndex = input.readInt(); + groupIndex = input.readInt(); + trackIndex = input.readInt(); + } + return new StreamKey(periodIndex, groupIndex, trackIndex); + } + + private static String generateDownloadId(Uri uri, @Nullable String customCacheKey) { + return customCacheKey != null ? customCacheKey : uri.toString(); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java new file mode 100644 index 000000000000..aa66c73e6baf --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java @@ -0,0 +1,120 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STATE_QUEUED; + +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import java.io.File; +import java.io.IOException; + +/** Utility class for upgrading legacy action files into {@link DefaultDownloadIndex}. */ +public final class ActionFileUpgradeUtil { + + /** Provides download IDs during action file upgrade. */ + public interface DownloadIdProvider { + + /** + * Returns a download id for given request. + * + * @param downloadRequest The request for which an ID is required. + * @return A corresponding download ID. + */ + String getId(DownloadRequest downloadRequest); + } + + private ActionFileUpgradeUtil() {} + + /** + * Merges {@link DownloadRequest DownloadRequests} contained in a legacy action file into a {@link + * DefaultDownloadIndex}, deleting the action file if the merge is successful or if {@code + * deleteOnFailure} is {@code true}. + * + *

This method must not be called while the {@link DefaultDownloadIndex} is being used by a + * {@link DownloadManager}. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @param actionFilePath The action file path. + * @param downloadIdProvider A download ID provider, or {@code null}. If {@code null} then ID of + * each download will be its custom cache key if one is specified, or else its URL. + * @param downloadIndex The index into which the requests will be merged. + * @param deleteOnFailure Whether to delete the action file if the merge fails. + * @param addNewDownloadsAsCompleted Whether to add new downloads as completed. + * @throws IOException If an error occurs loading or merging the requests. + */ + @WorkerThread + @SuppressWarnings("deprecation") + public static void upgradeAndDelete( + File actionFilePath, + @Nullable DownloadIdProvider downloadIdProvider, + DefaultDownloadIndex downloadIndex, + boolean deleteOnFailure, + boolean addNewDownloadsAsCompleted) + throws IOException { + ActionFile actionFile = new ActionFile(actionFilePath); + if (actionFile.exists()) { + boolean success = false; + try { + long nowMs = System.currentTimeMillis(); + for (DownloadRequest request : actionFile.load()) { + if (downloadIdProvider != null) { + request = request.copyWithId(downloadIdProvider.getId(request)); + } + mergeRequest(request, downloadIndex, addNewDownloadsAsCompleted, nowMs); + } + success = true; + } finally { + if (success || deleteOnFailure) { + actionFile.delete(); + } + } + } + } + + /** + * Merges a {@link DownloadRequest} into a {@link DefaultDownloadIndex}. + * + * @param request The request to be merged. + * @param downloadIndex The index into which the request will be merged. + * @param addNewDownloadAsCompleted Whether to add new downloads as completed. + * @throws IOException If an error occurs merging the request. + */ + /* package */ static void mergeRequest( + DownloadRequest request, + DefaultDownloadIndex downloadIndex, + boolean addNewDownloadAsCompleted, + long nowMs) + throws IOException { + @Nullable Download download = downloadIndex.getDownload(request.id); + if (download != null) { + download = DownloadManager.mergeRequest(download, request, download.stopReason, nowMs); + } else { + download = + new Download( + request, + addNewDownloadAsCompleted ? Download.STATE_COMPLETED : STATE_QUEUED, + /* startTimeMs= */ nowMs, + /* updateTimeMs= */ nowMs, + /* contentLength= */ C.LENGTH_UNSET, + Download.STOP_REASON_NONE, + Download.FAILURE_REASON_NONE); + } + downloadIndex.putDownload(download); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java new file mode 100644 index 000000000000..cc1a2873f53a --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -0,0 +1,452 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseIOException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseProvider; +import org.mozilla.thirdparty.com.google.android.exoplayer2.database.VersionTable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.FailureReason; +import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.State; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.List; + +/** A {@link DownloadIndex} that uses SQLite to persist {@link Download Downloads}. */ +public final class DefaultDownloadIndex implements WritableDownloadIndex { + + private static final String TABLE_PREFIX = DatabaseProvider.TABLE_PREFIX + "Downloads"; + + @VisibleForTesting /* package */ static final int TABLE_VERSION = 2; + + private static final String COLUMN_ID = "id"; + private static final String COLUMN_TYPE = "title"; + private static final String COLUMN_URI = "uri"; + private static final String COLUMN_STREAM_KEYS = "stream_keys"; + private static final String COLUMN_CUSTOM_CACHE_KEY = "custom_cache_key"; + private static final String COLUMN_DATA = "data"; + private static final String COLUMN_STATE = "state"; + private static final String COLUMN_START_TIME_MS = "start_time_ms"; + private static final String COLUMN_UPDATE_TIME_MS = "update_time_ms"; + private static final String COLUMN_CONTENT_LENGTH = "content_length"; + private static final String COLUMN_STOP_REASON = "stop_reason"; + private static final String COLUMN_FAILURE_REASON = "failure_reason"; + private static final String COLUMN_PERCENT_DOWNLOADED = "percent_downloaded"; + private static final String COLUMN_BYTES_DOWNLOADED = "bytes_downloaded"; + + private static final int COLUMN_INDEX_ID = 0; + private static final int COLUMN_INDEX_TYPE = 1; + private static final int COLUMN_INDEX_URI = 2; + private static final int COLUMN_INDEX_STREAM_KEYS = 3; + private static final int COLUMN_INDEX_CUSTOM_CACHE_KEY = 4; + private static final int COLUMN_INDEX_DATA = 5; + private static final int COLUMN_INDEX_STATE = 6; + private static final int COLUMN_INDEX_START_TIME_MS = 7; + private static final int COLUMN_INDEX_UPDATE_TIME_MS = 8; + private static final int COLUMN_INDEX_CONTENT_LENGTH = 9; + private static final int COLUMN_INDEX_STOP_REASON = 10; + private static final int COLUMN_INDEX_FAILURE_REASON = 11; + private static final int COLUMN_INDEX_PERCENT_DOWNLOADED = 12; + private static final int COLUMN_INDEX_BYTES_DOWNLOADED = 13; + + private static final String WHERE_ID_EQUALS = COLUMN_ID + " = ?"; + private static final String WHERE_STATE_IS_DOWNLOADING = + COLUMN_STATE + " = " + Download.STATE_DOWNLOADING; + private static final String WHERE_STATE_IS_TERMINAL = + getStateQuery(Download.STATE_COMPLETED, Download.STATE_FAILED); + + private static final String[] COLUMNS = + new String[] { + COLUMN_ID, + COLUMN_TYPE, + COLUMN_URI, + COLUMN_STREAM_KEYS, + COLUMN_CUSTOM_CACHE_KEY, + COLUMN_DATA, + COLUMN_STATE, + COLUMN_START_TIME_MS, + COLUMN_UPDATE_TIME_MS, + COLUMN_CONTENT_LENGTH, + COLUMN_STOP_REASON, + COLUMN_FAILURE_REASON, + COLUMN_PERCENT_DOWNLOADED, + COLUMN_BYTES_DOWNLOADED, + }; + + private static final String TABLE_SCHEMA = + "(" + + COLUMN_ID + + " TEXT PRIMARY KEY NOT NULL," + + COLUMN_TYPE + + " TEXT NOT NULL," + + COLUMN_URI + + " TEXT NOT NULL," + + COLUMN_STREAM_KEYS + + " TEXT NOT NULL," + + COLUMN_CUSTOM_CACHE_KEY + + " TEXT," + + COLUMN_DATA + + " BLOB NOT NULL," + + COLUMN_STATE + + " INTEGER NOT NULL," + + COLUMN_START_TIME_MS + + " INTEGER NOT NULL," + + COLUMN_UPDATE_TIME_MS + + " INTEGER NOT NULL," + + COLUMN_CONTENT_LENGTH + + " INTEGER NOT NULL," + + COLUMN_STOP_REASON + + " INTEGER NOT NULL," + + COLUMN_FAILURE_REASON + + " INTEGER NOT NULL," + + COLUMN_PERCENT_DOWNLOADED + + " REAL NOT NULL," + + COLUMN_BYTES_DOWNLOADED + + " INTEGER NOT NULL)"; + + private static final String TRUE = "1"; + + private final String name; + private final String tableName; + private final DatabaseProvider databaseProvider; + + private boolean initialized; + + /** + * Creates an instance that stores the {@link Download Downloads} in an SQLite database provided + * by a {@link DatabaseProvider}. + * + *

Equivalent to calling {@link #DefaultDownloadIndex(DatabaseProvider, String)} with {@code + * name=""}. + * + *

Applications that only have one download index may use this constructor. Applications that + * have multiple download indices should call {@link #DefaultDownloadIndex(DatabaseProvider, + * String)} to specify a unique name for each index. + * + * @param databaseProvider Provides the SQLite database in which downloads are persisted. + */ + public DefaultDownloadIndex(DatabaseProvider databaseProvider) { + this(databaseProvider, ""); + } + + /** + * Creates an instance that stores the {@link Download Downloads} in an SQLite database provided + * by a {@link DatabaseProvider}. + * + * @param databaseProvider Provides the SQLite database in which downloads are persisted. + * @param name The name of the index. This name is incorporated into the names of the SQLite + * tables in which downloads are persisted. + */ + public DefaultDownloadIndex(DatabaseProvider databaseProvider, String name) { + this.name = name; + this.databaseProvider = databaseProvider; + tableName = TABLE_PREFIX + name; + } + + @Override + @Nullable + public Download getDownload(String id) throws DatabaseIOException { + ensureInitialized(); + try (Cursor cursor = getCursor(WHERE_ID_EQUALS, new String[] {id})) { + if (cursor.getCount() == 0) { + return null; + } + cursor.moveToNext(); + return getDownloadForCurrentRow(cursor); + } catch (SQLiteException e) { + throw new DatabaseIOException(e); + } + } + + @Override + public DownloadCursor getDownloads(@Download.State int... states) throws DatabaseIOException { + ensureInitialized(); + Cursor cursor = getCursor(getStateQuery(states), /* selectionArgs= */ null); + return new DownloadCursorImpl(cursor); + } + + @Override + public void putDownload(Download download) throws DatabaseIOException { + ensureInitialized(); + ContentValues values = new ContentValues(); + values.put(COLUMN_ID, download.request.id); + values.put(COLUMN_TYPE, download.request.type); + values.put(COLUMN_URI, download.request.uri.toString()); + values.put(COLUMN_STREAM_KEYS, encodeStreamKeys(download.request.streamKeys)); + values.put(COLUMN_CUSTOM_CACHE_KEY, download.request.customCacheKey); + values.put(COLUMN_DATA, download.request.data); + values.put(COLUMN_STATE, download.state); + values.put(COLUMN_START_TIME_MS, download.startTimeMs); + values.put(COLUMN_UPDATE_TIME_MS, download.updateTimeMs); + values.put(COLUMN_CONTENT_LENGTH, download.contentLength); + values.put(COLUMN_STOP_REASON, download.stopReason); + values.put(COLUMN_FAILURE_REASON, download.failureReason); + values.put(COLUMN_PERCENT_DOWNLOADED, download.getPercentDownloaded()); + values.put(COLUMN_BYTES_DOWNLOADED, download.getBytesDownloaded()); + try { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values); + } catch (SQLiteException e) { + throw new DatabaseIOException(e); + } + } + + @Override + public void removeDownload(String id) throws DatabaseIOException { + ensureInitialized(); + try { + databaseProvider.getWritableDatabase().delete(tableName, WHERE_ID_EQUALS, new String[] {id}); + } catch (SQLiteException e) { + throw new DatabaseIOException(e); + } + } + + @Override + public void setDownloadingStatesToQueued() throws DatabaseIOException { + ensureInitialized(); + try { + ContentValues values = new ContentValues(); + values.put(COLUMN_STATE, Download.STATE_QUEUED); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.update(tableName, values, WHERE_STATE_IS_DOWNLOADING, /* whereArgs= */ null); + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + @Override + public void setStatesToRemoving() throws DatabaseIOException { + ensureInitialized(); + try { + ContentValues values = new ContentValues(); + values.put(COLUMN_STATE, Download.STATE_REMOVING); + // Only downloads in STATE_FAILED are allowed a failure reason, so we need to clear it here in + // case we're moving downloads from STATE_FAILED to STATE_REMOVING. + values.put(COLUMN_FAILURE_REASON, Download.FAILURE_REASON_NONE); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.update(tableName, values, /* whereClause= */ null, /* whereArgs= */ null); + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + @Override + public void setStopReason(int stopReason) throws DatabaseIOException { + ensureInitialized(); + try { + ContentValues values = new ContentValues(); + values.put(COLUMN_STOP_REASON, stopReason); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.update(tableName, values, WHERE_STATE_IS_TERMINAL, /* whereArgs= */ null); + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + @Override + public void setStopReason(String id, int stopReason) throws DatabaseIOException { + ensureInitialized(); + try { + ContentValues values = new ContentValues(); + values.put(COLUMN_STOP_REASON, stopReason); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.update( + tableName, + values, + WHERE_STATE_IS_TERMINAL + " AND " + WHERE_ID_EQUALS, + new String[] {id}); + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + private void ensureInitialized() throws DatabaseIOException { + if (initialized) { + return; + } + try { + SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase(); + int version = VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, name); + if (version != TABLE_VERSION) { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransactionNonExclusive(); + try { + VersionTable.setVersion( + writableDatabase, VersionTable.FEATURE_OFFLINE, name, TABLE_VERSION); + writableDatabase.execSQL("DROP TABLE IF EXISTS " + tableName); + writableDatabase.execSQL("CREATE TABLE " + tableName + " " + TABLE_SCHEMA); + writableDatabase.setTransactionSuccessful(); + } finally { + writableDatabase.endTransaction(); + } + } + initialized = true; + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + // incompatible types in argument. + @SuppressWarnings("nullness:argument.type.incompatible") + private Cursor getCursor(String selection, @Nullable String[] selectionArgs) + throws DatabaseIOException { + try { + String sortOrder = COLUMN_START_TIME_MS + " ASC"; + return databaseProvider + .getReadableDatabase() + .query( + tableName, + COLUMNS, + selection, + selectionArgs, + /* groupBy= */ null, + /* having= */ null, + sortOrder); + } catch (SQLiteException e) { + throw new DatabaseIOException(e); + } + } + + private static String getStateQuery(@Download.State int... states) { + if (states.length == 0) { + return TRUE; + } + StringBuilder selectionBuilder = new StringBuilder(); + selectionBuilder.append(COLUMN_STATE).append(" IN ("); + for (int i = 0; i < states.length; i++) { + if (i > 0) { + selectionBuilder.append(','); + } + selectionBuilder.append(states[i]); + } + selectionBuilder.append(')'); + return selectionBuilder.toString(); + } + + private static Download getDownloadForCurrentRow(Cursor cursor) { + DownloadRequest request = + new DownloadRequest( + /* id= */ cursor.getString(COLUMN_INDEX_ID), + /* type= */ cursor.getString(COLUMN_INDEX_TYPE), + /* uri= */ Uri.parse(cursor.getString(COLUMN_INDEX_URI)), + /* streamKeys= */ decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)), + /* customCacheKey= */ cursor.getString(COLUMN_INDEX_CUSTOM_CACHE_KEY), + /* data= */ cursor.getBlob(COLUMN_INDEX_DATA)); + DownloadProgress downloadProgress = new DownloadProgress(); + downloadProgress.bytesDownloaded = cursor.getLong(COLUMN_INDEX_BYTES_DOWNLOADED); + downloadProgress.percentDownloaded = cursor.getFloat(COLUMN_INDEX_PERCENT_DOWNLOADED); + @State int state = cursor.getInt(COLUMN_INDEX_STATE); + // It's possible the database contains failure reasons for non-failed downloads, which is + // invalid. Clear them here. See https://github.com/google/ExoPlayer/issues/6785. + @FailureReason + int failureReason = + state == Download.STATE_FAILED + ? cursor.getInt(COLUMN_INDEX_FAILURE_REASON) + : Download.FAILURE_REASON_NONE; + return new Download( + request, + state, + /* startTimeMs= */ cursor.getLong(COLUMN_INDEX_START_TIME_MS), + /* updateTimeMs= */ cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS), + /* contentLength= */ cursor.getLong(COLUMN_INDEX_CONTENT_LENGTH), + /* stopReason= */ cursor.getInt(COLUMN_INDEX_STOP_REASON), + failureReason, + downloadProgress); + } + + private static String encodeStreamKeys(List streamKeys) { + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < streamKeys.size(); i++) { + StreamKey streamKey = streamKeys.get(i); + stringBuilder + .append(streamKey.periodIndex) + .append('.') + .append(streamKey.groupIndex) + .append('.') + .append(streamKey.trackIndex) + .append(','); + } + if (stringBuilder.length() > 0) { + stringBuilder.setLength(stringBuilder.length() - 1); + } + return stringBuilder.toString(); + } + + private static List decodeStreamKeys(String encodedStreamKeys) { + ArrayList streamKeys = new ArrayList<>(); + if (encodedStreamKeys.isEmpty()) { + return streamKeys; + } + String[] streamKeysStrings = Util.split(encodedStreamKeys, ","); + for (String streamKeysString : streamKeysStrings) { + String[] indices = Util.split(streamKeysString, "\\."); + Assertions.checkState(indices.length == 3); + streamKeys.add( + new StreamKey( + Integer.parseInt(indices[0]), + Integer.parseInt(indices[1]), + Integer.parseInt(indices[2]))); + } + return streamKeys; + } + + private static final class DownloadCursorImpl implements DownloadCursor { + + private final Cursor cursor; + + private DownloadCursorImpl(Cursor cursor) { + this.cursor = cursor; + } + + @Override + public Download getDownload() { + return getDownloadForCurrentRow(cursor); + } + + @Override + public int getCount() { + return cursor.getCount(); + } + + @Override + public int getPosition() { + return cursor.getPosition(); + } + + @Override + public boolean moveToPosition(int position) { + return cursor.moveToPosition(position); + } + + @Override + public void close() { + cursor.close(); + } + + @Override + public boolean isClosed() { + return cursor.isClosed(); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java new file mode 100644 index 000000000000..6391af8a954f --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import android.net.Uri; +import androidx.annotation.Nullable; +import java.lang.reflect.Constructor; +import java.util.List; + +/** + * Default {@link DownloaderFactory}, supporting creation of progressive, DASH, HLS and + * SmoothStreaming downloaders. Note that for the latter three, the corresponding library module + * must be built into the application. + */ +public class DefaultDownloaderFactory implements DownloaderFactory { + + @Nullable private static final Constructor DASH_DOWNLOADER_CONSTRUCTOR; + @Nullable private static final Constructor HLS_DOWNLOADER_CONSTRUCTOR; + @Nullable private static final Constructor SS_DOWNLOADER_CONSTRUCTOR; + + static { + Constructor dashDownloaderConstructor = null; + try { + // LINT.IfChange + dashDownloaderConstructor = + getDownloaderConstructor( + Class.forName("com.google.android.exoplayer2.source.dash.offline.DashDownloader")); + // LINT.ThenChange(../../../../../../../../proguard-rules.txt) + } catch (ClassNotFoundException e) { + // Expected if the app was built without the DASH module. + } + DASH_DOWNLOADER_CONSTRUCTOR = dashDownloaderConstructor; + Constructor hlsDownloaderConstructor = null; + try { + // LINT.IfChange + hlsDownloaderConstructor = + getDownloaderConstructor( + Class.forName("com.google.android.exoplayer2.source.hls.offline.HlsDownloader")); + // LINT.ThenChange(../../../../../../../../proguard-rules.txt) + } catch (ClassNotFoundException e) { + // Expected if the app was built without the HLS module. + } + HLS_DOWNLOADER_CONSTRUCTOR = hlsDownloaderConstructor; + Constructor ssDownloaderConstructor = null; + try { + // LINT.IfChange + ssDownloaderConstructor = + getDownloaderConstructor( + Class.forName( + "com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader")); + // LINT.ThenChange(../../../../../../../../proguard-rules.txt) + } catch (ClassNotFoundException e) { + // Expected if the app was built without the SmoothStreaming module. + } + SS_DOWNLOADER_CONSTRUCTOR = ssDownloaderConstructor; + } + + private final DownloaderConstructorHelper downloaderConstructorHelper; + + /** @param downloaderConstructorHelper A helper for instantiating downloaders. */ + public DefaultDownloaderFactory(DownloaderConstructorHelper downloaderConstructorHelper) { + this.downloaderConstructorHelper = downloaderConstructorHelper; + } + + @Override + public Downloader createDownloader(DownloadRequest request) { + switch (request.type) { + case DownloadRequest.TYPE_PROGRESSIVE: + return new ProgressiveDownloader( + request.uri, request.customCacheKey, downloaderConstructorHelper); + case DownloadRequest.TYPE_DASH: + return createDownloader(request, DASH_DOWNLOADER_CONSTRUCTOR); + case DownloadRequest.TYPE_HLS: + return createDownloader(request, HLS_DOWNLOADER_CONSTRUCTOR); + case DownloadRequest.TYPE_SS: + return createDownloader(request, SS_DOWNLOADER_CONSTRUCTOR); + default: + throw new IllegalArgumentException("Unsupported type: " + request.type); + } + } + + private Downloader createDownloader( + DownloadRequest request, @Nullable Constructor constructor) { + if (constructor == null) { + throw new IllegalStateException("Module missing for: " + request.type); + } + try { + return constructor.newInstance(request.uri, request.streamKeys, downloaderConstructorHelper); + } catch (Exception e) { + throw new RuntimeException("Failed to instantiate downloader for: " + request.type, e); + } + } + + // LINT.IfChange + private static Constructor getDownloaderConstructor(Class clazz) { + try { + return clazz + .asSubclass(Downloader.class) + .getConstructor(Uri.class, List.class, DownloaderConstructorHelper.class); + } catch (NoSuchMethodException e) { + // The downloader is present, but the expected constructor is missing. + throw new RuntimeException("Downloader constructor missing", e); + } + } + // LINT.ThenChange(../../../../../../../../proguard-rules.txt) +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/Download.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/Download.java new file mode 100644 index 000000000000..a3bc253a6e41 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/Download.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Represents state of a download. */ +public final class Download { + + /** + * Download states. One of {@link #STATE_QUEUED}, {@link #STATE_STOPPED}, {@link + * #STATE_DOWNLOADING}, {@link #STATE_COMPLETED}, {@link #STATE_FAILED}, {@link #STATE_REMOVING} + * or {@link #STATE_RESTARTING}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + STATE_QUEUED, + STATE_STOPPED, + STATE_DOWNLOADING, + STATE_COMPLETED, + STATE_FAILED, + STATE_REMOVING, + STATE_RESTARTING + }) + public @interface State {} + // Important: These constants are persisted into DownloadIndex. Do not change them. + /** + * The download is waiting to be started. A download may be queued because the {@link + * DownloadManager} + * + *

    + *
  • Is {@link DownloadManager#getDownloadsPaused() paused} + *
  • Has {@link DownloadManager#getRequirements() Requirements} that are not met + *
  • Has already started {@link DownloadManager#getMaxParallelDownloads() + * maxParallelDownloads} + *
+ */ + public static final int STATE_QUEUED = 0; + /** The download is stopped for a specified {@link #stopReason}. */ + public static final int STATE_STOPPED = 1; + /** The download is currently started. */ + public static final int STATE_DOWNLOADING = 2; + /** The download completed. */ + public static final int STATE_COMPLETED = 3; + /** The download failed. */ + public static final int STATE_FAILED = 4; + /** The download is being removed. */ + public static final int STATE_REMOVING = 5; + /** The download will restart after all downloaded data is removed. */ + public static final int STATE_RESTARTING = 7; + + /** Failure reasons. Either {@link #FAILURE_REASON_NONE} or {@link #FAILURE_REASON_UNKNOWN}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({FAILURE_REASON_NONE, FAILURE_REASON_UNKNOWN}) + public @interface FailureReason {} + /** The download isn't failed. */ + public static final int FAILURE_REASON_NONE = 0; + /** The download is failed because of unknown reason. */ + public static final int FAILURE_REASON_UNKNOWN = 1; + + /** The download isn't stopped. */ + public static final int STOP_REASON_NONE = 0; + + /** The download request. */ + public final DownloadRequest request; + /** The state of the download. */ + @State public final int state; + /** The first time when download entry is created. */ + public final long startTimeMs; + /** The last update time. */ + public final long updateTimeMs; + /** The total size of the content in bytes, or {@link C#LENGTH_UNSET} if unknown. */ + public final long contentLength; + /** The reason the download is stopped, or {@link #STOP_REASON_NONE}. */ + public final int stopReason; + /** + * If {@link #state} is {@link #STATE_FAILED} then this is the cause, otherwise {@link + * #FAILURE_REASON_NONE}. + */ + @FailureReason public final int failureReason; + + /* package */ final DownloadProgress progress; + + public Download( + DownloadRequest request, + @State int state, + long startTimeMs, + long updateTimeMs, + long contentLength, + int stopReason, + @FailureReason int failureReason) { + this( + request, + state, + startTimeMs, + updateTimeMs, + contentLength, + stopReason, + failureReason, + new DownloadProgress()); + } + + public Download( + DownloadRequest request, + @State int state, + long startTimeMs, + long updateTimeMs, + long contentLength, + int stopReason, + @FailureReason int failureReason, + DownloadProgress progress) { + Assertions.checkNotNull(progress); + Assertions.checkArgument((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED)); + if (stopReason != 0) { + Assertions.checkArgument(state != STATE_DOWNLOADING && state != STATE_QUEUED); + } + this.request = request; + this.state = state; + this.startTimeMs = startTimeMs; + this.updateTimeMs = updateTimeMs; + this.contentLength = contentLength; + this.stopReason = stopReason; + this.failureReason = failureReason; + this.progress = progress; + } + + /** Returns whether the download is completed or failed. These are terminal states. */ + public boolean isTerminalState() { + return state == STATE_COMPLETED || state == STATE_FAILED; + } + + /** Returns the total number of downloaded bytes. */ + public long getBytesDownloaded() { + return progress.bytesDownloaded; + } + + /** + * Returns the estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is + * available. + */ + public float getPercentDownloaded() { + return progress.percentDownloaded; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadCursor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadCursor.java new file mode 100644 index 000000000000..9693e4300231 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadCursor.java @@ -0,0 +1,129 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import java.io.Closeable; + +/** Provides random read-write access to the result set returned by a database query. */ +public interface DownloadCursor extends Closeable { + + /** Returns the download at the current position. */ + Download getDownload(); + + /** Returns the numbers of downloads in the cursor. */ + int getCount(); + + /** + * Returns the current position of the cursor in the download set. The value is zero-based. When + * the download set is first returned the cursor will be at positon -1, which is before the first + * download. After the last download is returned another call to next() will leave the cursor past + * the last entry, at a position of count(). + * + * @return the current cursor position. + */ + int getPosition(); + + /** + * Move the cursor to an absolute position. The valid range of values is -1 <= position <= + * count. + * + *

This method will return true if the request destination was reachable, otherwise, it returns + * false. + * + * @param position the zero-based position to move to. + * @return whether the requested move fully succeeded. + */ + boolean moveToPosition(int position); + + /** + * Move the cursor to the first download. + * + *

This method will return false if the cursor is empty. + * + * @return whether the move succeeded. + */ + default boolean moveToFirst() { + return moveToPosition(0); + } + + /** + * Move the cursor to the last download. + * + *

This method will return false if the cursor is empty. + * + * @return whether the move succeeded. + */ + default boolean moveToLast() { + return moveToPosition(getCount() - 1); + } + + /** + * Move the cursor to the next download. + * + *

This method will return false if the cursor is already past the last entry in the result + * set. + * + * @return whether the move succeeded. + */ + default boolean moveToNext() { + return moveToPosition(getPosition() + 1); + } + + /** + * Move the cursor to the previous download. + * + *

This method will return false if the cursor is already before the first entry in the result + * set. + * + * @return whether the move succeeded. + */ + default boolean moveToPrevious() { + return moveToPosition(getPosition() - 1); + } + + /** Returns whether the cursor is pointing to the first download. */ + default boolean isFirst() { + return getPosition() == 0 && getCount() != 0; + } + + /** Returns whether the cursor is pointing to the last download. */ + default boolean isLast() { + int count = getCount(); + return getPosition() == (count - 1) && count != 0; + } + + /** Returns whether the cursor is pointing to the position before the first download. */ + default boolean isBeforeFirst() { + if (getCount() == 0) { + return true; + } + return getPosition() == -1; + } + + /** Returns whether the cursor is pointing to the position after the last download. */ + default boolean isAfterLast() { + if (getCount() == 0) { + return true; + } + return getPosition() == getCount(); + } + + /** Returns whether the cursor is closed */ + boolean isClosed(); + + @Override + void close(); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadException.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadException.java new file mode 100644 index 000000000000..cd95b5f922c9 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import java.io.IOException; + +/** Thrown on an error during downloading. */ +public final class DownloadException extends IOException { + + /** @param message The message for the exception. */ + public DownloadException(String message) { + super(message); + } + + /** @param cause The cause for the exception. */ + public DownloadException(Throwable cause) { + super(cause); + } + +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadHelper.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadHelper.java new file mode 100644 index 000000000000..6070b3a80fbd --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -0,0 +1,1174 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import android.content.Context; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.util.SparseIntArray; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RenderersFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaPeriod; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ProgressiveMediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.BaseTrackSelection; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectorResult; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource.Factory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultAllocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +/** + * A helper for initializing and removing downloads. + * + *

The helper extracts track information from the media, selects tracks for downloading, and + * creates {@link DownloadRequest download requests} based on the selected tracks. + * + *

A typical usage of DownloadHelper follows these steps: + * + *

    + *
  1. Build the helper using one of the {@code forXXX} methods. + *
  2. Prepare the helper using {@link #prepare(Callback)} and wait for the callback. + *
  3. Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link + * #getTrackSelections(int, int)}, and make adjustments using {@link + * #clearTrackSelections(int)}, {@link #replaceTrackSelections(int, Parameters)} and {@link + * #addTrackSelection(int, Parameters)}. + *
  4. Create a download request for the selected track using {@link #getDownloadRequest(byte[])}. + *
  5. Release the helper using {@link #release()}. + *
+ */ +public final class DownloadHelper { + + /** + * Default track selection parameters for downloading, but without any {@link Context} + * constraints. + * + *

If possible, use {@link #getDefaultTrackSelectorParameters(Context)} instead. + * + * @see Parameters#DEFAULT_WITHOUT_CONTEXT + */ + public static final Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT = + Parameters.DEFAULT_WITHOUT_CONTEXT.buildUpon().setForceHighestSupportedBitrate(true).build(); + + /** + * @deprecated This instance does not have {@link Context} constraints. Use {@link + * #getDefaultTrackSelectorParameters(Context)} instead. + */ + @Deprecated + public static final Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT = + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT; + + /** + * @deprecated This instance does not have {@link Context} constraints. Use {@link + * #getDefaultTrackSelectorParameters(Context)} instead. + */ + @Deprecated + public static final DefaultTrackSelector.Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS = + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT; + + /** Returns the default parameters used for track selection for downloading. */ + public static DefaultTrackSelector.Parameters getDefaultTrackSelectorParameters(Context context) { + return Parameters.getDefaults(context) + .buildUpon() + .setForceHighestSupportedBitrate(true) + .build(); + } + + /** A callback to be notified when the {@link DownloadHelper} is prepared. */ + public interface Callback { + + /** + * Called when preparation completes. + * + * @param helper The reporting {@link DownloadHelper}. + */ + void onPrepared(DownloadHelper helper); + + /** + * Called when preparation fails. + * + * @param helper The reporting {@link DownloadHelper}. + * @param e The error. + */ + void onPrepareError(DownloadHelper helper, IOException e); + } + + /** Thrown at an attempt to download live content. */ + public static class LiveContentUnsupportedException extends IOException {} + + @Nullable + private static final Constructor DASH_FACTORY_CONSTRUCTOR = + getConstructor("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory"); + + @Nullable + private static final Constructor SS_FACTORY_CONSTRUCTOR = + getConstructor("com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory"); + + @Nullable + private static final Constructor HLS_FACTORY_CONSTRUCTOR = + getConstructor("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); + + /** @deprecated Use {@link #forProgressive(Context, Uri)} */ + @Deprecated + @SuppressWarnings("deprecation") + public static DownloadHelper forProgressive(Uri uri) { + return forProgressive(uri, /* cacheKey= */ null); + } + + /** + * Creates a {@link DownloadHelper} for progressive streams. + * + * @param context Any {@link Context}. + * @param uri A stream {@link Uri}. + * @return A {@link DownloadHelper} for progressive streams. + */ + public static DownloadHelper forProgressive(Context context, Uri uri) { + return forProgressive(context, uri, /* cacheKey= */ null); + } + + /** @deprecated Use {@link #forProgressive(Context, Uri, String)} */ + @Deprecated + public static DownloadHelper forProgressive(Uri uri, @Nullable String cacheKey) { + return new DownloadHelper( + DownloadRequest.TYPE_PROGRESSIVE, + uri, + cacheKey, + /* mediaSource= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT, + /* rendererCapabilities= */ new RendererCapabilities[0]); + } + + /** + * Creates a {@link DownloadHelper} for progressive streams. + * + * @param context Any {@link Context}. + * @param uri A stream {@link Uri}. + * @param cacheKey An optional cache key. + * @return A {@link DownloadHelper} for progressive streams. + */ + public static DownloadHelper forProgressive(Context context, Uri uri, @Nullable String cacheKey) { + return new DownloadHelper( + DownloadRequest.TYPE_PROGRESSIVE, + uri, + cacheKey, + /* mediaSource= */ null, + getDefaultTrackSelectorParameters(context), + /* rendererCapabilities= */ new RendererCapabilities[0]); + } + + /** @deprecated Use {@link #forDash(Context, Uri, Factory, RenderersFactory)} */ + @Deprecated + public static DownloadHelper forDash( + Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + return forDash( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); + } + + /** + * Creates a {@link DownloadHelper} for DASH streams. + * + * @param context Any {@link Context}. + * @param uri A manifest {@link Uri}. + * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. + * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are + * selected. + * @return A {@link DownloadHelper} for DASH streams. + * @throws IllegalStateException If the DASH module is missing. + */ + public static DownloadHelper forDash( + Context context, + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory) { + return forDash( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + getDefaultTrackSelectorParameters(context)); + } + + /** + * Creates a {@link DownloadHelper} for DASH streams. + * + * @param uri A manifest {@link Uri}. + * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. + * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are + * selected. + * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which + * tracks can be selected. + * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for + * downloading. + * @return A {@link DownloadHelper} for DASH streams. + * @throws IllegalStateException If the DASH module is missing. + */ + public static DownloadHelper forDash( + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory, + @Nullable DrmSessionManager drmSessionManager, + DefaultTrackSelector.Parameters trackSelectorParameters) { + return new DownloadHelper( + DownloadRequest.TYPE_DASH, + uri, + /* cacheKey= */ null, + createMediaSourceInternal( + DASH_FACTORY_CONSTRUCTOR, + uri, + dataSourceFactory, + drmSessionManager, + /* streamKeys= */ null), + trackSelectorParameters, + Util.getRendererCapabilities(renderersFactory)); + } + + /** @deprecated Use {@link #forHls(Context, Uri, Factory, RenderersFactory)} */ + @Deprecated + public static DownloadHelper forHls( + Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + return forHls( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); + } + + /** + * Creates a {@link DownloadHelper} for HLS streams. + * + * @param context Any {@link Context}. + * @param uri A playlist {@link Uri}. + * @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist. + * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are + * selected. + * @return A {@link DownloadHelper} for HLS streams. + * @throws IllegalStateException If the HLS module is missing. + */ + public static DownloadHelper forHls( + Context context, + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory) { + return forHls( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + getDefaultTrackSelectorParameters(context)); + } + + /** + * Creates a {@link DownloadHelper} for HLS streams. + * + * @param uri A playlist {@link Uri}. + * @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist. + * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are + * selected. + * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which + * tracks can be selected. + * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for + * downloading. + * @return A {@link DownloadHelper} for HLS streams. + * @throws IllegalStateException If the HLS module is missing. + */ + public static DownloadHelper forHls( + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory, + @Nullable DrmSessionManager drmSessionManager, + DefaultTrackSelector.Parameters trackSelectorParameters) { + return new DownloadHelper( + DownloadRequest.TYPE_HLS, + uri, + /* cacheKey= */ null, + createMediaSourceInternal( + HLS_FACTORY_CONSTRUCTOR, + uri, + dataSourceFactory, + drmSessionManager, + /* streamKeys= */ null), + trackSelectorParameters, + Util.getRendererCapabilities(renderersFactory)); + } + + /** @deprecated Use {@link #forSmoothStreaming(Context, Uri, Factory, RenderersFactory)} */ + @Deprecated + public static DownloadHelper forSmoothStreaming( + Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + return forSmoothStreaming( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); + } + + /** + * Creates a {@link DownloadHelper} for SmoothStreaming streams. + * + * @param context Any {@link Context}. + * @param uri A manifest {@link Uri}. + * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. + * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are + * selected. + * @return A {@link DownloadHelper} for SmoothStreaming streams. + * @throws IllegalStateException If the SmoothStreaming module is missing. + */ + public static DownloadHelper forSmoothStreaming( + Context context, + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory) { + return forSmoothStreaming( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + getDefaultTrackSelectorParameters(context)); + } + + /** + * Creates a {@link DownloadHelper} for SmoothStreaming streams. + * + * @param uri A manifest {@link Uri}. + * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. + * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are + * selected. + * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which + * tracks can be selected. + * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for + * downloading. + * @return A {@link DownloadHelper} for SmoothStreaming streams. + * @throws IllegalStateException If the SmoothStreaming module is missing. + */ + public static DownloadHelper forSmoothStreaming( + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory, + @Nullable DrmSessionManager drmSessionManager, + DefaultTrackSelector.Parameters trackSelectorParameters) { + return new DownloadHelper( + DownloadRequest.TYPE_SS, + uri, + /* cacheKey= */ null, + createMediaSourceInternal( + SS_FACTORY_CONSTRUCTOR, + uri, + dataSourceFactory, + drmSessionManager, + /* streamKeys= */ null), + trackSelectorParameters, + Util.getRendererCapabilities(renderersFactory)); + } + + /** + * Equivalent to {@link #createMediaSource(DownloadRequest, Factory, DrmSessionManager) + * createMediaSource(downloadRequest, dataSourceFactory, null)}. + */ + public static MediaSource createMediaSource( + DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) { + return createMediaSource(downloadRequest, dataSourceFactory, /* drmSessionManager= */ null); + } + + /** + * Utility method to create a {@link MediaSource} that only exposes the tracks defined in {@code + * downloadRequest}. + * + * @param downloadRequest A {@link DownloadRequest}. + * @param dataSourceFactory A factory for {@link DataSource}s to read the media. + * @param drmSessionManager An optional {@link DrmSessionManager} to be passed to the {@link + * MediaSource}. + * @return A {@link MediaSource} that only exposes the tracks defined in {@code downloadRequest}. + */ + public static MediaSource createMediaSource( + DownloadRequest downloadRequest, + DataSource.Factory dataSourceFactory, + @Nullable DrmSessionManager drmSessionManager) { + @Nullable Constructor constructor; + switch (downloadRequest.type) { + case DownloadRequest.TYPE_DASH: + constructor = DASH_FACTORY_CONSTRUCTOR; + break; + case DownloadRequest.TYPE_SS: + constructor = SS_FACTORY_CONSTRUCTOR; + break; + case DownloadRequest.TYPE_HLS: + constructor = HLS_FACTORY_CONSTRUCTOR; + break; + case DownloadRequest.TYPE_PROGRESSIVE: + return new ProgressiveMediaSource.Factory(dataSourceFactory) + .setCustomCacheKey(downloadRequest.customCacheKey) + .createMediaSource(downloadRequest.uri); + default: + throw new IllegalStateException("Unsupported type: " + downloadRequest.type); + } + return createMediaSourceInternal( + constructor, + downloadRequest.uri, + dataSourceFactory, + drmSessionManager, + downloadRequest.streamKeys); + } + + private final String downloadType; + private final Uri uri; + @Nullable private final String cacheKey; + @Nullable private final MediaSource mediaSource; + private final DefaultTrackSelector trackSelector; + private final RendererCapabilities[] rendererCapabilities; + private final SparseIntArray scratchSet; + private final Handler callbackHandler; + private final Timeline.Window window; + + private boolean isPreparedWithMedia; + private @MonotonicNonNull Callback callback; + private @MonotonicNonNull MediaPreparer mediaPreparer; + private TrackGroupArray @MonotonicNonNull [] trackGroupArrays; + private MappedTrackInfo @MonotonicNonNull [] mappedTrackInfos; + private List @MonotonicNonNull [][] trackSelectionsByPeriodAndRenderer; + private List @MonotonicNonNull [][] immutableTrackSelectionsByPeriodAndRenderer; + + /** + * Creates download helper. + * + * @param downloadType A download type. This value will be used as {@link DownloadRequest#type}. + * @param uri A {@link Uri}. + * @param cacheKey An optional cache key. + * @param mediaSource A {@link MediaSource} for which tracks are selected, or null if no track + * selection needs to be made. + * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for + * downloading. + * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks + * are selected. + */ + public DownloadHelper( + String downloadType, + Uri uri, + @Nullable String cacheKey, + @Nullable MediaSource mediaSource, + DefaultTrackSelector.Parameters trackSelectorParameters, + RendererCapabilities[] rendererCapabilities) { + this.downloadType = downloadType; + this.uri = uri; + this.cacheKey = cacheKey; + this.mediaSource = mediaSource; + this.trackSelector = + new DefaultTrackSelector(trackSelectorParameters, new DownloadTrackSelection.Factory()); + this.rendererCapabilities = rendererCapabilities; + this.scratchSet = new SparseIntArray(); + trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter()); + callbackHandler = new Handler(Util.getLooper()); + window = new Timeline.Window(); + } + + /** + * Initializes the helper for starting a download. + * + * @param callback A callback to be notified when preparation completes or fails. + * @throws IllegalStateException If the download helper has already been prepared. + */ + public void prepare(Callback callback) { + Assertions.checkState(this.callback == null); + this.callback = callback; + if (mediaSource != null) { + mediaPreparer = new MediaPreparer(mediaSource, /* downloadHelper= */ this); + } else { + callbackHandler.post(() -> callback.onPrepared(this)); + } + } + + /** Releases the helper and all resources it is holding. */ + public void release() { + if (mediaPreparer != null) { + mediaPreparer.release(); + } + } + + /** + * Returns the manifest, or null if no manifest is loaded. Must not be called until after + * preparation completes. + */ + @Nullable + public Object getManifest() { + if (mediaSource == null) { + return null; + } + assertPreparedWithMedia(); + return mediaPreparer.timeline.getWindowCount() > 0 + ? mediaPreparer.timeline.getWindow(/* windowIndex= */ 0, window).manifest + : null; + } + + /** + * Returns the number of periods for which media is available. Must not be called until after + * preparation completes. + */ + public int getPeriodCount() { + if (mediaSource == null) { + return 0; + } + assertPreparedWithMedia(); + return trackGroupArrays.length; + } + + /** + * Returns the track groups for the given period. Must not be called until after preparation + * completes. + * + *

Use {@link #getMappedTrackInfo(int)} to get the track groups mapped to renderers. + * + * @param periodIndex The period index. + * @return The track groups for the period. May be {@link TrackGroupArray#EMPTY} for single stream + * content. + */ + public TrackGroupArray getTrackGroups(int periodIndex) { + assertPreparedWithMedia(); + return trackGroupArrays[periodIndex]; + } + + /** + * Returns the mapped track info for the given period. Must not be called until after preparation + * completes. + * + * @param periodIndex The period index. + * @return The {@link MappedTrackInfo} for the period. + */ + public MappedTrackInfo getMappedTrackInfo(int periodIndex) { + assertPreparedWithMedia(); + return mappedTrackInfos[periodIndex]; + } + + /** + * Returns all {@link TrackSelection track selections} for a period and renderer. Must not be + * called until after preparation completes. + * + * @param periodIndex The period index. + * @param rendererIndex The renderer index. + * @return A list of selected {@link TrackSelection track selections}. + */ + public List getTrackSelections(int periodIndex, int rendererIndex) { + assertPreparedWithMedia(); + return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]; + } + + /** + * Clears the selection of tracks for a period. Must not be called until after preparation + * completes. + * + * @param periodIndex The period index for which track selections are cleared. + */ + public void clearTrackSelections(int periodIndex) { + assertPreparedWithMedia(); + for (int i = 0; i < rendererCapabilities.length; i++) { + trackSelectionsByPeriodAndRenderer[periodIndex][i].clear(); + } + } + + /** + * Replaces a selection of tracks to be downloaded. Must not be called until after preparation + * completes. + * + * @param periodIndex The period index for which the track selection is replaced. + * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new + * selection of tracks. + */ + public void replaceTrackSelections( + int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) { + clearTrackSelections(periodIndex); + addTrackSelection(periodIndex, trackSelectorParameters); + } + + /** + * Adds a selection of tracks to be downloaded. Must not be called until after preparation + * completes. + * + * @param periodIndex The period index this track selection is added for. + * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new + * selection of tracks. + */ + public void addTrackSelection( + int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) { + assertPreparedWithMedia(); + trackSelector.setParameters(trackSelectorParameters); + runTrackSelection(periodIndex); + } + + /** + * Convenience method to add selections of tracks for all specified audio languages. If an audio + * track in one of the specified languages is not available, the default fallback audio track is + * used instead. Must not be called until after preparation completes. + * + * @param languages A list of audio languages for which tracks should be added to the download + * selection, as IETF BCP 47 conformant tags. + */ + public void addAudioLanguagesToSelection(String... languages) { + assertPreparedWithMedia(); + for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) { + DefaultTrackSelector.ParametersBuilder parametersBuilder = + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon(); + MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex]; + int rendererCount = mappedTrackInfo.getRendererCount(); + for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { + if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_AUDIO) { + parametersBuilder.setRendererDisabled(rendererIndex, /* disabled= */ true); + } + } + for (String language : languages) { + parametersBuilder.setPreferredAudioLanguage(language); + addTrackSelection(periodIndex, parametersBuilder.build()); + } + } + } + + /** + * Convenience method to add selections of tracks for all specified text languages. Must not be + * called until after preparation completes. + * + * @param selectUndeterminedTextLanguage Whether a text track with undetermined language should be + * selected for downloading if no track with one of the specified {@code languages} is + * available. + * @param languages A list of text languages for which tracks should be added to the download + * selection, as IETF BCP 47 conformant tags. + */ + public void addTextLanguagesToSelection( + boolean selectUndeterminedTextLanguage, String... languages) { + assertPreparedWithMedia(); + for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) { + DefaultTrackSelector.ParametersBuilder parametersBuilder = + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon(); + MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex]; + int rendererCount = mappedTrackInfo.getRendererCount(); + for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { + if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_TEXT) { + parametersBuilder.setRendererDisabled(rendererIndex, /* disabled= */ true); + } + } + parametersBuilder.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage); + for (String language : languages) { + parametersBuilder.setPreferredTextLanguage(language); + addTrackSelection(periodIndex, parametersBuilder.build()); + } + } + } + + /** + * Convenience method to add a selection of tracks to be downloaded for a single renderer. Must + * not be called until after preparation completes. + * + * @param periodIndex The period index the track selection is added for. + * @param rendererIndex The renderer index the track selection is added for. + * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new + * selection of tracks. + * @param overrides A list of {@link SelectionOverride SelectionOverrides} to apply to the {@code + * trackSelectorParameters}. If empty, {@code trackSelectorParameters} are used as they are. + */ + public void addTrackSelectionForSingleRenderer( + int periodIndex, + int rendererIndex, + DefaultTrackSelector.Parameters trackSelectorParameters, + List overrides) { + assertPreparedWithMedia(); + DefaultTrackSelector.ParametersBuilder builder = trackSelectorParameters.buildUpon(); + for (int i = 0; i < mappedTrackInfos[periodIndex].getRendererCount(); i++) { + builder.setRendererDisabled(/* rendererIndex= */ i, /* disabled= */ i != rendererIndex); + } + if (overrides.isEmpty()) { + addTrackSelection(periodIndex, builder.build()); + } else { + TrackGroupArray trackGroupArray = mappedTrackInfos[periodIndex].getTrackGroups(rendererIndex); + for (int i = 0; i < overrides.size(); i++) { + builder.setSelectionOverride(rendererIndex, trackGroupArray, overrides.get(i)); + addTrackSelection(periodIndex, builder.build()); + } + } + } + + /** + * Builds a {@link DownloadRequest} for downloading the selected tracks. Must not be called until + * after preparation completes. The uri of the {@link DownloadRequest} will be used as content id. + * + * @param data Application provided data to store in {@link DownloadRequest#data}. + * @return The built {@link DownloadRequest}. + */ + public DownloadRequest getDownloadRequest(@Nullable byte[] data) { + return getDownloadRequest(uri.toString(), data); + } + + /** + * Builds a {@link DownloadRequest} for downloading the selected tracks. Must not be called until + * after preparation completes. + * + * @param id The unique content id. + * @param data Application provided data to store in {@link DownloadRequest#data}. + * @return The built {@link DownloadRequest}. + */ + public DownloadRequest getDownloadRequest(String id, @Nullable byte[] data) { + if (mediaSource == null) { + return new DownloadRequest( + id, downloadType, uri, /* streamKeys= */ Collections.emptyList(), cacheKey, data); + } + assertPreparedWithMedia(); + List streamKeys = new ArrayList<>(); + List allSelections = new ArrayList<>(); + int periodCount = trackSelectionsByPeriodAndRenderer.length; + for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) { + allSelections.clear(); + int rendererCount = trackSelectionsByPeriodAndRenderer[periodIndex].length; + for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { + allSelections.addAll(trackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]); + } + streamKeys.addAll(mediaPreparer.mediaPeriods[periodIndex].getStreamKeys(allSelections)); + } + return new DownloadRequest(id, downloadType, uri, streamKeys, cacheKey, data); + } + + // Initialization of array of Lists. + @SuppressWarnings("unchecked") + private void onMediaPrepared() { + Assertions.checkNotNull(mediaPreparer); + Assertions.checkNotNull(mediaPreparer.mediaPeriods); + Assertions.checkNotNull(mediaPreparer.timeline); + int periodCount = mediaPreparer.mediaPeriods.length; + int rendererCount = rendererCapabilities.length; + trackSelectionsByPeriodAndRenderer = + (List[][]) new List[periodCount][rendererCount]; + immutableTrackSelectionsByPeriodAndRenderer = + (List[][]) new List[periodCount][rendererCount]; + for (int i = 0; i < periodCount; i++) { + for (int j = 0; j < rendererCount; j++) { + trackSelectionsByPeriodAndRenderer[i][j] = new ArrayList<>(); + immutableTrackSelectionsByPeriodAndRenderer[i][j] = + Collections.unmodifiableList(trackSelectionsByPeriodAndRenderer[i][j]); + } + } + trackGroupArrays = new TrackGroupArray[periodCount]; + mappedTrackInfos = new MappedTrackInfo[periodCount]; + for (int i = 0; i < periodCount; i++) { + trackGroupArrays[i] = mediaPreparer.mediaPeriods[i].getTrackGroups(); + TrackSelectorResult trackSelectorResult = runTrackSelection(/* periodIndex= */ i); + trackSelector.onSelectionActivated(trackSelectorResult.info); + mappedTrackInfos[i] = Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); + } + setPreparedWithMedia(); + Assertions.checkNotNull(callbackHandler) + .post(() -> Assertions.checkNotNull(callback).onPrepared(this)); + } + + private void onMediaPreparationFailed(IOException error) { + Assertions.checkNotNull(callbackHandler) + .post(() -> Assertions.checkNotNull(callback).onPrepareError(this, error)); + } + + @RequiresNonNull({ + "trackGroupArrays", + "mappedTrackInfos", + "trackSelectionsByPeriodAndRenderer", + "immutableTrackSelectionsByPeriodAndRenderer", + "mediaPreparer", + "mediaPreparer.timeline", + "mediaPreparer.mediaPeriods" + }) + private void setPreparedWithMedia() { + isPreparedWithMedia = true; + } + + @EnsuresNonNull({ + "trackGroupArrays", + "mappedTrackInfos", + "trackSelectionsByPeriodAndRenderer", + "immutableTrackSelectionsByPeriodAndRenderer", + "mediaPreparer", + "mediaPreparer.timeline", + "mediaPreparer.mediaPeriods" + }) + @SuppressWarnings("nullness:contracts.postcondition.not.satisfied") + private void assertPreparedWithMedia() { + Assertions.checkState(isPreparedWithMedia); + } + + /** + * Runs the track selection for a given period index with the current parameters. The selected + * tracks will be added to {@link #trackSelectionsByPeriodAndRenderer}. + */ + // Intentional reference comparison of track group instances. + @SuppressWarnings("ReferenceEquality") + @RequiresNonNull({ + "trackGroupArrays", + "trackSelectionsByPeriodAndRenderer", + "mediaPreparer", + "mediaPreparer.timeline" + }) + private TrackSelectorResult runTrackSelection(int periodIndex) { + try { + TrackSelectorResult trackSelectorResult = + trackSelector.selectTracks( + rendererCapabilities, + trackGroupArrays[periodIndex], + new MediaPeriodId(mediaPreparer.timeline.getUidOfPeriod(periodIndex)), + mediaPreparer.timeline); + for (int i = 0; i < trackSelectorResult.length; i++) { + @Nullable TrackSelection newSelection = trackSelectorResult.selections.get(i); + if (newSelection == null) { + continue; + } + List existingSelectionList = + trackSelectionsByPeriodAndRenderer[periodIndex][i]; + boolean mergedWithExistingSelection = false; + for (int j = 0; j < existingSelectionList.size(); j++) { + TrackSelection existingSelection = existingSelectionList.get(j); + if (existingSelection.getTrackGroup() == newSelection.getTrackGroup()) { + // Merge with existing selection. + scratchSet.clear(); + for (int k = 0; k < existingSelection.length(); k++) { + scratchSet.put(existingSelection.getIndexInTrackGroup(k), 0); + } + for (int k = 0; k < newSelection.length(); k++) { + scratchSet.put(newSelection.getIndexInTrackGroup(k), 0); + } + int[] mergedTracks = new int[scratchSet.size()]; + for (int k = 0; k < scratchSet.size(); k++) { + mergedTracks[k] = scratchSet.keyAt(k); + } + existingSelectionList.set( + j, new DownloadTrackSelection(existingSelection.getTrackGroup(), mergedTracks)); + mergedWithExistingSelection = true; + break; + } + } + if (!mergedWithExistingSelection) { + existingSelectionList.add(newSelection); + } + } + return trackSelectorResult; + } catch (ExoPlaybackException e) { + // DefaultTrackSelector does not throw exceptions during track selection. + throw new UnsupportedOperationException(e); + } + } + + @Nullable + private static Constructor getConstructor(String className) { + try { + // LINT.IfChange + Class factoryClazz = + Class.forName(className).asSubclass(MediaSourceFactory.class); + return factoryClazz.getConstructor(Factory.class); + // LINT.ThenChange(../../../../../../../../proguard-rules.txt) + } catch (ClassNotFoundException e) { + // Expected if the app was built without the respective module. + return null; + } catch (NoSuchMethodException e) { + // Something is wrong with the library or the proguard configuration. + throw new IllegalStateException(e); + } + } + + private static MediaSource createMediaSourceInternal( + @Nullable Constructor constructor, + Uri uri, + Factory dataSourceFactory, + @Nullable DrmSessionManager drmSessionManager, + @Nullable List streamKeys) { + if (constructor == null) { + throw new IllegalStateException("Module missing to create media source."); + } + try { + MediaSourceFactory factory = constructor.newInstance(dataSourceFactory); + if (drmSessionManager != null) { + factory.setDrmSessionManager(drmSessionManager); + } + if (streamKeys != null) { + factory.setStreamKeys(streamKeys); + } + return Assertions.checkNotNull(factory.createMediaSource(uri)); + } catch (Exception e) { + throw new IllegalStateException("Failed to instantiate media source.", e); + } + } + + private static final class MediaPreparer + implements MediaSourceCaller, MediaPeriod.Callback, Handler.Callback { + + private static final int MESSAGE_PREPARE_SOURCE = 0; + private static final int MESSAGE_CHECK_FOR_FAILURE = 1; + private static final int MESSAGE_CONTINUE_LOADING = 2; + private static final int MESSAGE_RELEASE = 3; + + private static final int DOWNLOAD_HELPER_CALLBACK_MESSAGE_PREPARED = 0; + private static final int DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED = 1; + + private final MediaSource mediaSource; + private final DownloadHelper downloadHelper; + private final Allocator allocator; + private final ArrayList pendingMediaPeriods; + private final Handler downloadHelperHandler; + private final HandlerThread mediaSourceThread; + private final Handler mediaSourceHandler; + + public @MonotonicNonNull Timeline timeline; + public MediaPeriod @MonotonicNonNull [] mediaPeriods; + + private boolean released; + + public MediaPreparer(MediaSource mediaSource, DownloadHelper downloadHelper) { + this.mediaSource = mediaSource; + this.downloadHelper = downloadHelper; + allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); + pendingMediaPeriods = new ArrayList<>(); + @SuppressWarnings("methodref.receiver.bound.invalid") + Handler downloadThreadHandler = Util.createHandler(this::handleDownloadHelperCallbackMessage); + this.downloadHelperHandler = downloadThreadHandler; + mediaSourceThread = new HandlerThread("DownloadHelper"); + mediaSourceThread.start(); + mediaSourceHandler = Util.createHandler(mediaSourceThread.getLooper(), /* callback= */ this); + mediaSourceHandler.sendEmptyMessage(MESSAGE_PREPARE_SOURCE); + } + + public void release() { + if (released) { + return; + } + released = true; + mediaSourceHandler.sendEmptyMessage(MESSAGE_RELEASE); + } + + // Handler.Callback + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_PREPARE_SOURCE: + mediaSource.prepareSource(/* caller= */ this, /* mediaTransferListener= */ null); + mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE); + return true; + case MESSAGE_CHECK_FOR_FAILURE: + try { + if (mediaPeriods == null) { + mediaSource.maybeThrowSourceInfoRefreshError(); + } else { + for (int i = 0; i < pendingMediaPeriods.size(); i++) { + pendingMediaPeriods.get(i).maybeThrowPrepareError(); + } + } + mediaSourceHandler.sendEmptyMessageDelayed( + MESSAGE_CHECK_FOR_FAILURE, /* delayMillis= */ 100); + } catch (IOException e) { + downloadHelperHandler + .obtainMessage(DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED, /* obj= */ e) + .sendToTarget(); + } + return true; + case MESSAGE_CONTINUE_LOADING: + MediaPeriod mediaPeriod = (MediaPeriod) msg.obj; + if (pendingMediaPeriods.contains(mediaPeriod)) { + mediaPeriod.continueLoading(/* positionUs= */ 0); + } + return true; + case MESSAGE_RELEASE: + if (mediaPeriods != null) { + for (MediaPeriod period : mediaPeriods) { + mediaSource.releasePeriod(period); + } + } + mediaSource.releaseSource(this); + mediaSourceHandler.removeCallbacksAndMessages(null); + mediaSourceThread.quit(); + return true; + default: + return false; + } + } + + // MediaSource.MediaSourceCaller implementation. + + @Override + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { + if (this.timeline != null) { + // Ignore dynamic updates. + return; + } + if (timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).isLive) { + downloadHelperHandler + .obtainMessage( + DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED, + /* obj= */ new LiveContentUnsupportedException()) + .sendToTarget(); + return; + } + this.timeline = timeline; + mediaPeriods = new MediaPeriod[timeline.getPeriodCount()]; + for (int i = 0; i < mediaPeriods.length; i++) { + MediaPeriod mediaPeriod = + mediaSource.createPeriod( + new MediaPeriodId(timeline.getUidOfPeriod(/* periodIndex= */ i)), + allocator, + /* startPositionUs= */ 0); + mediaPeriods[i] = mediaPeriod; + pendingMediaPeriods.add(mediaPeriod); + } + for (MediaPeriod mediaPeriod : mediaPeriods) { + mediaPeriod.prepare(/* callback= */ this, /* positionUs= */ 0); + } + } + + // MediaPeriod.Callback implementation. + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + pendingMediaPeriods.remove(mediaPeriod); + if (pendingMediaPeriods.isEmpty()) { + mediaSourceHandler.removeMessages(MESSAGE_CHECK_FOR_FAILURE); + downloadHelperHandler.sendEmptyMessage(DOWNLOAD_HELPER_CALLBACK_MESSAGE_PREPARED); + } + } + + @Override + public void onContinueLoadingRequested(MediaPeriod mediaPeriod) { + if (pendingMediaPeriods.contains(mediaPeriod)) { + mediaSourceHandler.obtainMessage(MESSAGE_CONTINUE_LOADING, mediaPeriod).sendToTarget(); + } + } + + private boolean handleDownloadHelperCallbackMessage(Message msg) { + if (released) { + // Stale message. + return false; + } + switch (msg.what) { + case DOWNLOAD_HELPER_CALLBACK_MESSAGE_PREPARED: + downloadHelper.onMediaPrepared(); + return true; + case DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED: + release(); + downloadHelper.onMediaPreparationFailed((IOException) Util.castNonNull(msg.obj)); + return true; + default: + return false; + } + } + } + + private static final class DownloadTrackSelection extends BaseTrackSelection { + + private static final class Factory implements TrackSelection.Factory { + + @Override + public @NullableType TrackSelection[] createTrackSelections( + @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { + @NullableType TrackSelection[] selections = new TrackSelection[definitions.length]; + for (int i = 0; i < definitions.length; i++) { + selections[i] = + definitions[i] == null + ? null + : new DownloadTrackSelection(definitions[i].group, definitions[i].tracks); + } + return selections; + } + } + + public DownloadTrackSelection(TrackGroup trackGroup, int[] tracks) { + super(trackGroup, tracks); + } + + @Override + public int getSelectedIndex() { + return 0; + } + + @Override + public int getSelectionReason() { + return C.SELECTION_REASON_UNKNOWN; + } + + @Nullable + @Override + public Object getSelectionData() { + return null; + } + + @Override + public void updateSelectedTrack( + long playbackPositionUs, + long bufferedDurationUs, + long availableDurationUs, + List queue, + MediaChunkIterator[] mediaChunkIterators) { + // Do nothing. + } + } + + private static final class DummyBandwidthMeter implements BandwidthMeter { + + @Override + public long getBitrateEstimate() { + return 0; + } + + @Nullable + @Override + public TransferListener getTransferListener() { + return null; + } + + @Override + public void addEventListener(Handler eventHandler, EventListener eventListener) { + // Do nothing. + } + + @Override + public void removeEventListener(EventListener eventListener) { + // Do nothing. + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadIndex.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadIndex.java new file mode 100644 index 000000000000..5fbb3e7c0b8c --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadIndex.java @@ -0,0 +1,49 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +import java.io.IOException; + +/** An index of {@link Download Downloads}. */ +@WorkerThread +public interface DownloadIndex { + + /** + * Returns the {@link Download} with the given {@code id}, or null. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @param id ID of a {@link Download}. + * @return The {@link Download} with the given {@code id}, or null if a download state with this + * id doesn't exist. + * @throws IOException If an error occurs reading the state. + */ + @Nullable + Download getDownload(String id) throws IOException; + + /** + * Returns a {@link DownloadCursor} to {@link Download}s with the given {@code states}. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @param states Returns only the {@link Download}s with this states. If empty, returns all. + * @return A cursor to {@link Download}s with the given {@code states}. + * @throws IOException If an error occurs reading the state. + */ + DownloadCursor getDownloads(@Download.State int... states) throws IOException; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadManager.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadManager.java new file mode 100644 index 000000000000..a6ace1234381 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadManager.java @@ -0,0 +1,1346 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.FAILURE_REASON_NONE; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.FAILURE_REASON_UNKNOWN; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STATE_COMPLETED; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STATE_DOWNLOADING; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STATE_FAILED; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STATE_QUEUED; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STATE_REMOVING; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STATE_RESTARTING; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STATE_STOPPED; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STOP_REASON_NONE; + +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import androidx.annotation.CheckResult; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseProvider; +import org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler.Requirements; +import org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler.RequirementsWatcher; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource.Factory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.Cache; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.CacheEvictor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Manages downloads. + * + *

Normally a download manager should be accessed via a {@link DownloadService}. When a download + * manager is used directly instead, downloads will be initially paused and so must be resumed by + * calling {@link #resumeDownloads()}. + * + *

A download manager instance must be accessed only from the thread that created it, unless that + * thread does not have a {@link Looper}. In that case, it must be accessed only from the + * application's main thread. Registered listeners will be called on the same thread. + */ +public final class DownloadManager { + + /** Listener for {@link DownloadManager} events. */ + public interface Listener { + + /** + * Called when all downloads have been restored. + * + * @param downloadManager The reporting instance. + */ + default void onInitialized(DownloadManager downloadManager) {} + + /** + * Called when downloads are ({@link #pauseDownloads() paused} or {@link #resumeDownloads() + * resumed}. + * + * @param downloadManager The reporting instance. + * @param downloadsPaused Whether downloads are currently paused. + */ + default void onDownloadsPausedChanged( + DownloadManager downloadManager, boolean downloadsPaused) {} + + /** + * Called when the state of a download changes. + * + * @param downloadManager The reporting instance. + * @param download The state of the download. + */ + default void onDownloadChanged(DownloadManager downloadManager, Download download) {} + + /** + * Called when a download is removed. + * + * @param downloadManager The reporting instance. + * @param download The last state of the download before it was removed. + */ + default void onDownloadRemoved(DownloadManager downloadManager, Download download) {} + + /** + * Called when there is no active download left. + * + * @param downloadManager The reporting instance. + */ + default void onIdle(DownloadManager downloadManager) {} + + /** + * Called when the download requirements state changed. + * + * @param downloadManager The reporting instance. + * @param requirements Requirements needed to be met to start downloads. + * @param notMetRequirements {@link Requirements.RequirementFlags RequirementFlags} that are not + * met, or 0. + */ + default void onRequirementsStateChanged( + DownloadManager downloadManager, + Requirements requirements, + @Requirements.RequirementFlags int notMetRequirements) {} + + /** + * Called when there is a change in whether this manager has one or more downloads that are not + * progressing for the sole reason that the {@link #getRequirements() Requirements} are not met. + * See {@link #isWaitingForRequirements()} for more information. + * + * @param downloadManager The reporting instance. + * @param waitingForRequirements Whether this manager has one or more downloads that are not + * progressing for the sole reason that the {@link #getRequirements() Requirements} are not + * met. + */ + default void onWaitingForRequirementsChanged( + DownloadManager downloadManager, boolean waitingForRequirements) {} + } + + /** The default maximum number of parallel downloads. */ + public static final int DEFAULT_MAX_PARALLEL_DOWNLOADS = 3; + /** The default minimum number of times a download must be retried before failing. */ + public static final int DEFAULT_MIN_RETRY_COUNT = 5; + /** The default requirement is that the device has network connectivity. */ + public static final Requirements DEFAULT_REQUIREMENTS = new Requirements(Requirements.NETWORK); + + // Messages posted to the main handler. + private static final int MSG_INITIALIZED = 0; + private static final int MSG_PROCESSED = 1; + private static final int MSG_DOWNLOAD_UPDATE = 2; + + // Messages posted to the background handler. + private static final int MSG_INITIALIZE = 0; + private static final int MSG_SET_DOWNLOADS_PAUSED = 1; + private static final int MSG_SET_NOT_MET_REQUIREMENTS = 2; + private static final int MSG_SET_STOP_REASON = 3; + private static final int MSG_SET_MAX_PARALLEL_DOWNLOADS = 4; + private static final int MSG_SET_MIN_RETRY_COUNT = 5; + private static final int MSG_ADD_DOWNLOAD = 6; + private static final int MSG_REMOVE_DOWNLOAD = 7; + private static final int MSG_REMOVE_ALL_DOWNLOADS = 8; + private static final int MSG_TASK_STOPPED = 9; + private static final int MSG_CONTENT_LENGTH_CHANGED = 10; + private static final int MSG_UPDATE_PROGRESS = 11; + private static final int MSG_RELEASE = 12; + + private static final String TAG = "DownloadManager"; + + private final Context context; + private final WritableDownloadIndex downloadIndex; + private final Handler mainHandler; + private final InternalHandler internalHandler; + private final RequirementsWatcher.Listener requirementsListener; + private final CopyOnWriteArraySet listeners; + + private int pendingMessages; + private int activeTaskCount; + private boolean initialized; + private boolean downloadsPaused; + private int maxParallelDownloads; + private int minRetryCount; + private int notMetRequirements; + private boolean waitingForRequirements; + private List downloads; + private RequirementsWatcher requirementsWatcher; + + /** + * Constructs a {@link DownloadManager}. + * + * @param context Any context. + * @param databaseProvider Provides the SQLite database in which downloads are persisted. + * @param cache A cache to be used to store downloaded data. The cache should be configured with + * an {@link CacheEvictor} that will not evict downloaded content, for example {@link + * NoOpCacheEvictor}. + * @param upstreamFactory A {@link Factory} for creating {@link DataSource}s for downloading data. + */ + public DownloadManager( + Context context, DatabaseProvider databaseProvider, Cache cache, Factory upstreamFactory) { + this( + context, + new DefaultDownloadIndex(databaseProvider), + new DefaultDownloaderFactory(new DownloaderConstructorHelper(cache, upstreamFactory))); + } + + /** + * Constructs a {@link DownloadManager}. + * + * @param context Any context. + * @param downloadIndex The download index used to hold the download information. + * @param downloaderFactory A factory for creating {@link Downloader}s. + */ + public DownloadManager( + Context context, WritableDownloadIndex downloadIndex, DownloaderFactory downloaderFactory) { + this.context = context.getApplicationContext(); + this.downloadIndex = downloadIndex; + + maxParallelDownloads = DEFAULT_MAX_PARALLEL_DOWNLOADS; + minRetryCount = DEFAULT_MIN_RETRY_COUNT; + downloadsPaused = true; + downloads = Collections.emptyList(); + listeners = new CopyOnWriteArraySet<>(); + + @SuppressWarnings("methodref.receiver.bound.invalid") + Handler mainHandler = Util.createHandler(this::handleMainMessage); + this.mainHandler = mainHandler; + HandlerThread internalThread = new HandlerThread("DownloadManager file i/o"); + internalThread.start(); + internalHandler = + new InternalHandler( + internalThread, + downloadIndex, + downloaderFactory, + mainHandler, + maxParallelDownloads, + minRetryCount, + downloadsPaused); + + @SuppressWarnings("methodref.receiver.bound.invalid") + RequirementsWatcher.Listener requirementsListener = this::onRequirementsStateChanged; + this.requirementsListener = requirementsListener; + requirementsWatcher = + new RequirementsWatcher(context, requirementsListener, DEFAULT_REQUIREMENTS); + notMetRequirements = requirementsWatcher.start(); + + pendingMessages = 1; + internalHandler + .obtainMessage(MSG_INITIALIZE, notMetRequirements, /* unused */ 0) + .sendToTarget(); + } + + /** Returns whether the manager has completed initialization. */ + public boolean isInitialized() { + return initialized; + } + + /** + * Returns whether the manager is currently idle. The manager is idle if all downloads are in a + * terminal state (i.e. completed or failed), or if no progress can be made (e.g. because the + * download requirements are not met). + */ + public boolean isIdle() { + return activeTaskCount == 0 && pendingMessages == 0; + } + + /** + * Returns whether this manager has one or more downloads that are not progressing for the sole + * reason that the {@link #getRequirements() Requirements} are not met. This is true if: + * + *

    + *
  • The {@link #getRequirements() Requirements} are not met. + *
  • The downloads are not paused (i.e. {@link #getDownloadsPaused()} is {@code false}). + *
  • There are downloads in the {@link Download#STATE_QUEUED queued state}. + *
+ */ + public boolean isWaitingForRequirements() { + return waitingForRequirements; + } + + /** + * Adds a {@link Listener}. + * + * @param listener The listener to be added. + */ + public void addListener(Listener listener) { + listeners.add(listener); + } + + /** + * Removes a {@link Listener}. + * + * @param listener The listener to be removed. + */ + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + /** Returns the requirements needed to be met to progress. */ + public Requirements getRequirements() { + return requirementsWatcher.getRequirements(); + } + + /** + * Returns the requirements needed for downloads to progress that are not currently met. + * + * @return The not met {@link Requirements.RequirementFlags}, or 0 if all requirements are met. + */ + @Requirements.RequirementFlags + public int getNotMetRequirements() { + return notMetRequirements; + } + + /** + * Sets the requirements that need to be met for downloads to progress. + * + * @param requirements A {@link Requirements}. + */ + public void setRequirements(Requirements requirements) { + if (requirements.equals(requirementsWatcher.getRequirements())) { + return; + } + requirementsWatcher.stop(); + requirementsWatcher = new RequirementsWatcher(context, requirementsListener, requirements); + int notMetRequirements = requirementsWatcher.start(); + onRequirementsStateChanged(requirementsWatcher, notMetRequirements); + } + + /** Returns the maximum number of parallel downloads. */ + public int getMaxParallelDownloads() { + return maxParallelDownloads; + } + + /** + * Sets the maximum number of parallel downloads. + * + * @param maxParallelDownloads The maximum number of parallel downloads. Must be greater than 0. + */ + public void setMaxParallelDownloads(int maxParallelDownloads) { + Assertions.checkArgument(maxParallelDownloads > 0); + if (this.maxParallelDownloads == maxParallelDownloads) { + return; + } + this.maxParallelDownloads = maxParallelDownloads; + pendingMessages++; + internalHandler + .obtainMessage(MSG_SET_MAX_PARALLEL_DOWNLOADS, maxParallelDownloads, /* unused */ 0) + .sendToTarget(); + } + + /** + * Returns the minimum number of times that a download will be retried. A download will fail if + * the specified number of retries is exceeded without any progress being made. + */ + public int getMinRetryCount() { + return minRetryCount; + } + + /** + * Sets the minimum number of times that a download will be retried. A download will fail if the + * specified number of retries is exceeded without any progress being made. + * + * @param minRetryCount The minimum number of times that a download will be retried. + */ + public void setMinRetryCount(int minRetryCount) { + Assertions.checkArgument(minRetryCount >= 0); + if (this.minRetryCount == minRetryCount) { + return; + } + this.minRetryCount = minRetryCount; + pendingMessages++; + internalHandler + .obtainMessage(MSG_SET_MIN_RETRY_COUNT, minRetryCount, /* unused */ 0) + .sendToTarget(); + } + + /** Returns the used {@link DownloadIndex}. */ + public DownloadIndex getDownloadIndex() { + return downloadIndex; + } + + /** + * Returns current downloads. Downloads that are in terminal states (i.e. completed or failed) are + * not included. To query all downloads including those in terminal states, use {@link + * #getDownloadIndex()} instead. + */ + public List getCurrentDownloads() { + return downloads; + } + + /** Returns whether downloads are currently paused. */ + public boolean getDownloadsPaused() { + return downloadsPaused; + } + + /** + * Resumes downloads. + * + *

If the {@link #setRequirements(Requirements) Requirements} are met up to {@link + * #getMaxParallelDownloads() maxParallelDownloads} will be started, excluding those with non-zero + * {@link Download#stopReason stopReasons}. + */ + public void resumeDownloads() { + setDownloadsPaused(/* downloadsPaused= */ false); + } + + /** + * Pauses downloads. Downloads that would otherwise be making progress will transition to {@link + * Download#STATE_QUEUED}. + */ + public void pauseDownloads() { + setDownloadsPaused(/* downloadsPaused= */ true); + } + + /** + * Sets the stop reason for one or all downloads. To clear the stop reason, pass {@link + * Download#STOP_REASON_NONE}. + * + * @param id The content id of the download to update, or {@code null} to set the stop reason for + * all downloads. + * @param stopReason The stop reason, or {@link Download#STOP_REASON_NONE}. + */ + public void setStopReason(@Nullable String id, int stopReason) { + pendingMessages++; + internalHandler + .obtainMessage(MSG_SET_STOP_REASON, stopReason, /* unused */ 0, id) + .sendToTarget(); + } + + /** + * Adds a download defined by the given request. + * + * @param request The download request. + */ + public void addDownload(DownloadRequest request) { + addDownload(request, STOP_REASON_NONE); + } + + /** + * Adds a download defined by the given request and with the specified stop reason. + * + * @param request The download request. + * @param stopReason An initial stop reason for the download, or {@link Download#STOP_REASON_NONE} + * if the download should be started. + */ + public void addDownload(DownloadRequest request, int stopReason) { + pendingMessages++; + internalHandler + .obtainMessage(MSG_ADD_DOWNLOAD, stopReason, /* unused */ 0, request) + .sendToTarget(); + } + + /** + * Cancels the download with the {@code id} and removes all downloaded data. + * + * @param id The unique content id of the download to be started. + */ + public void removeDownload(String id) { + pendingMessages++; + internalHandler.obtainMessage(MSG_REMOVE_DOWNLOAD, id).sendToTarget(); + } + + /** Cancels all pending downloads and removes all downloaded data. */ + public void removeAllDownloads() { + pendingMessages++; + internalHandler.obtainMessage(MSG_REMOVE_ALL_DOWNLOADS).sendToTarget(); + } + + /** + * Stops the downloads and releases resources. Waits until the downloads are persisted to the + * download index. The manager must not be accessed after this method has been called. + */ + public void release() { + synchronized (internalHandler) { + if (internalHandler.released) { + return; + } + internalHandler.sendEmptyMessage(MSG_RELEASE); + boolean wasInterrupted = false; + while (!internalHandler.released) { + try { + internalHandler.wait(); + } catch (InterruptedException e) { + wasInterrupted = true; + } + } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } + mainHandler.removeCallbacksAndMessages(/* token= */ null); + // Reset state. + downloads = Collections.emptyList(); + pendingMessages = 0; + activeTaskCount = 0; + initialized = false; + notMetRequirements = 0; + waitingForRequirements = false; + } + } + + private void setDownloadsPaused(boolean downloadsPaused) { + if (this.downloadsPaused == downloadsPaused) { + return; + } + this.downloadsPaused = downloadsPaused; + pendingMessages++; + internalHandler + .obtainMessage(MSG_SET_DOWNLOADS_PAUSED, downloadsPaused ? 1 : 0, /* unused */ 0) + .sendToTarget(); + boolean waitingForRequirementsChanged = updateWaitingForRequirements(); + for (Listener listener : listeners) { + listener.onDownloadsPausedChanged(this, downloadsPaused); + } + if (waitingForRequirementsChanged) { + notifyWaitingForRequirementsChanged(); + } + } + + private void onRequirementsStateChanged( + RequirementsWatcher requirementsWatcher, + @Requirements.RequirementFlags int notMetRequirements) { + Requirements requirements = requirementsWatcher.getRequirements(); + if (this.notMetRequirements != notMetRequirements) { + this.notMetRequirements = notMetRequirements; + pendingMessages++; + internalHandler + .obtainMessage(MSG_SET_NOT_MET_REQUIREMENTS, notMetRequirements, /* unused */ 0) + .sendToTarget(); + } + boolean waitingForRequirementsChanged = updateWaitingForRequirements(); + for (Listener listener : listeners) { + listener.onRequirementsStateChanged(this, requirements, notMetRequirements); + } + if (waitingForRequirementsChanged) { + notifyWaitingForRequirementsChanged(); + } + } + + private boolean updateWaitingForRequirements() { + boolean waitingForRequirements = false; + if (!downloadsPaused && notMetRequirements != 0) { + for (int i = 0; i < downloads.size(); i++) { + if (downloads.get(i).state == STATE_QUEUED) { + waitingForRequirements = true; + break; + } + } + } + boolean waitingForRequirementsChanged = this.waitingForRequirements != waitingForRequirements; + this.waitingForRequirements = waitingForRequirements; + return waitingForRequirementsChanged; + } + + private void notifyWaitingForRequirementsChanged() { + for (Listener listener : listeners) { + listener.onWaitingForRequirementsChanged(this, waitingForRequirements); + } + } + + // Main thread message handling. + + @SuppressWarnings("unchecked") + private boolean handleMainMessage(Message message) { + switch (message.what) { + case MSG_INITIALIZED: + List downloads = (List) message.obj; + onInitialized(downloads); + break; + case MSG_DOWNLOAD_UPDATE: + DownloadUpdate update = (DownloadUpdate) message.obj; + onDownloadUpdate(update); + break; + case MSG_PROCESSED: + int processedMessageCount = message.arg1; + int activeTaskCount = message.arg2; + onMessageProcessed(processedMessageCount, activeTaskCount); + break; + default: + throw new IllegalStateException(); + } + return true; + } + + private void onInitialized(List downloads) { + initialized = true; + this.downloads = Collections.unmodifiableList(downloads); + boolean waitingForRequirementsChanged = updateWaitingForRequirements(); + for (Listener listener : listeners) { + listener.onInitialized(DownloadManager.this); + } + if (waitingForRequirementsChanged) { + notifyWaitingForRequirementsChanged(); + } + } + + private void onDownloadUpdate(DownloadUpdate update) { + downloads = Collections.unmodifiableList(update.downloads); + Download updatedDownload = update.download; + boolean waitingForRequirementsChanged = updateWaitingForRequirements(); + if (update.isRemove) { + for (Listener listener : listeners) { + listener.onDownloadRemoved(this, updatedDownload); + } + } else { + for (Listener listener : listeners) { + listener.onDownloadChanged(this, updatedDownload); + } + } + if (waitingForRequirementsChanged) { + notifyWaitingForRequirementsChanged(); + } + } + + private void onMessageProcessed(int processedMessageCount, int activeTaskCount) { + this.pendingMessages -= processedMessageCount; + this.activeTaskCount = activeTaskCount; + if (isIdle()) { + for (Listener listener : listeners) { + listener.onIdle(this); + } + } + } + + /* package */ static Download mergeRequest( + Download download, DownloadRequest request, int stopReason, long nowMs) { + @Download.State int state = download.state; + // Treat the merge as creating a new download if we're currently removing the existing one, or + // if the existing download is in a terminal state. Else treat the merge as updating the + // existing download. + long startTimeMs = + state == STATE_REMOVING || download.isTerminalState() ? nowMs : download.startTimeMs; + if (state == STATE_REMOVING || state == STATE_RESTARTING) { + state = STATE_RESTARTING; + } else if (stopReason != STOP_REASON_NONE) { + state = STATE_STOPPED; + } else { + state = STATE_QUEUED; + } + return new Download( + download.request.copyWithMergedRequest(request), + state, + startTimeMs, + /* updateTimeMs= */ nowMs, + /* contentLength= */ C.LENGTH_UNSET, + stopReason, + FAILURE_REASON_NONE); + } + + private static final class InternalHandler extends Handler { + + private static final int UPDATE_PROGRESS_INTERVAL_MS = 5000; + + public boolean released; + + private final HandlerThread thread; + private final WritableDownloadIndex downloadIndex; + private final DownloaderFactory downloaderFactory; + private final Handler mainHandler; + private final ArrayList downloads; + private final HashMap activeTasks; + + @Requirements.RequirementFlags private int notMetRequirements; + private boolean downloadsPaused; + private int maxParallelDownloads; + private int minRetryCount; + private int activeDownloadTaskCount; + + public InternalHandler( + HandlerThread thread, + WritableDownloadIndex downloadIndex, + DownloaderFactory downloaderFactory, + Handler mainHandler, + int maxParallelDownloads, + int minRetryCount, + boolean downloadsPaused) { + super(thread.getLooper()); + this.thread = thread; + this.downloadIndex = downloadIndex; + this.downloaderFactory = downloaderFactory; + this.mainHandler = mainHandler; + this.maxParallelDownloads = maxParallelDownloads; + this.minRetryCount = minRetryCount; + this.downloadsPaused = downloadsPaused; + downloads = new ArrayList<>(); + activeTasks = new HashMap<>(); + } + + @Override + public void handleMessage(Message message) { + boolean processedExternalMessage = true; + switch (message.what) { + case MSG_INITIALIZE: + int notMetRequirements = message.arg1; + initialize(notMetRequirements); + break; + case MSG_SET_DOWNLOADS_PAUSED: + boolean downloadsPaused = message.arg1 != 0; + setDownloadsPaused(downloadsPaused); + break; + case MSG_SET_NOT_MET_REQUIREMENTS: + notMetRequirements = message.arg1; + setNotMetRequirements(notMetRequirements); + break; + case MSG_SET_STOP_REASON: + String id = (String) message.obj; + int stopReason = message.arg1; + setStopReason(id, stopReason); + break; + case MSG_SET_MAX_PARALLEL_DOWNLOADS: + int maxParallelDownloads = message.arg1; + setMaxParallelDownloads(maxParallelDownloads); + break; + case MSG_SET_MIN_RETRY_COUNT: + int minRetryCount = message.arg1; + setMinRetryCount(minRetryCount); + break; + case MSG_ADD_DOWNLOAD: + DownloadRequest request = (DownloadRequest) message.obj; + stopReason = message.arg1; + addDownload(request, stopReason); + break; + case MSG_REMOVE_DOWNLOAD: + id = (String) message.obj; + removeDownload(id); + break; + case MSG_REMOVE_ALL_DOWNLOADS: + removeAllDownloads(); + break; + case MSG_TASK_STOPPED: + Task task = (Task) message.obj; + onTaskStopped(task); + processedExternalMessage = false; // This message is posted internally. + break; + case MSG_CONTENT_LENGTH_CHANGED: + task = (Task) message.obj; + onContentLengthChanged(task); + return; // No need to post back to mainHandler. + case MSG_UPDATE_PROGRESS: + updateProgress(); + return; // No need to post back to mainHandler. + case MSG_RELEASE: + release(); + return; // No need to post back to mainHandler. + default: + throw new IllegalStateException(); + } + mainHandler + .obtainMessage(MSG_PROCESSED, processedExternalMessage ? 1 : 0, activeTasks.size()) + .sendToTarget(); + } + + private void initialize(int notMetRequirements) { + this.notMetRequirements = notMetRequirements; + DownloadCursor cursor = null; + try { + downloadIndex.setDownloadingStatesToQueued(); + cursor = + downloadIndex.getDownloads( + STATE_QUEUED, STATE_STOPPED, STATE_DOWNLOADING, STATE_REMOVING, STATE_RESTARTING); + while (cursor.moveToNext()) { + downloads.add(cursor.getDownload()); + } + } catch (IOException e) { + Log.e(TAG, "Failed to load index.", e); + downloads.clear(); + } finally { + Util.closeQuietly(cursor); + } + // A copy must be used for the message to ensure that subsequent changes to the downloads list + // are not visible to the main thread when it processes the message. + ArrayList downloadsForMessage = new ArrayList<>(downloads); + mainHandler.obtainMessage(MSG_INITIALIZED, downloadsForMessage).sendToTarget(); + syncTasks(); + } + + private void setDownloadsPaused(boolean downloadsPaused) { + this.downloadsPaused = downloadsPaused; + syncTasks(); + } + + private void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) { + this.notMetRequirements = notMetRequirements; + syncTasks(); + } + + private void setStopReason(@Nullable String id, int stopReason) { + if (id == null) { + for (int i = 0; i < downloads.size(); i++) { + setStopReason(downloads.get(i), stopReason); + } + try { + // Set the stop reason for downloads in terminal states as well. + downloadIndex.setStopReason(stopReason); + } catch (IOException e) { + Log.e(TAG, "Failed to set manual stop reason", e); + } + } else { + @Nullable Download download = getDownload(id, /* loadFromIndex= */ false); + if (download != null) { + setStopReason(download, stopReason); + } else { + try { + // Set the stop reason if the download is in a terminal state. + downloadIndex.setStopReason(id, stopReason); + } catch (IOException e) { + Log.e(TAG, "Failed to set manual stop reason: " + id, e); + } + } + } + syncTasks(); + } + + private void setStopReason(Download download, int stopReason) { + if (stopReason == STOP_REASON_NONE) { + if (download.state == STATE_STOPPED) { + putDownloadWithState(download, STATE_QUEUED); + } + } else if (stopReason != download.stopReason) { + @Download.State int state = download.state; + if (state == STATE_QUEUED || state == STATE_DOWNLOADING) { + state = STATE_STOPPED; + } + putDownload( + new Download( + download.request, + state, + download.startTimeMs, + /* updateTimeMs= */ System.currentTimeMillis(), + download.contentLength, + stopReason, + FAILURE_REASON_NONE, + download.progress)); + } + } + + private void setMaxParallelDownloads(int maxParallelDownloads) { + this.maxParallelDownloads = maxParallelDownloads; + syncTasks(); + } + + private void setMinRetryCount(int minRetryCount) { + this.minRetryCount = minRetryCount; + } + + private void addDownload(DownloadRequest request, int stopReason) { + @Nullable Download download = getDownload(request.id, /* loadFromIndex= */ true); + long nowMs = System.currentTimeMillis(); + if (download != null) { + putDownload(mergeRequest(download, request, stopReason, nowMs)); + } else { + putDownload( + new Download( + request, + stopReason != STOP_REASON_NONE ? STATE_STOPPED : STATE_QUEUED, + /* startTimeMs= */ nowMs, + /* updateTimeMs= */ nowMs, + /* contentLength= */ C.LENGTH_UNSET, + stopReason, + FAILURE_REASON_NONE)); + } + syncTasks(); + } + + private void removeDownload(String id) { + @Nullable Download download = getDownload(id, /* loadFromIndex= */ true); + if (download == null) { + Log.e(TAG, "Failed to remove nonexistent download: " + id); + return; + } + putDownloadWithState(download, STATE_REMOVING); + syncTasks(); + } + + private void removeAllDownloads() { + List terminalDownloads = new ArrayList<>(); + try (DownloadCursor cursor = downloadIndex.getDownloads(STATE_COMPLETED, STATE_FAILED)) { + while (cursor.moveToNext()) { + terminalDownloads.add(cursor.getDownload()); + } + } catch (IOException e) { + Log.e(TAG, "Failed to load downloads."); + } + for (int i = 0; i < downloads.size(); i++) { + downloads.set(i, copyDownloadWithState(downloads.get(i), STATE_REMOVING)); + } + for (int i = 0; i < terminalDownloads.size(); i++) { + downloads.add(copyDownloadWithState(terminalDownloads.get(i), STATE_REMOVING)); + } + Collections.sort(downloads, InternalHandler::compareStartTimes); + try { + downloadIndex.setStatesToRemoving(); + } catch (IOException e) { + Log.e(TAG, "Failed to update index.", e); + } + ArrayList updateList = new ArrayList<>(downloads); + for (int i = 0; i < downloads.size(); i++) { + DownloadUpdate update = + new DownloadUpdate(downloads.get(i), /* isRemove= */ false, updateList); + mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget(); + } + syncTasks(); + } + + private void release() { + for (Task task : activeTasks.values()) { + task.cancel(/* released= */ true); + } + try { + downloadIndex.setDownloadingStatesToQueued(); + } catch (IOException e) { + Log.e(TAG, "Failed to update index.", e); + } + downloads.clear(); + thread.quit(); + synchronized (this) { + released = true; + notifyAll(); + } + } + + // Start and cancel tasks based on the current download and manager states. + + private void syncTasks() { + int accumulatingDownloadTaskCount = 0; + for (int i = 0; i < downloads.size(); i++) { + Download download = downloads.get(i); + @Nullable Task activeTask = activeTasks.get(download.request.id); + switch (download.state) { + case STATE_STOPPED: + syncStoppedDownload(activeTask); + break; + case STATE_QUEUED: + activeTask = syncQueuedDownload(activeTask, download); + break; + case STATE_DOWNLOADING: + Assertions.checkNotNull(activeTask); + syncDownloadingDownload(activeTask, download, accumulatingDownloadTaskCount); + break; + case STATE_REMOVING: + case STATE_RESTARTING: + syncRemovingDownload(activeTask, download); + break; + case STATE_COMPLETED: + case STATE_FAILED: + default: + throw new IllegalStateException(); + } + if (activeTask != null && !activeTask.isRemove) { + accumulatingDownloadTaskCount++; + } + } + } + + private void syncStoppedDownload(@Nullable Task activeTask) { + if (activeTask != null) { + // We have a task, which must be a download task. Cancel it. + Assertions.checkState(!activeTask.isRemove); + activeTask.cancel(/* released= */ false); + } + } + + @Nullable + @CheckResult + private Task syncQueuedDownload(@Nullable Task activeTask, Download download) { + if (activeTask != null) { + // We have a task, which must be a download task. If the download state is queued we need to + // cancel it and start a new one, since a new request has been merged into the download. + Assertions.checkState(!activeTask.isRemove); + activeTask.cancel(/* released= */ false); + return activeTask; + } + + if (!canDownloadsRun() || activeDownloadTaskCount >= maxParallelDownloads) { + return null; + } + + // We can start a download task. + download = putDownloadWithState(download, STATE_DOWNLOADING); + Downloader downloader = downloaderFactory.createDownloader(download.request); + activeTask = + new Task( + download.request, + downloader, + download.progress, + /* isRemove= */ false, + minRetryCount, + /* internalHandler= */ this); + activeTasks.put(download.request.id, activeTask); + if (activeDownloadTaskCount++ == 0) { + sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, UPDATE_PROGRESS_INTERVAL_MS); + } + activeTask.start(); + return activeTask; + } + + private void syncDownloadingDownload( + Task activeTask, Download download, int accumulatingDownloadTaskCount) { + Assertions.checkState(!activeTask.isRemove); + if (!canDownloadsRun() || accumulatingDownloadTaskCount >= maxParallelDownloads) { + putDownloadWithState(download, STATE_QUEUED); + activeTask.cancel(/* released= */ false); + } + } + + private void syncRemovingDownload(@Nullable Task activeTask, Download download) { + if (activeTask != null) { + if (!activeTask.isRemove) { + // Cancel the downloading task. + activeTask.cancel(/* released= */ false); + } + // The activeTask is either a remove task, or a downloading task that we just cancelled. In + // the latter case we need to wait for the task to stop before we start a remove task. + return; + } + + // We can start a remove task. + Downloader downloader = downloaderFactory.createDownloader(download.request); + activeTask = + new Task( + download.request, + downloader, + download.progress, + /* isRemove= */ true, + minRetryCount, + /* internalHandler= */ this); + activeTasks.put(download.request.id, activeTask); + activeTask.start(); + } + + // Task event processing. + + private void onContentLengthChanged(Task task) { + String downloadId = task.request.id; + long contentLength = task.contentLength; + Download download = + Assertions.checkNotNull(getDownload(downloadId, /* loadFromIndex= */ false)); + if (contentLength == download.contentLength || contentLength == C.LENGTH_UNSET) { + return; + } + putDownload( + new Download( + download.request, + download.state, + download.startTimeMs, + /* updateTimeMs= */ System.currentTimeMillis(), + contentLength, + download.stopReason, + download.failureReason, + download.progress)); + } + + private void onTaskStopped(Task task) { + String downloadId = task.request.id; + activeTasks.remove(downloadId); + + boolean isRemove = task.isRemove; + if (!isRemove && --activeDownloadTaskCount == 0) { + removeMessages(MSG_UPDATE_PROGRESS); + } + + if (task.isCanceled) { + syncTasks(); + return; + } + + @Nullable Throwable finalError = task.finalError; + if (finalError != null) { + Log.e(TAG, "Task failed: " + task.request + ", " + isRemove, finalError); + } + + Download download = + Assertions.checkNotNull(getDownload(downloadId, /* loadFromIndex= */ false)); + switch (download.state) { + case STATE_DOWNLOADING: + Assertions.checkState(!isRemove); + onDownloadTaskStopped(download, finalError); + break; + case STATE_REMOVING: + case STATE_RESTARTING: + Assertions.checkState(isRemove); + onRemoveTaskStopped(download); + break; + case STATE_QUEUED: + case STATE_STOPPED: + case STATE_COMPLETED: + case STATE_FAILED: + default: + throw new IllegalStateException(); + } + + syncTasks(); + } + + private void onDownloadTaskStopped(Download download, @Nullable Throwable finalError) { + download = + new Download( + download.request, + finalError == null ? STATE_COMPLETED : STATE_FAILED, + download.startTimeMs, + /* updateTimeMs= */ System.currentTimeMillis(), + download.contentLength, + download.stopReason, + finalError == null ? FAILURE_REASON_NONE : FAILURE_REASON_UNKNOWN, + download.progress); + // The download is now in a terminal state, so should not be in the downloads list. + downloads.remove(getDownloadIndex(download.request.id)); + // We still need to update the download index and main thread. + try { + downloadIndex.putDownload(download); + } catch (IOException e) { + Log.e(TAG, "Failed to update index.", e); + } + DownloadUpdate update = + new DownloadUpdate(download, /* isRemove= */ false, new ArrayList<>(downloads)); + mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget(); + } + + private void onRemoveTaskStopped(Download download) { + if (download.state == STATE_RESTARTING) { + putDownloadWithState( + download, download.stopReason == STOP_REASON_NONE ? STATE_QUEUED : STATE_STOPPED); + syncTasks(); + } else { + int removeIndex = getDownloadIndex(download.request.id); + downloads.remove(removeIndex); + try { + downloadIndex.removeDownload(download.request.id); + } catch (IOException e) { + Log.e(TAG, "Failed to remove from database"); + } + DownloadUpdate update = + new DownloadUpdate(download, /* isRemove= */ true, new ArrayList<>(downloads)); + mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget(); + } + } + + // Progress updates. + + private void updateProgress() { + for (int i = 0; i < downloads.size(); i++) { + Download download = downloads.get(i); + if (download.state == STATE_DOWNLOADING) { + try { + downloadIndex.putDownload(download); + } catch (IOException e) { + Log.e(TAG, "Failed to update index.", e); + } + } + } + sendEmptyMessageDelayed(MSG_UPDATE_PROGRESS, UPDATE_PROGRESS_INTERVAL_MS); + } + + // Helper methods. + + private boolean canDownloadsRun() { + return !downloadsPaused && notMetRequirements == 0; + } + + private Download putDownloadWithState(Download download, @Download.State int state) { + // Downloads in terminal states shouldn't be in the downloads list. This method cannot be used + // to set STATE_STOPPED either, because it doesn't have a stopReason argument. + Assertions.checkState( + state != STATE_COMPLETED && state != STATE_FAILED && state != STATE_STOPPED); + return putDownload(copyDownloadWithState(download, state)); + } + + private Download putDownload(Download download) { + // Downloads in terminal states shouldn't be in the downloads list. + Assertions.checkState(download.state != STATE_COMPLETED && download.state != STATE_FAILED); + int changedIndex = getDownloadIndex(download.request.id); + if (changedIndex == C.INDEX_UNSET) { + downloads.add(download); + Collections.sort(downloads, InternalHandler::compareStartTimes); + } else { + boolean needsSort = download.startTimeMs != downloads.get(changedIndex).startTimeMs; + downloads.set(changedIndex, download); + if (needsSort) { + Collections.sort(downloads, InternalHandler::compareStartTimes); + } + } + try { + downloadIndex.putDownload(download); + } catch (IOException e) { + Log.e(TAG, "Failed to update index.", e); + } + DownloadUpdate update = + new DownloadUpdate(download, /* isRemove= */ false, new ArrayList<>(downloads)); + mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget(); + return download; + } + + @Nullable + private Download getDownload(String id, boolean loadFromIndex) { + int index = getDownloadIndex(id); + if (index != C.INDEX_UNSET) { + return downloads.get(index); + } + if (loadFromIndex) { + try { + return downloadIndex.getDownload(id); + } catch (IOException e) { + Log.e(TAG, "Failed to load download: " + id, e); + } + } + return null; + } + + private int getDownloadIndex(String id) { + for (int i = 0; i < downloads.size(); i++) { + Download download = downloads.get(i); + if (download.request.id.equals(id)) { + return i; + } + } + return C.INDEX_UNSET; + } + + private static Download copyDownloadWithState(Download download, @Download.State int state) { + return new Download( + download.request, + state, + download.startTimeMs, + /* updateTimeMs= */ System.currentTimeMillis(), + download.contentLength, + /* stopReason= */ 0, + FAILURE_REASON_NONE, + download.progress); + } + + private static int compareStartTimes(Download first, Download second) { + return Util.compareLong(first.startTimeMs, second.startTimeMs); + } + } + + private static class Task extends Thread implements Downloader.ProgressListener { + + private final DownloadRequest request; + private final Downloader downloader; + private final DownloadProgress downloadProgress; + private final boolean isRemove; + private final int minRetryCount; + + @Nullable private volatile InternalHandler internalHandler; + private volatile boolean isCanceled; + @Nullable private Throwable finalError; + + private long contentLength; + + private Task( + DownloadRequest request, + Downloader downloader, + DownloadProgress downloadProgress, + boolean isRemove, + int minRetryCount, + InternalHandler internalHandler) { + this.request = request; + this.downloader = downloader; + this.downloadProgress = downloadProgress; + this.isRemove = isRemove; + this.minRetryCount = minRetryCount; + this.internalHandler = internalHandler; + contentLength = C.LENGTH_UNSET; + } + + @SuppressWarnings("nullness:assignment.type.incompatible") + public void cancel(boolean released) { + if (released) { + // Download threads are GC roots for as long as they're running. The time taken for + // cancellation to complete depends on the implementation of the downloader being used. We + // null the handler reference here so that it doesn't prevent garbage collection of the + // download manager whilst cancellation is ongoing. + internalHandler = null; + } + if (!isCanceled) { + isCanceled = true; + downloader.cancel(); + interrupt(); + } + } + + // Methods running on download thread. + + @Override + public void run() { + try { + if (isRemove) { + downloader.remove(); + } else { + int errorCount = 0; + long errorPosition = C.LENGTH_UNSET; + while (!isCanceled) { + try { + downloader.download(/* progressListener= */ this); + break; + } catch (IOException e) { + if (!isCanceled) { + long bytesDownloaded = downloadProgress.bytesDownloaded; + if (bytesDownloaded != errorPosition) { + errorPosition = bytesDownloaded; + errorCount = 0; + } + if (++errorCount > minRetryCount) { + throw e; + } + Thread.sleep(getRetryDelayMillis(errorCount)); + } + } + } + } + } catch (Throwable e) { + finalError = e; + } + @Nullable Handler internalHandler = this.internalHandler; + if (internalHandler != null) { + internalHandler.obtainMessage(MSG_TASK_STOPPED, this).sendToTarget(); + } + } + + @Override + public void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded) { + downloadProgress.bytesDownloaded = bytesDownloaded; + downloadProgress.percentDownloaded = percentDownloaded; + if (contentLength != this.contentLength) { + this.contentLength = contentLength; + @Nullable Handler internalHandler = this.internalHandler; + if (internalHandler != null) { + internalHandler.obtainMessage(MSG_CONTENT_LENGTH_CHANGED, this).sendToTarget(); + } + } + } + + private static int getRetryDelayMillis(int errorCount) { + return Math.min((errorCount - 1) * 1000, 5000); + } + } + + private static final class DownloadUpdate { + + public final Download download; + public final boolean isRemove; + public final List downloads; + + public DownloadUpdate(Download download, boolean isRemove, List downloads) { + this.download = download; + this.isRemove = isRemove; + this.downloads = downloads; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadProgress.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadProgress.java new file mode 100644 index 000000000000..177698ec1e5c --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadProgress.java @@ -0,0 +1,28 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; + +/** Mutable {@link Download} progress. */ +public class DownloadProgress { + + /** The number of bytes that have been downloaded. */ + public long bytesDownloaded; + + /** The percentage that has been downloaded, or {@link C#PERCENTAGE_UNSET} if unknown. */ + public float percentDownloaded; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadRequest.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadRequest.java new file mode 100644 index 000000000000..31a441aa2d0b --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadRequest.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** Defines content to be downloaded. */ +public final class DownloadRequest implements Parcelable { + + /** Thrown when the encoded request data belongs to an unsupported request type. */ + public static class UnsupportedRequestException extends IOException {} + + /** Type for progressive downloads. */ + public static final String TYPE_PROGRESSIVE = "progressive"; + /** Type for DASH downloads. */ + public static final String TYPE_DASH = "dash"; + /** Type for HLS downloads. */ + public static final String TYPE_HLS = "hls"; + /** Type for SmoothStreaming downloads. */ + public static final String TYPE_SS = "ss"; + + /** The unique content id. */ + public final String id; + /** The type of the request. */ + public final String type; + /** The uri being downloaded. */ + public final Uri uri; + /** Stream keys to be downloaded. If empty, all streams will be downloaded. */ + public final List streamKeys; + /** + * Custom key for cache indexing, or null. Must be null for DASH, HLS and SmoothStreaming + * downloads. + */ + @Nullable public final String customCacheKey; + /** Application defined data associated with the download. May be empty. */ + public final byte[] data; + + /** + * @param id See {@link #id}. + * @param type See {@link #type}. + * @param uri See {@link #uri}. + * @param streamKeys See {@link #streamKeys}. + * @param customCacheKey See {@link #customCacheKey}. + * @param data See {@link #data}. + */ + public DownloadRequest( + String id, + String type, + Uri uri, + List streamKeys, + @Nullable String customCacheKey, + @Nullable byte[] data) { + if (TYPE_DASH.equals(type) || TYPE_HLS.equals(type) || TYPE_SS.equals(type)) { + Assertions.checkArgument( + customCacheKey == null, "customCacheKey must be null for type: " + type); + } + this.id = id; + this.type = type; + this.uri = uri; + ArrayList mutableKeys = new ArrayList<>(streamKeys); + Collections.sort(mutableKeys); + this.streamKeys = Collections.unmodifiableList(mutableKeys); + this.customCacheKey = customCacheKey; + this.data = data != null ? Arrays.copyOf(data, data.length) : Util.EMPTY_BYTE_ARRAY; + } + + /* package */ DownloadRequest(Parcel in) { + id = castNonNull(in.readString()); + type = castNonNull(in.readString()); + uri = Uri.parse(castNonNull(in.readString())); + int streamKeyCount = in.readInt(); + ArrayList mutableStreamKeys = new ArrayList<>(streamKeyCount); + for (int i = 0; i < streamKeyCount; i++) { + mutableStreamKeys.add(in.readParcelable(StreamKey.class.getClassLoader())); + } + streamKeys = Collections.unmodifiableList(mutableStreamKeys); + customCacheKey = in.readString(); + data = castNonNull(in.createByteArray()); + } + + /** + * Returns a copy with the specified ID. + * + * @param id The ID of the copy. + * @return The copy with the specified ID. + */ + public DownloadRequest copyWithId(String id) { + return new DownloadRequest(id, type, uri, streamKeys, customCacheKey, data); + } + + /** + * Returns the result of merging {@code newRequest} into this request. The requests must have the + * same {@link #id} and {@link #type}. + * + *

If the requests have different {@link #uri}, {@link #customCacheKey} and {@link #data} + * values, then those from the request being merged are included in the result. + * + * @param newRequest The request being merged. + * @return The merged result. + * @throws IllegalArgumentException If the requests do not have the same {@link #id} and {@link + * #type}. + */ + public DownloadRequest copyWithMergedRequest(DownloadRequest newRequest) { + Assertions.checkArgument(id.equals(newRequest.id)); + Assertions.checkArgument(type.equals(newRequest.type)); + List mergedKeys; + if (streamKeys.isEmpty() || newRequest.streamKeys.isEmpty()) { + // If either streamKeys is empty then all streams should be downloaded. + mergedKeys = Collections.emptyList(); + } else { + mergedKeys = new ArrayList<>(streamKeys); + for (int i = 0; i < newRequest.streamKeys.size(); i++) { + StreamKey newKey = newRequest.streamKeys.get(i); + if (!mergedKeys.contains(newKey)) { + mergedKeys.add(newKey); + } + } + } + return new DownloadRequest( + id, type, newRequest.uri, mergedKeys, newRequest.customCacheKey, newRequest.data); + } + + @Override + public String toString() { + return type + ":" + id; + } + + @Override + public boolean equals(@Nullable Object o) { + if (!(o instanceof DownloadRequest)) { + return false; + } + DownloadRequest that = (DownloadRequest) o; + return id.equals(that.id) + && type.equals(that.type) + && uri.equals(that.uri) + && streamKeys.equals(that.streamKeys) + && Util.areEqual(customCacheKey, that.customCacheKey) + && Arrays.equals(data, that.data); + } + + @Override + public final int hashCode() { + int result = type.hashCode(); + result = 31 * result + id.hashCode(); + result = 31 * result + type.hashCode(); + result = 31 * result + uri.hashCode(); + result = 31 * result + streamKeys.hashCode(); + result = 31 * result + (customCacheKey != null ? customCacheKey.hashCode() : 0); + result = 31 * result + Arrays.hashCode(data); + return result; + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(type); + dest.writeString(uri.toString()); + dest.writeInt(streamKeys.size()); + for (int i = 0; i < streamKeys.size(); i++) { + dest.writeParcelable(streamKeys.get(i), /* parcelableFlags= */ 0); + } + dest.writeString(customCacheKey); + dest.writeByteArray(data); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public DownloadRequest createFromParcel(Parcel in) { + return new DownloadRequest(in); + } + + @Override + public DownloadRequest[] newArray(int size) { + return new DownloadRequest[size]; + } + }; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadService.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadService.java new file mode 100644 index 000000000000..a2d7d82438dd --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadService.java @@ -0,0 +1,1049 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.offline.Download.STOP_REASON_NONE; + +import android.app.Notification; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler.Requirements; +import org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler.Scheduler; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NotificationUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.HashMap; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** A {@link Service} for downloading media. */ +public abstract class DownloadService extends Service { + + /** + * Starts a download service to resume any ongoing downloads. Extras: + * + *

    + *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + *
+ */ + public static final String ACTION_INIT = + "com.google.android.exoplayer.downloadService.action.INIT"; + + /** Like {@link #ACTION_INIT}, but with {@link #KEY_FOREGROUND} implicitly set to true. */ + private static final String ACTION_RESTART = + "com.google.android.exoplayer.downloadService.action.RESTART"; + + /** + * Adds a new download. Extras: + * + *
    + *
  • {@link #KEY_DOWNLOAD_REQUEST} - A {@link DownloadRequest} defining the download to be + * added. + *
  • {@link #KEY_STOP_REASON} - An initial stop reason for the download. If omitted {@link + * Download#STOP_REASON_NONE} is used. + *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + *
+ */ + public static final String ACTION_ADD_DOWNLOAD = + "com.google.android.exoplayer.downloadService.action.ADD_DOWNLOAD"; + + /** + * Removes a download. Extras: + * + *
    + *
  • {@link #KEY_CONTENT_ID} - The content id of a download to remove. + *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + *
+ */ + public static final String ACTION_REMOVE_DOWNLOAD = + "com.google.android.exoplayer.downloadService.action.REMOVE_DOWNLOAD"; + + /** + * Removes all downloads. Extras: + * + *
    + *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + *
+ */ + public static final String ACTION_REMOVE_ALL_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.REMOVE_ALL_DOWNLOADS"; + + /** + * Resumes all downloads except those that have a non-zero {@link Download#stopReason}. Extras: + * + *
    + *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + *
+ */ + public static final String ACTION_RESUME_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.RESUME_DOWNLOADS"; + + /** + * Pauses all downloads. Extras: + * + *
    + *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + *
+ */ + public static final String ACTION_PAUSE_DOWNLOADS = + "com.google.android.exoplayer.downloadService.action.PAUSE_DOWNLOADS"; + + /** + * Sets the stop reason for one or all downloads. To clear the stop reason, pass {@link + * Download#STOP_REASON_NONE}. Extras: + * + *
    + *
  • {@link #KEY_CONTENT_ID} - The content id of a single download to update with the stop + * reason. If omitted, all downloads will be updated. + *
  • {@link #KEY_STOP_REASON} - An application provided reason for stopping the download or + * downloads, or {@link Download#STOP_REASON_NONE} to clear the stop reason. + *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + *
+ */ + public static final String ACTION_SET_STOP_REASON = + "com.google.android.exoplayer.downloadService.action.SET_STOP_REASON"; + + /** + * Sets the requirements that need to be met for downloads to progress. Extras: + * + *
    + *
  • {@link #KEY_REQUIREMENTS} - A {@link Requirements}. + *
  • {@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}. + *
+ */ + public static final String ACTION_SET_REQUIREMENTS = + "com.google.android.exoplayer.downloadService.action.SET_REQUIREMENTS"; + + /** Key for the {@link DownloadRequest} in {@link #ACTION_ADD_DOWNLOAD} intents. */ + public static final String KEY_DOWNLOAD_REQUEST = "download_request"; + + /** + * Key for the {@link String} content id in {@link #ACTION_SET_STOP_REASON} and {@link + * #ACTION_REMOVE_DOWNLOAD} intents. + */ + public static final String KEY_CONTENT_ID = "content_id"; + + /** + * Key for the integer stop reason in {@link #ACTION_SET_STOP_REASON} and {@link + * #ACTION_ADD_DOWNLOAD} intents. + */ + public static final String KEY_STOP_REASON = "stop_reason"; + + /** Key for the {@link Requirements} in {@link #ACTION_SET_REQUIREMENTS} intents. */ + public static final String KEY_REQUIREMENTS = "requirements"; + + /** + * Key for a boolean extra that can be set on any intent to indicate whether the service was + * started in the foreground. If set, the service is guaranteed to call {@link + * #startForeground(int, Notification)}. + */ + public static final String KEY_FOREGROUND = "foreground"; + + /** Invalid foreground notification id that can be used to run the service in the background. */ + public static final int FOREGROUND_NOTIFICATION_ID_NONE = 0; + + /** Default foreground notification update interval in milliseconds. */ + public static final long DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL = 1000; + + private static final String TAG = "DownloadService"; + + // Keep a DownloadManagerHelper for each DownloadService as long as the process is running. The + // helper is needed to restart the DownloadService when there's no scheduler. Even when there is a + // scheduler, the DownloadManagerHelper is typically able to restart the DownloadService faster. + private static final HashMap, DownloadManagerHelper> + downloadManagerHelpers = new HashMap<>(); + + @Nullable private final ForegroundNotificationUpdater foregroundNotificationUpdater; + @Nullable private final String channelId; + @StringRes private final int channelNameResourceId; + @StringRes private final int channelDescriptionResourceId; + + @MonotonicNonNull private DownloadManager downloadManager; + private int lastStartId; + private boolean startedInForeground; + private boolean taskRemoved; + private boolean isStopped; + private boolean isDestroyed; + + /** + * Creates a DownloadService. + * + *

If {@code foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE} then the + * service will only ever run in the background. No foreground notification will be displayed and + * {@link #getScheduler()} will not be called. + * + *

If {@code foregroundNotificationId} is not {@link #FOREGROUND_NOTIFICATION_ID_NONE} then the + * service will run in the foreground. The foreground notification will be updated at least as + * often as the interval specified by {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. + * + * @param foregroundNotificationId The notification id for the foreground notification, or {@link + * #FOREGROUND_NOTIFICATION_ID_NONE} if the service should only ever run in the background. + */ + protected DownloadService(int foregroundNotificationId) { + this(foregroundNotificationId, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL); + } + + /** + * Creates a DownloadService. + * + * @param foregroundNotificationId The notification id for the foreground notification, or {@link + * #FOREGROUND_NOTIFICATION_ID_NONE} if the service should only ever run in the background. + * @param foregroundNotificationUpdateInterval The maximum interval between updates to the + * foreground notification, in milliseconds. Ignored if {@code foregroundNotificationId} is + * {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + */ + protected DownloadService( + int foregroundNotificationId, long foregroundNotificationUpdateInterval) { + this( + foregroundNotificationId, + foregroundNotificationUpdateInterval, + /* channelId= */ null, + /* channelNameResourceId= */ 0, + /* channelDescriptionResourceId= */ 0); + } + + /** @deprecated Use {@link #DownloadService(int, long, String, int, int)}. */ + @Deprecated + protected DownloadService( + int foregroundNotificationId, + long foregroundNotificationUpdateInterval, + @Nullable String channelId, + @StringRes int channelNameResourceId) { + this( + foregroundNotificationId, + foregroundNotificationUpdateInterval, + channelId, + channelNameResourceId, + /* channelDescriptionResourceId= */ 0); + } + + /** + * Creates a DownloadService. + * + * @param foregroundNotificationId The notification id for the foreground notification, or {@link + * #FOREGROUND_NOTIFICATION_ID_NONE} if the service should only ever run in the background. + * @param foregroundNotificationUpdateInterval The maximum interval between updates to the + * foreground notification, in milliseconds. Ignored if {@code foregroundNotificationId} is + * {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + * @param channelId An id for a low priority notification channel to create, or {@code null} if + * the app will take care of creating a notification channel if needed. If specified, must be + * unique per package. The value may be truncated if it's too long. Ignored if {@code + * foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + * @param channelNameResourceId A string resource identifier for the user visible name of the + * notification channel. The recommended maximum length is 40 characters. The value may be + * truncated if it's too long. Ignored if {@code channelId} is null or if {@code + * foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + * @param channelDescriptionResourceId A string resource identifier for the user visible + * description of the notification channel, or 0 if no description is provided. The + * recommended maximum length is 300 characters. The value may be truncated if it is too long. + * Ignored if {@code channelId} is null or if {@code foregroundNotificationId} is {@link + * #FOREGROUND_NOTIFICATION_ID_NONE}. + */ + protected DownloadService( + int foregroundNotificationId, + long foregroundNotificationUpdateInterval, + @Nullable String channelId, + @StringRes int channelNameResourceId, + @StringRes int channelDescriptionResourceId) { + if (foregroundNotificationId == FOREGROUND_NOTIFICATION_ID_NONE) { + this.foregroundNotificationUpdater = null; + this.channelId = null; + this.channelNameResourceId = 0; + this.channelDescriptionResourceId = 0; + } else { + this.foregroundNotificationUpdater = + new ForegroundNotificationUpdater( + foregroundNotificationId, foregroundNotificationUpdateInterval); + this.channelId = channelId; + this.channelNameResourceId = channelNameResourceId; + this.channelDescriptionResourceId = channelDescriptionResourceId; + } + } + + /** + * Builds an {@link Intent} for adding a new download. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param downloadRequest The request to be executed. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildAddDownloadIntent( + Context context, + Class clazz, + DownloadRequest downloadRequest, + boolean foreground) { + return buildAddDownloadIntent(context, clazz, downloadRequest, STOP_REASON_NONE, foreground); + } + + /** + * Builds an {@link Intent} for adding a new download. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param downloadRequest The request to be executed. + * @param stopReason An initial stop reason for the download, or {@link Download#STOP_REASON_NONE} + * if the download should be started. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildAddDownloadIntent( + Context context, + Class clazz, + DownloadRequest downloadRequest, + int stopReason, + boolean foreground) { + return getIntent(context, clazz, ACTION_ADD_DOWNLOAD, foreground) + .putExtra(KEY_DOWNLOAD_REQUEST, downloadRequest) + .putExtra(KEY_STOP_REASON, stopReason); + } + + /** + * Builds an {@link Intent} for removing the download with the {@code id}. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param id The content id. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildRemoveDownloadIntent( + Context context, Class clazz, String id, boolean foreground) { + return getIntent(context, clazz, ACTION_REMOVE_DOWNLOAD, foreground) + .putExtra(KEY_CONTENT_ID, id); + } + + /** + * Builds an {@link Intent} for removing all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildRemoveAllDownloadsIntent( + Context context, Class clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_REMOVE_ALL_DOWNLOADS, foreground); + } + + /** + * Builds an {@link Intent} for resuming all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildResumeDownloadsIntent( + Context context, Class clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_RESUME_DOWNLOADS, foreground); + } + + /** + * Builds an {@link Intent} to pause all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildPauseDownloadsIntent( + Context context, Class clazz, boolean foreground) { + return getIntent(context, clazz, ACTION_PAUSE_DOWNLOADS, foreground); + } + + /** + * Builds an {@link Intent} for setting the stop reason for one or all downloads. To clear the + * stop reason, pass {@link Download#STOP_REASON_NONE}. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param id The content id, or {@code null} to set the stop reason for all downloads. + * @param stopReason An application defined stop reason. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildSetStopReasonIntent( + Context context, + Class clazz, + @Nullable String id, + int stopReason, + boolean foreground) { + return getIntent(context, clazz, ACTION_SET_STOP_REASON, foreground) + .putExtra(KEY_CONTENT_ID, id) + .putExtra(KEY_STOP_REASON, stopReason); + } + + /** + * Builds an {@link Intent} for setting the requirements that need to be met for downloads to + * progress. + * + * @param context A {@link Context}. + * @param clazz The concrete download service being targeted by the intent. + * @param requirements A {@link Requirements}. + * @param foreground Whether this intent will be used to start the service in the foreground. + * @return The created intent. + */ + public static Intent buildSetRequirementsIntent( + Context context, + Class clazz, + Requirements requirements, + boolean foreground) { + return getIntent(context, clazz, ACTION_SET_REQUIREMENTS, foreground) + .putExtra(KEY_REQUIREMENTS, requirements); + } + + /** + * Starts the service if not started already and adds a new download. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param downloadRequest The request to be executed. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendAddDownload( + Context context, + Class clazz, + DownloadRequest downloadRequest, + boolean foreground) { + Intent intent = buildAddDownloadIntent(context, clazz, downloadRequest, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and adds a new download. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param downloadRequest The request to be executed. + * @param stopReason An initial stop reason for the download, or {@link Download#STOP_REASON_NONE} + * if the download should be started. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendAddDownload( + Context context, + Class clazz, + DownloadRequest downloadRequest, + int stopReason, + boolean foreground) { + Intent intent = buildAddDownloadIntent(context, clazz, downloadRequest, stopReason, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and removes a download. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param id The content id. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendRemoveDownload( + Context context, Class clazz, String id, boolean foreground) { + Intent intent = buildRemoveDownloadIntent(context, clazz, id, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and removes all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendRemoveAllDownloads( + Context context, Class clazz, boolean foreground) { + Intent intent = buildRemoveAllDownloadsIntent(context, clazz, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and resumes all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendResumeDownloads( + Context context, Class clazz, boolean foreground) { + Intent intent = buildResumeDownloadsIntent(context, clazz, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and pauses all downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendPauseDownloads( + Context context, Class clazz, boolean foreground) { + Intent intent = buildPauseDownloadsIntent(context, clazz, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and sets the stop reason for one or all downloads. To + * clear stop reason, pass {@link Download#STOP_REASON_NONE}. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param id The content id, or {@code null} to set the stop reason for all downloads. + * @param stopReason An application defined stop reason. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendSetStopReason( + Context context, + Class clazz, + @Nullable String id, + int stopReason, + boolean foreground) { + Intent intent = buildSetStopReasonIntent(context, clazz, id, stopReason, foreground); + startService(context, intent, foreground); + } + + /** + * Starts the service if not started already and sets the requirements that need to be met for + * downloads to progress. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @param requirements A {@link Requirements}. + * @param foreground Whether the service is started in the foreground. + */ + public static void sendSetRequirements( + Context context, + Class clazz, + Requirements requirements, + boolean foreground) { + Intent intent = buildSetRequirementsIntent(context, clazz, requirements, foreground); + startService(context, intent, foreground); + } + + /** + * Starts a download service to resume any ongoing downloads. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @see #startForeground(Context, Class) + */ + public static void start(Context context, Class clazz) { + context.startService(getIntent(context, clazz, ACTION_INIT)); + } + + /** + * Starts the service in the foreground without adding a new download request. If there are any + * not finished downloads and the requirements are met, the service resumes downloading. Otherwise + * it stops immediately. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @see #start(Context, Class) + */ + public static void startForeground(Context context, Class clazz) { + Intent intent = getIntent(context, clazz, ACTION_INIT, true); + Util.startForegroundService(context, intent); + } + + @Override + public void onCreate() { + if (channelId != null) { + NotificationUtil.createNotificationChannel( + this, + channelId, + channelNameResourceId, + channelDescriptionResourceId, + NotificationUtil.IMPORTANCE_LOW); + } + Class clazz = getClass(); + @Nullable DownloadManagerHelper downloadManagerHelper = downloadManagerHelpers.get(clazz); + if (downloadManagerHelper == null) { + boolean foregroundAllowed = foregroundNotificationUpdater != null; + @Nullable Scheduler scheduler = foregroundAllowed ? getScheduler() : null; + downloadManager = getDownloadManager(); + downloadManager.resumeDownloads(); + downloadManagerHelper = + new DownloadManagerHelper( + getApplicationContext(), downloadManager, foregroundAllowed, scheduler, clazz); + downloadManagerHelpers.put(clazz, downloadManagerHelper); + } else { + downloadManager = downloadManagerHelper.downloadManager; + } + downloadManagerHelper.attachService(this); + } + + @Override + public int onStartCommand(@Nullable Intent intent, int flags, int startId) { + lastStartId = startId; + taskRemoved = false; + @Nullable String intentAction = null; + @Nullable String contentId = null; + if (intent != null) { + intentAction = intent.getAction(); + contentId = intent.getStringExtra(KEY_CONTENT_ID); + startedInForeground |= + intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction); + } + // intentAction is null if the service is restarted or no action is specified. + if (intentAction == null) { + intentAction = ACTION_INIT; + } + DownloadManager downloadManager = Assertions.checkNotNull(this.downloadManager); + switch (intentAction) { + case ACTION_INIT: + case ACTION_RESTART: + // Do nothing. + break; + case ACTION_ADD_DOWNLOAD: + @Nullable + DownloadRequest downloadRequest = + Assertions.checkNotNull(intent).getParcelableExtra(KEY_DOWNLOAD_REQUEST); + if (downloadRequest == null) { + Log.e(TAG, "Ignored ADD_DOWNLOAD: Missing " + KEY_DOWNLOAD_REQUEST + " extra"); + } else { + int stopReason = intent.getIntExtra(KEY_STOP_REASON, Download.STOP_REASON_NONE); + downloadManager.addDownload(downloadRequest, stopReason); + } + break; + case ACTION_REMOVE_DOWNLOAD: + if (contentId == null) { + Log.e(TAG, "Ignored REMOVE_DOWNLOAD: Missing " + KEY_CONTENT_ID + " extra"); + } else { + downloadManager.removeDownload(contentId); + } + break; + case ACTION_REMOVE_ALL_DOWNLOADS: + downloadManager.removeAllDownloads(); + break; + case ACTION_RESUME_DOWNLOADS: + downloadManager.resumeDownloads(); + break; + case ACTION_PAUSE_DOWNLOADS: + downloadManager.pauseDownloads(); + break; + case ACTION_SET_STOP_REASON: + if (!Assertions.checkNotNull(intent).hasExtra(KEY_STOP_REASON)) { + Log.e(TAG, "Ignored SET_STOP_REASON: Missing " + KEY_STOP_REASON + " extra"); + } else { + int stopReason = intent.getIntExtra(KEY_STOP_REASON, /* defaultValue= */ 0); + downloadManager.setStopReason(contentId, stopReason); + } + break; + case ACTION_SET_REQUIREMENTS: + @Nullable + Requirements requirements = + Assertions.checkNotNull(intent).getParcelableExtra(KEY_REQUIREMENTS); + if (requirements == null) { + Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra"); + } else { + downloadManager.setRequirements(requirements); + } + break; + default: + Log.e(TAG, "Ignored unrecognized action: " + intentAction); + break; + } + + if (Util.SDK_INT >= 26 && startedInForeground && foregroundNotificationUpdater != null) { + // From API level 26, services started in the foreground are required to show a notification. + foregroundNotificationUpdater.showNotificationIfNotAlready(); + } + + isStopped = false; + if (downloadManager.isIdle()) { + stop(); + } + return START_STICKY; + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + taskRemoved = true; + } + + @Override + public void onDestroy() { + isDestroyed = true; + DownloadManagerHelper downloadManagerHelper = + Assertions.checkNotNull(downloadManagerHelpers.get(getClass())); + downloadManagerHelper.detachService(this); + if (foregroundNotificationUpdater != null) { + foregroundNotificationUpdater.stopPeriodicUpdates(); + } + } + + /** + * Throws {@link UnsupportedOperationException} because this service is not designed to be bound. + */ + @Nullable + @Override + public final IBinder onBind(Intent intent) { + throw new UnsupportedOperationException(); + } + + /** + * Returns a {@link DownloadManager} to be used to downloaded content. Called only once in the + * life cycle of the process. + */ + protected abstract DownloadManager getDownloadManager(); + + /** + * Returns a {@link Scheduler} to restart the service when requirements allowing downloads to take + * place are met. If {@code null}, the service will only be restarted if the process is still in + * memory when the requirements are met. + * + *

This method is not called for services whose {@code foregroundNotificationId} is set to + * {@link #FOREGROUND_NOTIFICATION_ID_NONE}. Such services will only be restarted if the process + * is still in memory and considered non-idle, meaning that it's either in the foreground or was + * backgrounded within the last few minutes. + */ + @Nullable + protected abstract Scheduler getScheduler(); + + /** + * Returns a notification to be displayed when this service running in the foreground. + * + *

Download services that do not wish to run in the foreground should be created by setting the + * {@code foregroundNotificationId} constructor argument to {@link + * #FOREGROUND_NOTIFICATION_ID_NONE}. This method is not called for such services, meaning it can + * be implemented to throw {@link UnsupportedOperationException}. + * + * @param downloads The current downloads. + * @return The foreground notification to display. + */ + protected abstract Notification getForegroundNotification(List downloads); + + /** + * Invalidates the current foreground notification and causes {@link + * #getForegroundNotification(List)} to be invoked again if the service isn't stopped. + */ + protected final void invalidateForegroundNotification() { + if (foregroundNotificationUpdater != null && !isDestroyed) { + foregroundNotificationUpdater.invalidate(); + } + } + + /** + * @deprecated Some state change events may not be delivered to this method. Instead, use {@link + * DownloadManager#addListener(DownloadManager.Listener)} to register a listener directly to + * the {@link DownloadManager} that you return through {@link #getDownloadManager()}. + */ + @Deprecated + protected void onDownloadChanged(Download download) { + // Do nothing. + } + + /** + * @deprecated Some download removal events may not be delivered to this method. Instead, use + * {@link DownloadManager#addListener(DownloadManager.Listener)} to register a listener + * directly to the {@link DownloadManager} that you return through {@link + * #getDownloadManager()}. + */ + @Deprecated + protected void onDownloadRemoved(Download download) { + // Do nothing. + } + + /** + * Called after the service is created, once the downloads are known. + * + * @param downloads The current downloads. + */ + private void notifyDownloads(List downloads) { + if (foregroundNotificationUpdater != null) { + for (int i = 0; i < downloads.size(); i++) { + if (needsStartedService(downloads.get(i).state)) { + foregroundNotificationUpdater.startPeriodicUpdates(); + break; + } + } + } + } + + /** + * Called when the state of a download changes. + * + * @param download The state of the download. + */ + @SuppressWarnings("deprecation") + private void notifyDownloadChanged(Download download) { + onDownloadChanged(download); + if (foregroundNotificationUpdater != null) { + if (needsStartedService(download.state)) { + foregroundNotificationUpdater.startPeriodicUpdates(); + } else { + foregroundNotificationUpdater.invalidate(); + } + } + } + + /** + * Called when a download is removed. + * + * @param download The last state of the download before it was removed. + */ + @SuppressWarnings("deprecation") + private void notifyDownloadRemoved(Download download) { + onDownloadRemoved(download); + if (foregroundNotificationUpdater != null) { + foregroundNotificationUpdater.invalidate(); + } + } + + /** Returns whether the service is stopped. */ + private boolean isStopped() { + return isStopped; + } + + private void stop() { + if (foregroundNotificationUpdater != null) { + foregroundNotificationUpdater.stopPeriodicUpdates(); + } + if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644]. + stopSelf(); + isStopped = true; + } else { + isStopped |= stopSelfResult(lastStartId); + } + } + + private static boolean needsStartedService(@Download.State int state) { + return state == Download.STATE_DOWNLOADING + || state == Download.STATE_REMOVING + || state == Download.STATE_RESTARTING; + } + + private static Intent getIntent( + Context context, Class clazz, String action, boolean foreground) { + return getIntent(context, clazz, action).putExtra(KEY_FOREGROUND, foreground); + } + + private static Intent getIntent( + Context context, Class clazz, String action) { + return new Intent(context, clazz).setAction(action); + } + + private static void startService(Context context, Intent intent, boolean foreground) { + if (foreground) { + Util.startForegroundService(context, intent); + } else { + context.startService(intent); + } + } + + private final class ForegroundNotificationUpdater { + + private final int notificationId; + private final long updateInterval; + private final Handler handler; + + private boolean periodicUpdatesStarted; + private boolean notificationDisplayed; + + public ForegroundNotificationUpdater(int notificationId, long updateInterval) { + this.notificationId = notificationId; + this.updateInterval = updateInterval; + this.handler = new Handler(Looper.getMainLooper()); + } + + public void startPeriodicUpdates() { + periodicUpdatesStarted = true; + update(); + } + + public void stopPeriodicUpdates() { + periodicUpdatesStarted = false; + handler.removeCallbacksAndMessages(null); + } + + public void showNotificationIfNotAlready() { + if (!notificationDisplayed) { + update(); + } + } + + public void invalidate() { + if (notificationDisplayed) { + update(); + } + } + + private void update() { + List downloads = Assertions.checkNotNull(downloadManager).getCurrentDownloads(); + startForeground(notificationId, getForegroundNotification(downloads)); + notificationDisplayed = true; + if (periodicUpdatesStarted) { + handler.removeCallbacksAndMessages(null); + handler.postDelayed(this::update, updateInterval); + } + } + } + + private static final class DownloadManagerHelper implements DownloadManager.Listener { + + private final Context context; + private final DownloadManager downloadManager; + private final boolean foregroundAllowed; + @Nullable private final Scheduler scheduler; + private final Class serviceClass; + @Nullable private DownloadService downloadService; + + private DownloadManagerHelper( + Context context, + DownloadManager downloadManager, + boolean foregroundAllowed, + @Nullable Scheduler scheduler, + Class serviceClass) { + this.context = context; + this.downloadManager = downloadManager; + this.foregroundAllowed = foregroundAllowed; + this.scheduler = scheduler; + this.serviceClass = serviceClass; + downloadManager.addListener(this); + updateScheduler(); + } + + public void attachService(DownloadService downloadService) { + Assertions.checkState(this.downloadService == null); + this.downloadService = downloadService; + if (downloadManager.isInitialized()) { + // The call to DownloadService.notifyDownloads is posted to avoid it being called directly + // from DownloadService.onCreate. This is a good idea because it may in turn call + // DownloadService.getForegroundNotification, and concrete subclass implementations may + // not anticipate the possibility of this method being called before their onCreate + // implementation has finished executing. + new Handler() + .postAtFrontOfQueue( + () -> downloadService.notifyDownloads(downloadManager.getCurrentDownloads())); + } + } + + public void detachService(DownloadService downloadService) { + Assertions.checkState(this.downloadService == downloadService); + this.downloadService = null; + if (scheduler != null && !downloadManager.isWaitingForRequirements()) { + scheduler.cancel(); + } + } + + // DownloadManager.Listener implementation. + + @Override + public void onInitialized(DownloadManager downloadManager) { + if (downloadService != null) { + downloadService.notifyDownloads(downloadManager.getCurrentDownloads()); + } + } + + @Override + public void onDownloadChanged(DownloadManager downloadManager, Download download) { + if (downloadService != null) { + downloadService.notifyDownloadChanged(download); + } + if (serviceMayNeedRestart() && needsStartedService(download.state)) { + // This shouldn't happen unless (a) application code is changing the downloads by calling + // the DownloadManager directly rather than sending actions through the service, or (b) if + // the service is background only and a previous attempt to start it was prevented. Try and + // restart the service to robust against such cases. + Log.w(TAG, "DownloadService wasn't running. Restarting."); + restartService(); + } + } + + @Override + public void onDownloadRemoved(DownloadManager downloadManager, Download download) { + if (downloadService != null) { + downloadService.notifyDownloadRemoved(download); + } + } + + @Override + public final void onIdle(DownloadManager downloadManager) { + if (downloadService != null) { + downloadService.stop(); + } + } + + @Override + public void onWaitingForRequirementsChanged( + DownloadManager downloadManager, boolean waitingForRequirements) { + if (!waitingForRequirements + && !downloadManager.getDownloadsPaused() + && serviceMayNeedRestart()) { + // We're no longer waiting for requirements and downloads aren't paused, meaning the manager + // will be able to resume downloads that are currently queued. If there exist queued + // downloads then we should ensure the service is started. + List downloads = downloadManager.getCurrentDownloads(); + for (int i = 0; i < downloads.size(); i++) { + if (downloads.get(i).state == Download.STATE_QUEUED) { + restartService(); + break; + } + } + } + updateScheduler(); + } + + // Internal methods. + + private boolean serviceMayNeedRestart() { + return downloadService == null || downloadService.isStopped(); + } + + private void restartService() { + if (foregroundAllowed) { + Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_RESTART); + Util.startForegroundService(context, intent); + } else { + // The service is background only. Use ACTION_INIT rather than ACTION_RESTART because + // ACTION_RESTART is handled as though KEY_FOREGROUND is set to true. + try { + Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT); + context.startService(intent); + } catch (IllegalArgumentException e) { + // The process is classed as idle by the platform. Starting a background service is not + // allowed in this state. + Log.w(TAG, "Failed to restart DownloadService (process is idle)."); + } + } + } + + private void updateScheduler() { + if (scheduler == null) { + return; + } + if (downloadManager.isWaitingForRequirements()) { + String servicePackage = context.getPackageName(); + Requirements requirements = downloadManager.getRequirements(); + boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART); + if (!success) { + Log.e(TAG, "Scheduling downloads failed."); + } + } else { + scheduler.cancel(); + } + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/Downloader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/Downloader.java new file mode 100644 index 000000000000..894d908e72e0 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/Downloader.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import java.io.IOException; + +/** Downloads and removes a piece of content. */ +public interface Downloader { + + /** Receives progress updates during download operations. */ + interface ProgressListener { + + /** + * Called when progress is made during a download operation. + * + * @param contentLength The length of the content in bytes, or {@link C#LENGTH_UNSET} if + * unknown. + * @param bytesDownloaded The number of bytes that have been downloaded. + * @param percentDownloaded The percentage of the content that has been downloaded, or {@link + * C#PERCENTAGE_UNSET}. + */ + void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded); + } + + /** + * Downloads the content. + * + * @param progressListener A listener to receive progress updates, or {@code null}. + * @throws DownloadException Thrown if the content cannot be downloaded. + * @throws InterruptedException If the thread has been interrupted. + * @throws IOException Thrown when there is an io error while downloading. + */ + void download(@Nullable ProgressListener progressListener) + throws InterruptedException, IOException; + + /** Cancels the download operation and prevents future download operations from running. */ + void cancel(); + + /** + * Removes the content. + * + * @throws InterruptedException Thrown if the thread was interrupted. + */ + void remove() throws InterruptedException; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java new file mode 100644 index 000000000000..5b2f579868d6 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSink; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DummyDataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.FileDataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.PriorityDataSourceFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.Cache; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.CacheDataSink; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.CacheDataSinkFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.CacheKeyFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.CacheUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.PriorityTaskManager; + +/** A helper class that holds necessary parameters for {@link Downloader} construction. */ +public final class DownloaderConstructorHelper { + + private final Cache cache; + @Nullable private final CacheKeyFactory cacheKeyFactory; + @Nullable private final PriorityTaskManager priorityTaskManager; + private final CacheDataSourceFactory onlineCacheDataSourceFactory; + private final CacheDataSourceFactory offlineCacheDataSourceFactory; + + /** + * @param cache Cache instance to be used to store downloaded data. + * @param upstreamFactory A {@link DataSource.Factory} for creating {@link DataSource}s for + * downloading data. + */ + public DownloaderConstructorHelper(Cache cache, DataSource.Factory upstreamFactory) { + this( + cache, + upstreamFactory, + /* cacheReadDataSourceFactory= */ null, + /* cacheWriteDataSinkFactory= */ null, + /* priorityTaskManager= */ null); + } + + /** + * @param cache Cache instance to be used to store downloaded data. + * @param upstreamFactory A {@link DataSource.Factory} for creating {@link DataSource}s for + * downloading data. + * @param cacheReadDataSourceFactory A {@link DataSource.Factory} for creating {@link DataSource}s + * for reading data from the cache. If null then a {@link FileDataSource.Factory} will be + * used. + * @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for creating {@link DataSource}s + * for writing data to the cache. If null then a {@link CacheDataSinkFactory} will be used. + * @param priorityTaskManager A {@link PriorityTaskManager} to use when downloading. If non-null, + * downloaders will register as tasks with priority {@link C#PRIORITY_DOWNLOAD} whilst + * downloading. + */ + public DownloaderConstructorHelper( + Cache cache, + DataSource.Factory upstreamFactory, + @Nullable DataSource.Factory cacheReadDataSourceFactory, + @Nullable DataSink.Factory cacheWriteDataSinkFactory, + @Nullable PriorityTaskManager priorityTaskManager) { + this( + cache, + upstreamFactory, + cacheReadDataSourceFactory, + cacheWriteDataSinkFactory, + priorityTaskManager, + /* cacheKeyFactory= */ null); + } + + /** + * @param cache Cache instance to be used to store downloaded data. + * @param upstreamFactory A {@link DataSource.Factory} for creating {@link DataSource}s for + * downloading data. + * @param cacheReadDataSourceFactory A {@link DataSource.Factory} for creating {@link DataSource}s + * for reading data from the cache. If null then a {@link FileDataSource.Factory} will be + * used. + * @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for creating {@link DataSource}s + * for writing data to the cache. If null then a {@link CacheDataSinkFactory} will be used. + * @param priorityTaskManager A {@link PriorityTaskManager} to use when downloading. If non-null, + * downloaders will register as tasks with priority {@link C#PRIORITY_DOWNLOAD} whilst + * downloading. + * @param cacheKeyFactory An optional factory for cache keys. + */ + public DownloaderConstructorHelper( + Cache cache, + DataSource.Factory upstreamFactory, + @Nullable DataSource.Factory cacheReadDataSourceFactory, + @Nullable DataSink.Factory cacheWriteDataSinkFactory, + @Nullable PriorityTaskManager priorityTaskManager, + @Nullable CacheKeyFactory cacheKeyFactory) { + if (priorityTaskManager != null) { + upstreamFactory = + new PriorityDataSourceFactory(upstreamFactory, priorityTaskManager, C.PRIORITY_DOWNLOAD); + } + DataSource.Factory readDataSourceFactory = + cacheReadDataSourceFactory != null + ? cacheReadDataSourceFactory + : new FileDataSource.Factory(); + if (cacheWriteDataSinkFactory == null) { + cacheWriteDataSinkFactory = + new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE); + } + onlineCacheDataSourceFactory = + new CacheDataSourceFactory( + cache, + upstreamFactory, + readDataSourceFactory, + cacheWriteDataSinkFactory, + CacheDataSource.FLAG_BLOCK_ON_CACHE, + /* eventListener= */ null, + cacheKeyFactory); + offlineCacheDataSourceFactory = + new CacheDataSourceFactory( + cache, + DummyDataSource.FACTORY, + readDataSourceFactory, + null, + CacheDataSource.FLAG_BLOCK_ON_CACHE, + /* eventListener= */ null, + cacheKeyFactory); + this.cache = cache; + this.priorityTaskManager = priorityTaskManager; + this.cacheKeyFactory = cacheKeyFactory; + } + + /** Returns the {@link Cache} instance. */ + public Cache getCache() { + return cache; + } + + /** Returns the {@link CacheKeyFactory}. */ + public CacheKeyFactory getCacheKeyFactory() { + return cacheKeyFactory != null ? cacheKeyFactory : CacheUtil.DEFAULT_CACHE_KEY_FACTORY; + } + + /** Returns a {@link PriorityTaskManager} instance. */ + public PriorityTaskManager getPriorityTaskManager() { + // Return a dummy PriorityTaskManager if none is provided. Create a new PriorityTaskManager + // each time so clients don't affect each other over the dummy PriorityTaskManager instance. + return priorityTaskManager != null ? priorityTaskManager : new PriorityTaskManager(); + } + + /** Returns a new {@link CacheDataSource} instance. */ + public CacheDataSource createCacheDataSource() { + return onlineCacheDataSourceFactory.createDataSource(); + } + + /** + * Returns a new {@link CacheDataSource} instance which accesses cache read-only and throws an + * exception on cache miss. + */ + public CacheDataSource createOfflineCacheDataSource() { + return offlineCacheDataSourceFactory.createDataSource(); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloaderFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloaderFactory.java new file mode 100644 index 000000000000..944f55f16169 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloaderFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +/** Creates {@link Downloader Downloaders} for given {@link DownloadRequest DownloadRequests}. */ +public interface DownloaderFactory { + + /** + * Creates a {@link Downloader} to perform the given {@link DownloadRequest}. + * + * @param action The action. + * @return The downloader. + */ + Downloader createDownloader(DownloadRequest action); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/FilterableManifest.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/FilterableManifest.java new file mode 100644 index 000000000000..1bd32f7d4580 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/FilterableManifest.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import java.util.List; + +/** + * A manifest that can generate copies of itself including only the streams specified by the given + * keys. + * + * @param The manifest type. + */ +public interface FilterableManifest { + + /** + * Returns a copy of the manifest including only the streams specified by the given keys. If the + * manifest is unchanged then the instance may return itself. + * + * @param streamKeys A non-empty list of stream keys. + * @return The filtered manifest. + */ + T copy(List streamKeys); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/FilteringManifestParser.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/FilteringManifestParser.java new file mode 100644 index 000000000000..a34d749039b3 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/FilteringManifestParser.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import android.net.Uri; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.ParsingLoadable.Parser; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +/** + * A manifest parser that includes only the streams identified by the given stream keys. + * + * @param The {@link FilterableManifest} type. + */ +public final class FilteringManifestParser> implements Parser { + + private final Parser parser; + @Nullable private final List streamKeys; + + /** + * @param parser A parser for the manifest that will be filtered. + * @param streamKeys The stream keys. If null or empty then filtering will not occur. + */ + public FilteringManifestParser(Parser parser, @Nullable List streamKeys) { + this.parser = parser; + this.streamKeys = streamKeys; + } + + @Override + public T parse(Uri uri, InputStream inputStream) throws IOException { + T manifest = parser.parse(uri, inputStream); + return streamKeys == null || streamKeys.isEmpty() ? manifest : manifest.copy(streamKeys); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ProgressiveDownloader.java new file mode 100644 index 000000000000..7437dab5cac9 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ProgressiveDownloader.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import android.net.Uri; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.Cache; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.CacheKeyFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.CacheUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.PriorityTaskManager; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A downloader for progressive media streams. + * + *

The downloader attempts to download the entire media bytes referenced by a {@link Uri} into a + * cache as defined by {@link DownloaderConstructorHelper}. Callers can use the constructor to + * specify a custom cache key for the downloaded bytes. + * + *

The downloader will avoid downloading already-downloaded media bytes. + */ +public final class ProgressiveDownloader implements Downloader { + + private static final int BUFFER_SIZE_BYTES = 128 * 1024; + + private final DataSpec dataSpec; + private final Cache cache; + private final CacheDataSource dataSource; + private final CacheKeyFactory cacheKeyFactory; + private final PriorityTaskManager priorityTaskManager; + private final AtomicBoolean isCanceled; + + /** + * @param uri Uri of the data to be downloaded. + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache + * indexing. May be null. + * @param constructorHelper A {@link DownloaderConstructorHelper} instance. + */ + public ProgressiveDownloader( + Uri uri, @Nullable String customCacheKey, DownloaderConstructorHelper constructorHelper) { + this.dataSpec = + new DataSpec( + uri, + /* absoluteStreamPosition= */ 0, + C.LENGTH_UNSET, + customCacheKey, + /* flags= */ DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION); + this.cache = constructorHelper.getCache(); + this.dataSource = constructorHelper.createCacheDataSource(); + this.cacheKeyFactory = constructorHelper.getCacheKeyFactory(); + this.priorityTaskManager = constructorHelper.getPriorityTaskManager(); + isCanceled = new AtomicBoolean(); + } + + @Override + public void download(@Nullable ProgressListener progressListener) + throws InterruptedException, IOException { + priorityTaskManager.add(C.PRIORITY_DOWNLOAD); + try { + CacheUtil.cache( + dataSpec, + cache, + cacheKeyFactory, + dataSource, + new byte[BUFFER_SIZE_BYTES], + priorityTaskManager, + C.PRIORITY_DOWNLOAD, + progressListener == null ? null : new ProgressForwarder(progressListener), + isCanceled, + /* enableEOFException= */ true); + } finally { + priorityTaskManager.remove(C.PRIORITY_DOWNLOAD); + } + } + + @Override + public void cancel() { + isCanceled.set(true); + } + + @Override + public void remove() { + CacheUtil.remove(dataSpec, cache, cacheKeyFactory); + } + + private static final class ProgressForwarder implements CacheUtil.ProgressListener { + + private final ProgressListener progessListener; + + public ProgressForwarder(ProgressListener progressListener) { + this.progessListener = progressListener; + } + + @Override + public void onProgress(long contentLength, long bytesCached, long newBytesCached) { + float percentDownloaded = + contentLength == C.LENGTH_UNSET || contentLength == 0 + ? C.PERCENTAGE_UNSET + : ((bytesCached * 100f) / contentLength); + progessListener.onProgress(contentLength, bytesCached, percentDownloaded); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/SegmentDownloader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/SegmentDownloader.java new file mode 100644 index 000000000000..92947b9bc95e --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import android.net.Uri; +import android.util.Pair; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.Cache; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.CacheKeyFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.CacheUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.PriorityTaskManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Base class for multi segment stream downloaders. + * + * @param The type of the manifest object. + */ +public abstract class SegmentDownloader> implements Downloader { + + /** Smallest unit of content to be downloaded. */ + protected static class Segment implements Comparable { + + /** The start time of the segment in microseconds. */ + public final long startTimeUs; + + /** The {@link DataSpec} of the segment. */ + public final DataSpec dataSpec; + + /** Constructs a Segment. */ + public Segment(long startTimeUs, DataSpec dataSpec) { + this.startTimeUs = startTimeUs; + this.dataSpec = dataSpec; + } + + @Override + public int compareTo(Segment other) { + return Util.compareLong(startTimeUs, other.startTimeUs); + } + } + + private static final int BUFFER_SIZE_BYTES = 128 * 1024; + + private final DataSpec manifestDataSpec; + private final Cache cache; + private final CacheDataSource dataSource; + private final CacheDataSource offlineDataSource; + private final CacheKeyFactory cacheKeyFactory; + private final PriorityTaskManager priorityTaskManager; + private final ArrayList streamKeys; + private final AtomicBoolean isCanceled; + + /** + * @param manifestUri The {@link Uri} of the manifest to be downloaded. + * @param streamKeys Keys defining which streams in the manifest should be selected for download. + * If empty, all streams are downloaded. + * @param constructorHelper A {@link DownloaderConstructorHelper} instance. + */ + public SegmentDownloader( + Uri manifestUri, List streamKeys, DownloaderConstructorHelper constructorHelper) { + this.manifestDataSpec = getCompressibleDataSpec(manifestUri); + this.streamKeys = new ArrayList<>(streamKeys); + this.cache = constructorHelper.getCache(); + this.dataSource = constructorHelper.createCacheDataSource(); + this.offlineDataSource = constructorHelper.createOfflineCacheDataSource(); + this.cacheKeyFactory = constructorHelper.getCacheKeyFactory(); + this.priorityTaskManager = constructorHelper.getPriorityTaskManager(); + isCanceled = new AtomicBoolean(); + } + + /** + * Downloads the selected streams in the media. If multiple streams are selected, they are + * downloaded in sync with one another. + * + * @throws IOException Thrown when there is an error downloading. + * @throws InterruptedException If the thread has been interrupted. + */ + @Override + public final void download(@Nullable ProgressListener progressListener) + throws IOException, InterruptedException { + priorityTaskManager.add(C.PRIORITY_DOWNLOAD); + try { + // Get the manifest and all of the segments. + M manifest = getManifest(dataSource, manifestDataSpec); + if (!streamKeys.isEmpty()) { + manifest = manifest.copy(streamKeys); + } + List segments = getSegments(dataSource, manifest, /* allowIncompleteList= */ false); + + // Scan the segments, removing any that are fully downloaded. + int totalSegments = segments.size(); + int segmentsDownloaded = 0; + long contentLength = 0; + long bytesDownloaded = 0; + for (int i = segments.size() - 1; i >= 0; i--) { + Segment segment = segments.get(i); + Pair segmentLengthAndBytesDownloaded = + CacheUtil.getCached(segment.dataSpec, cache, cacheKeyFactory); + long segmentLength = segmentLengthAndBytesDownloaded.first; + long segmentBytesDownloaded = segmentLengthAndBytesDownloaded.second; + bytesDownloaded += segmentBytesDownloaded; + if (segmentLength != C.LENGTH_UNSET) { + if (segmentLength == segmentBytesDownloaded) { + // The segment is fully downloaded. + segmentsDownloaded++; + segments.remove(i); + } + if (contentLength != C.LENGTH_UNSET) { + contentLength += segmentLength; + } + } else { + contentLength = C.LENGTH_UNSET; + } + } + Collections.sort(segments); + + // Download the segments. + @Nullable ProgressNotifier progressNotifier = null; + if (progressListener != null) { + progressNotifier = + new ProgressNotifier( + progressListener, + contentLength, + totalSegments, + bytesDownloaded, + segmentsDownloaded); + } + byte[] buffer = new byte[BUFFER_SIZE_BYTES]; + for (int i = 0; i < segments.size(); i++) { + CacheUtil.cache( + segments.get(i).dataSpec, + cache, + cacheKeyFactory, + dataSource, + buffer, + priorityTaskManager, + C.PRIORITY_DOWNLOAD, + progressNotifier, + isCanceled, + true); + if (progressNotifier != null) { + progressNotifier.onSegmentDownloaded(); + } + } + } finally { + priorityTaskManager.remove(C.PRIORITY_DOWNLOAD); + } + } + + @Override + public void cancel() { + isCanceled.set(true); + } + + @Override + public final void remove() throws InterruptedException { + try { + M manifest = getManifest(offlineDataSource, manifestDataSpec); + List segments = getSegments(offlineDataSource, manifest, true); + for (int i = 0; i < segments.size(); i++) { + removeDataSpec(segments.get(i).dataSpec); + } + } catch (IOException e) { + // Ignore exceptions when removing. + } finally { + // Always attempt to remove the manifest. + removeDataSpec(manifestDataSpec); + } + } + + // Internal methods. + + /** + * Loads and parses the manifest. + * + * @param dataSource The {@link DataSource} through which to load. + * @param dataSpec The manifest {@link DataSpec}. + * @return The manifest. + * @throws IOException If an error occurs reading data. + */ + protected abstract M getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException; + + /** + * Returns a list of all downloadable {@link Segment}s for a given manifest. + * + * @param dataSource The {@link DataSource} through which to load any required data. + * @param manifest The manifest containing the segments. + * @param allowIncompleteList Whether to continue in the case that a load error prevents all + * segments from being listed. If true then a partial segment list will be returned. If false + * an {@link IOException} will be thrown. + * @return The list of downloadable {@link Segment}s. + * @throws InterruptedException Thrown if the thread was interrupted. + * @throws IOException Thrown if {@code allowPartialIndex} is false and a load error occurs, or if + * the media is not in a form that allows for its segments to be listed. + */ + protected abstract List getSegments( + DataSource dataSource, M manifest, boolean allowIncompleteList) + throws InterruptedException, IOException; + + private void removeDataSpec(DataSpec dataSpec) { + CacheUtil.remove(dataSpec, cache, cacheKeyFactory); + } + + protected static DataSpec getCompressibleDataSpec(Uri uri) { + return new DataSpec( + uri, + /* absoluteStreamPosition= */ 0, + /* length= */ C.LENGTH_UNSET, + /* key= */ null, + /* flags= */ DataSpec.FLAG_ALLOW_GZIP); + } + + private static final class ProgressNotifier implements CacheUtil.ProgressListener { + + private final ProgressListener progressListener; + + private final long contentLength; + private final int totalSegments; + + private long bytesDownloaded; + private int segmentsDownloaded; + + public ProgressNotifier( + ProgressListener progressListener, + long contentLength, + int totalSegments, + long bytesDownloaded, + int segmentsDownloaded) { + this.progressListener = progressListener; + this.contentLength = contentLength; + this.totalSegments = totalSegments; + this.bytesDownloaded = bytesDownloaded; + this.segmentsDownloaded = segmentsDownloaded; + } + + @Override + public void onProgress(long requestLength, long bytesCached, long newBytesCached) { + bytesDownloaded += newBytesCached; + progressListener.onProgress(contentLength, bytesDownloaded, getPercentDownloaded()); + } + + public void onSegmentDownloaded() { + segmentsDownloaded++; + progressListener.onProgress(contentLength, bytesDownloaded, getPercentDownloaded()); + } + + private float getPercentDownloaded() { + if (contentLength != C.LENGTH_UNSET && contentLength != 0) { + return (bytesDownloaded * 100f) / contentLength; + } else if (totalSegments != 0) { + return (segmentsDownloaded * 100f) / totalSegments; + } else { + return C.PERCENTAGE_UNSET; + } + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/StreamKey.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/StreamKey.java new file mode 100644 index 000000000000..acbcc9afa409 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/StreamKey.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; + +/** + * A key for a subset of media which can be separately loaded (a "stream"). + * + *

The stream key consists of a period index, a group index within the period and a track index + * within the group. The interpretation of these indices depends on the type of media for which the + * stream key is used. + */ +public final class StreamKey implements Comparable, Parcelable { + + /** The period index. */ + public final int periodIndex; + /** The group index. */ + public final int groupIndex; + /** The track index. */ + public final int trackIndex; + + /** + * @param groupIndex The group index. + * @param trackIndex The track index. + */ + public StreamKey(int groupIndex, int trackIndex) { + this(0, groupIndex, trackIndex); + } + + /** + * @param periodIndex The period index. + * @param groupIndex The group index. + * @param trackIndex The track index. + */ + public StreamKey(int periodIndex, int groupIndex, int trackIndex) { + this.periodIndex = periodIndex; + this.groupIndex = groupIndex; + this.trackIndex = trackIndex; + } + + /* package */ StreamKey(Parcel in) { + periodIndex = in.readInt(); + groupIndex = in.readInt(); + trackIndex = in.readInt(); + } + + @Override + public String toString() { + return periodIndex + "." + groupIndex + "." + trackIndex; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + StreamKey that = (StreamKey) o; + return periodIndex == that.periodIndex + && groupIndex == that.groupIndex + && trackIndex == that.trackIndex; + } + + @Override + public int hashCode() { + int result = periodIndex; + result = 31 * result + groupIndex; + result = 31 * result + trackIndex; + return result; + } + + // Comparable implementation. + + @Override + public int compareTo(StreamKey o) { + int result = periodIndex - o.periodIndex; + if (result == 0) { + result = groupIndex - o.groupIndex; + if (result == 0) { + result = trackIndex - o.trackIndex; + } + } + return result; + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(periodIndex); + dest.writeInt(groupIndex); + dest.writeInt(trackIndex); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public StreamKey createFromParcel(Parcel in) { + return new StreamKey(in); + } + + @Override + public StreamKey[] newArray(int size) { + return new StreamKey[size]; + } + }; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/WritableDownloadIndex.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/WritableDownloadIndex.java new file mode 100644 index 000000000000..f57619f0c45e --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/WritableDownloadIndex.java @@ -0,0 +1,87 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import androidx.annotation.WorkerThread; +import java.io.IOException; + +/** A writable index of {@link Download Downloads}. */ +@WorkerThread +public interface WritableDownloadIndex extends DownloadIndex { + + /** + * Adds or replaces a {@link Download}. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @param download The {@link Download} to be added. + * @throws IOException If an error occurs setting the state. + */ + void putDownload(Download download) throws IOException; + + /** + * Removes the download with the given ID. Does nothing if a download with the given ID does not + * exist. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @param id The ID of the download to remove. + * @throws IOException If an error occurs removing the state. + */ + void removeDownload(String id) throws IOException; + + /** + * Sets all {@link Download#STATE_DOWNLOADING} states to {@link Download#STATE_QUEUED}. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @throws IOException If an error occurs updating the state. + */ + void setDownloadingStatesToQueued() throws IOException; + + /** + * Sets all states to {@link Download#STATE_REMOVING}. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @throws IOException If an error occurs updating the state. + */ + void setStatesToRemoving() throws IOException; + + /** + * Sets the stop reason of the downloads in a terminal state ({@link Download#STATE_COMPLETED}, + * {@link Download#STATE_FAILED}). + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @param stopReason The stop reason. + * @throws IOException If an error occurs updating the state. + */ + void setStopReason(int stopReason) throws IOException; + + /** + * Sets the stop reason of the download with the given ID in a terminal state ({@link + * Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). Does nothing if a download with the + * given ID does not exist, or if it's not in a terminal state. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @param id The ID of the download to update. + * @param stopReason The stop reason. + * @throws IOException If an error occurs updating the state. + */ + void setStopReason(String id, int stopReason) throws IOException; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/package-info.java new file mode 100644 index 000000000000..a353e221075c --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.offline; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/package-info.java new file mode 100644 index 000000000000..d9cb1c149378 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/PlatformScheduler.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/PlatformScheduler.java new file mode 100644 index 000000000000..bb866944d450 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/PlatformScheduler.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler; + +import android.annotation.TargetApi; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.PersistableBundle; +import androidx.annotation.RequiresPermission; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * A {@link Scheduler} that uses {@link JobScheduler}. To use this scheduler, you must add {@link + * PlatformSchedulerService} to your manifest: + * + *

{@literal
+ * 
+ * 
+ *
+ * 
+ * }
+ */ +@TargetApi(21) +public final class PlatformScheduler implements Scheduler { + + private static final boolean DEBUG = false; + private static final String TAG = "PlatformScheduler"; + private static final String KEY_SERVICE_ACTION = "service_action"; + private static final String KEY_SERVICE_PACKAGE = "service_package"; + private static final String KEY_REQUIREMENTS = "requirements"; + + private final int jobId; + private final ComponentName jobServiceComponentName; + private final JobScheduler jobScheduler; + + /** + * @param context Any context. + * @param jobId An identifier for the jobs scheduled by this instance. If the same identifier was + * used by a previous instance, anything scheduled by the previous instance will be canceled + * by this instance if {@link #schedule(Requirements, String, String)} or {@link #cancel()} + * are called. + */ + @RequiresPermission(android.Manifest.permission.RECEIVE_BOOT_COMPLETED) + public PlatformScheduler(Context context, int jobId) { + context = context.getApplicationContext(); + this.jobId = jobId; + jobServiceComponentName = new ComponentName(context, PlatformSchedulerService.class); + jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + } + + @Override + public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) { + JobInfo jobInfo = + buildJobInfo(jobId, jobServiceComponentName, requirements, serviceAction, servicePackage); + int result = jobScheduler.schedule(jobInfo); + logd("Scheduling job: " + jobId + " result: " + result); + return result == JobScheduler.RESULT_SUCCESS; + } + + @Override + public boolean cancel() { + logd("Canceling job: " + jobId); + jobScheduler.cancel(jobId); + return true; + } + + // @RequiresPermission constructor annotation should ensure the permission is present. + @SuppressWarnings("MissingPermission") + private static JobInfo buildJobInfo( + int jobId, + ComponentName jobServiceComponentName, + Requirements requirements, + String serviceAction, + String servicePackage) { + JobInfo.Builder builder = new JobInfo.Builder(jobId, jobServiceComponentName); + + if (requirements.isUnmeteredNetworkRequired()) { + builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED); + } else if (requirements.isNetworkRequired()) { + builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + } + builder.setRequiresDeviceIdle(requirements.isIdleRequired()); + builder.setRequiresCharging(requirements.isChargingRequired()); + builder.setPersisted(true); + + PersistableBundle extras = new PersistableBundle(); + extras.putString(KEY_SERVICE_ACTION, serviceAction); + extras.putString(KEY_SERVICE_PACKAGE, servicePackage); + extras.putInt(KEY_REQUIREMENTS, requirements.getRequirements()); + builder.setExtras(extras); + + return builder.build(); + } + + private static void logd(String message) { + if (DEBUG) { + Log.d(TAG, message); + } + } + + /** A {@link JobService} that starts the target service if the requirements are met. */ + public static final class PlatformSchedulerService extends JobService { + @Override + public boolean onStartJob(JobParameters params) { + logd("PlatformSchedulerService started"); + PersistableBundle extras = params.getExtras(); + Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS)); + if (requirements.checkRequirements(this)) { + logd("Requirements are met"); + String serviceAction = extras.getString(KEY_SERVICE_ACTION); + String servicePackage = extras.getString(KEY_SERVICE_PACKAGE); + Intent intent = + new Intent(Assertions.checkNotNull(serviceAction)).setPackage(servicePackage); + logd("Starting service action: " + serviceAction + " package: " + servicePackage); + Util.startForegroundService(this, intent); + } else { + logd("Requirements are not met"); + jobFinished(params, /* needsReschedule */ true); + } + return false; + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Requirements.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Requirements.java new file mode 100644 index 000000000000..9ef8fdb3f668 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Requirements.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.os.BatteryManager; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PowerManager; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Defines a set of device state requirements. */ +public final class Requirements implements Parcelable { + + /** + * Requirement flags. Possible flag values are {@link #NETWORK}, {@link #NETWORK_UNMETERED}, + * {@link #DEVICE_IDLE} and {@link #DEVICE_CHARGING}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {NETWORK, NETWORK_UNMETERED, DEVICE_IDLE, DEVICE_CHARGING}) + public @interface RequirementFlags {} + + /** Requirement that the device has network connectivity. */ + public static final int NETWORK = 1; + /** Requirement that the device has a network connection that is unmetered. */ + public static final int NETWORK_UNMETERED = 1 << 1; + /** Requirement that the device is idle. */ + public static final int DEVICE_IDLE = 1 << 2; + /** Requirement that the device is charging. */ + public static final int DEVICE_CHARGING = 1 << 3; + + @RequirementFlags private final int requirements; + + /** @param requirements A combination of requirement flags. */ + public Requirements(@RequirementFlags int requirements) { + if ((requirements & NETWORK_UNMETERED) != 0) { + // Make sure network requirement flags are consistent. + requirements |= NETWORK; + } + this.requirements = requirements; + } + + /** Returns the requirements. */ + @RequirementFlags + public int getRequirements() { + return requirements; + } + + /** Returns whether network connectivity is required. */ + public boolean isNetworkRequired() { + return (requirements & NETWORK) != 0; + } + + /** Returns whether un-metered network connectivity is required. */ + public boolean isUnmeteredNetworkRequired() { + return (requirements & NETWORK_UNMETERED) != 0; + } + + /** Returns whether the device is required to be charging. */ + public boolean isChargingRequired() { + return (requirements & DEVICE_CHARGING) != 0; + } + + /** Returns whether the device is required to be idle. */ + public boolean isIdleRequired() { + return (requirements & DEVICE_IDLE) != 0; + } + + /** + * Returns whether the requirements are met. + * + * @param context Any context. + * @return Whether the requirements are met. + */ + public boolean checkRequirements(Context context) { + return getNotMetRequirements(context) == 0; + } + + /** + * Returns requirements that are not met, or 0. + * + * @param context Any context. + * @return The requirements that are not met, or 0. + */ + @RequirementFlags + public int getNotMetRequirements(Context context) { + @RequirementFlags int notMetRequirements = getNotMetNetworkRequirements(context); + if (isChargingRequired() && !isDeviceCharging(context)) { + notMetRequirements |= DEVICE_CHARGING; + } + if (isIdleRequired() && !isDeviceIdle(context)) { + notMetRequirements |= DEVICE_IDLE; + } + return notMetRequirements; + } + + @RequirementFlags + private int getNotMetNetworkRequirements(Context context) { + if (!isNetworkRequired()) { + return 0; + } + + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = Assertions.checkNotNull(connectivityManager).getActiveNetworkInfo(); + if (networkInfo == null + || !networkInfo.isConnected() + || !isInternetConnectivityValidated(connectivityManager)) { + return requirements & (NETWORK | NETWORK_UNMETERED); + } + + if (isUnmeteredNetworkRequired() && connectivityManager.isActiveNetworkMetered()) { + return NETWORK_UNMETERED; + } + + return 0; + } + + private boolean isDeviceCharging(Context context) { + Intent batteryStatus = + context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + if (batteryStatus == null) { + return false; + } + int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); + return status == BatteryManager.BATTERY_STATUS_CHARGING + || status == BatteryManager.BATTERY_STATUS_FULL; + } + + private boolean isDeviceIdle(Context context) { + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return Util.SDK_INT >= 23 + ? powerManager.isDeviceIdleMode() + : Util.SDK_INT >= 20 ? !powerManager.isInteractive() : !powerManager.isScreenOn(); + } + + private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) { + // It's possible to query NetworkCapabilities from API level 23, but RequirementsWatcher only + // fires an event to update its Requirements when NetworkCapabilities change from API level 24. + // Since Requirements won't be updated, we assume connectivity is validated on API level 23. + if (Util.SDK_INT < 24) { + return true; + } + Network activeNetwork = connectivityManager.getActiveNetwork(); + if (activeNetwork == null) { + return false; + } + NetworkCapabilities networkCapabilities = + connectivityManager.getNetworkCapabilities(activeNetwork); + return networkCapabilities != null + && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return requirements == ((Requirements) o).requirements; + } + + @Override + public int hashCode() { + return requirements; + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(requirements); + } + + public static final Parcelable.Creator CREATOR = + new Creator() { + + @Override + public Requirements createFromParcel(Parcel in) { + return new Requirements(in.readInt()); + } + + @Override + public Requirements[] newArray(int size) { + return new Requirements[size]; + } + }; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java new file mode 100644 index 000000000000..edb860ac050f --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler; + +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.os.Handler; +import android.os.Looper; +import android.os.PowerManager; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * Watches whether the {@link Requirements} are met and notifies the {@link Listener} on changes. + */ +public final class RequirementsWatcher { + + /** + * Notified when RequirementsWatcher instance first created and on changes whether the {@link + * Requirements} are met. + */ + public interface Listener { + /** + * Called when there is a change on the met requirements. + * + * @param requirementsWatcher Calling instance. + * @param notMetRequirements {@link Requirements.RequirementFlags RequirementFlags} that are not + * met, or 0. + */ + void onRequirementsStateChanged( + RequirementsWatcher requirementsWatcher, + @Requirements.RequirementFlags int notMetRequirements); + } + + private final Context context; + private final Listener listener; + private final Requirements requirements; + private final Handler handler; + + @Nullable private DeviceStatusChangeReceiver receiver; + + @Requirements.RequirementFlags private int notMetRequirements; + @Nullable private NetworkCallback networkCallback; + + /** + * @param context Any context. + * @param listener Notified whether the {@link Requirements} are met. + * @param requirements The requirements to watch. + */ + public RequirementsWatcher(Context context, Listener listener, Requirements requirements) { + this.context = context.getApplicationContext(); + this.listener = listener; + this.requirements = requirements; + handler = new Handler(Util.getLooper()); + } + + /** + * Starts watching for changes. Must be called from a thread that has an associated {@link + * Looper}. Listener methods are called on the caller thread. + * + * @return Initial {@link Requirements.RequirementFlags RequirementFlags} that are not met, or 0. + */ + @Requirements.RequirementFlags + public int start() { + notMetRequirements = requirements.getNotMetRequirements(context); + + IntentFilter filter = new IntentFilter(); + if (requirements.isNetworkRequired()) { + if (Util.SDK_INT >= 24) { + registerNetworkCallbackV24(); + } else { + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + } + } + if (requirements.isChargingRequired()) { + filter.addAction(Intent.ACTION_POWER_CONNECTED); + filter.addAction(Intent.ACTION_POWER_DISCONNECTED); + } + if (requirements.isIdleRequired()) { + if (Util.SDK_INT >= 23) { + filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); + } else { + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + } + } + receiver = new DeviceStatusChangeReceiver(); + context.registerReceiver(receiver, filter, null, handler); + return notMetRequirements; + } + + /** Stops watching for changes. */ + public void stop() { + context.unregisterReceiver(Assertions.checkNotNull(receiver)); + receiver = null; + if (Util.SDK_INT >= 24 && networkCallback != null) { + unregisterNetworkCallbackV24(); + } + } + + /** Returns watched {@link Requirements}. */ + public Requirements getRequirements() { + return requirements; + } + + @TargetApi(24) + private void registerNetworkCallbackV24() { + ConnectivityManager connectivityManager = + Assertions.checkNotNull( + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)); + networkCallback = new NetworkCallback(); + connectivityManager.registerDefaultNetworkCallback(networkCallback); + } + + @TargetApi(24) + private void unregisterNetworkCallbackV24() { + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + connectivityManager.unregisterNetworkCallback(Assertions.checkNotNull(networkCallback)); + networkCallback = null; + } + + private void checkRequirements() { + @Requirements.RequirementFlags + int notMetRequirements = requirements.getNotMetRequirements(context); + if (this.notMetRequirements != notMetRequirements) { + this.notMetRequirements = notMetRequirements; + listener.onRequirementsStateChanged(this, notMetRequirements); + } + } + + private class DeviceStatusChangeReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (!isInitialStickyBroadcast()) { + checkRequirements(); + } + } + } + + @RequiresApi(24) + private final class NetworkCallback extends ConnectivityManager.NetworkCallback { + boolean receivedCapabilitiesChange; + boolean networkValidated; + + @Override + public void onAvailable(Network network) { + onNetworkCallback(); + } + + @Override + public void onLost(Network network) { + onNetworkCallback(); + } + + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) { + boolean networkValidated = + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + if (!receivedCapabilitiesChange || this.networkValidated != networkValidated) { + receivedCapabilitiesChange = true; + this.networkValidated = networkValidated; + onNetworkCallback(); + } + } + + private void onNetworkCallback() { + handler.post( + () -> { + if (networkCallback != null) { + checkRequirements(); + } + }); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Scheduler.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Scheduler.java new file mode 100644 index 000000000000..c7a7afcd2d02 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Scheduler.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler; + +import android.app.Notification; +import android.app.Service; +import android.content.Intent; + +/** Schedules a service to be started in the foreground when some {@link Requirements} are met. */ +public interface Scheduler { + + /** + * Schedules a service to be started in the foreground when some {@link Requirements} are met. + * Anything that was previously scheduled will be canceled. + * + *

The service to be started must be declared in the manifest of {@code servicePackage} with an + * intent filter containing {@code serviceAction}. Note that when started with {@code + * serviceAction}, the service must call {@link Service#startForeground(int, Notification)} to + * make itself a foreground service, as documented by {@link + * Service#startForegroundService(Intent)}. + * + * @param requirements The requirements. + * @param servicePackage The package name. + * @param serviceAction The action with which the service will be started. + * @return Whether scheduling was successful. + */ + boolean schedule(Requirements requirements, String servicePackage, String serviceAction); + + /** + * Cancels anything that was previously scheduled, or else does nothing. + * + * @return Whether cancellation was successful. + */ + boolean cancel(); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/package-info.java new file mode 100644 index 000000000000..b4e68ebfff9a --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.scheduler; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java new file mode 100644 index 000000000000..1f67f7e6455d --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +import android.util.Pair; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; + +/** Abstract base class for the concatenation of one or more {@link Timeline}s. */ +/* package */ abstract class AbstractConcatenatedTimeline extends Timeline { + + private final int childCount; + private final ShuffleOrder shuffleOrder; + private final boolean isAtomic; + + /** + * Returns UID of child timeline from a concatenated period UID. + * + * @param concatenatedUid UID of a period in a concatenated timeline. + * @return UID of the child timeline this period belongs to. + */ + @SuppressWarnings("nullness:return.type.incompatible") + public static Object getChildTimelineUidFromConcatenatedUid(Object concatenatedUid) { + return ((Pair) concatenatedUid).first; + } + + /** + * Returns UID of the period in the child timeline from a concatenated period UID. + * + * @param concatenatedUid UID of a period in a concatenated timeline. + * @return UID of the period in the child timeline. + */ + @SuppressWarnings("nullness:return.type.incompatible") + public static Object getChildPeriodUidFromConcatenatedUid(Object concatenatedUid) { + return ((Pair) concatenatedUid).second; + } + + /** + * Returns a concatenated UID for a period or window in a child timeline. + * + * @param childTimelineUid UID of the child timeline this period or window belongs to. + * @param childPeriodOrWindowUid UID of the period or window in the child timeline. + * @return UID of the period or window in the concatenated timeline. + */ + public static Object getConcatenatedUid(Object childTimelineUid, Object childPeriodOrWindowUid) { + return Pair.create(childTimelineUid, childPeriodOrWindowUid); + } + + /** + * Sets up a concatenated timeline with a shuffle order of child timelines. + * + * @param isAtomic Whether the child timelines shall be treated as atomic, i.e., treated as a + * single item for repeating and shuffling. + * @param shuffleOrder A shuffle order of child timelines. The number of child timelines must + * match the number of elements in the shuffle order. + */ + public AbstractConcatenatedTimeline(boolean isAtomic, ShuffleOrder shuffleOrder) { + this.isAtomic = isAtomic; + this.shuffleOrder = shuffleOrder; + this.childCount = shuffleOrder.getLength(); + } + + @Override + public int getNextWindowIndex( + int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + if (isAtomic) { + // Adapt repeat and shuffle mode to atomic concatenation. + repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode; + shuffleModeEnabled = false; + } + // Find next window within current child. + int childIndex = getChildIndexByWindowIndex(windowIndex); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + int nextWindowIndexInChild = + getTimelineByChildIndex(childIndex) + .getNextWindowIndex( + windowIndex - firstWindowIndexInChild, + repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, + shuffleModeEnabled); + if (nextWindowIndexInChild != C.INDEX_UNSET) { + return firstWindowIndexInChild + nextWindowIndexInChild; + } + // If not found, find first window of next non-empty child. + int nextChildIndex = getNextChildIndex(childIndex, shuffleModeEnabled); + while (nextChildIndex != C.INDEX_UNSET && getTimelineByChildIndex(nextChildIndex).isEmpty()) { + nextChildIndex = getNextChildIndex(nextChildIndex, shuffleModeEnabled); + } + if (nextChildIndex != C.INDEX_UNSET) { + return getFirstWindowIndexByChildIndex(nextChildIndex) + + getTimelineByChildIndex(nextChildIndex).getFirstWindowIndex(shuffleModeEnabled); + } + // If not found, this is the last window. + if (repeatMode == Player.REPEAT_MODE_ALL) { + return getFirstWindowIndex(shuffleModeEnabled); + } + return C.INDEX_UNSET; + } + + @Override + public int getPreviousWindowIndex( + int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + if (isAtomic) { + // Adapt repeat and shuffle mode to atomic concatenation. + repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode; + shuffleModeEnabled = false; + } + // Find previous window within current child. + int childIndex = getChildIndexByWindowIndex(windowIndex); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + int previousWindowIndexInChild = + getTimelineByChildIndex(childIndex) + .getPreviousWindowIndex( + windowIndex - firstWindowIndexInChild, + repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, + shuffleModeEnabled); + if (previousWindowIndexInChild != C.INDEX_UNSET) { + return firstWindowIndexInChild + previousWindowIndexInChild; + } + // If not found, find last window of previous non-empty child. + int previousChildIndex = getPreviousChildIndex(childIndex, shuffleModeEnabled); + while (previousChildIndex != C.INDEX_UNSET + && getTimelineByChildIndex(previousChildIndex).isEmpty()) { + previousChildIndex = getPreviousChildIndex(previousChildIndex, shuffleModeEnabled); + } + if (previousChildIndex != C.INDEX_UNSET) { + return getFirstWindowIndexByChildIndex(previousChildIndex) + + getTimelineByChildIndex(previousChildIndex).getLastWindowIndex(shuffleModeEnabled); + } + // If not found, this is the first window. + if (repeatMode == Player.REPEAT_MODE_ALL) { + return getLastWindowIndex(shuffleModeEnabled); + } + return C.INDEX_UNSET; + } + + @Override + public int getLastWindowIndex(boolean shuffleModeEnabled) { + if (childCount == 0) { + return C.INDEX_UNSET; + } + if (isAtomic) { + shuffleModeEnabled = false; + } + // Find last non-empty child. + int lastChildIndex = shuffleModeEnabled ? shuffleOrder.getLastIndex() : childCount - 1; + while (getTimelineByChildIndex(lastChildIndex).isEmpty()) { + lastChildIndex = getPreviousChildIndex(lastChildIndex, shuffleModeEnabled); + if (lastChildIndex == C.INDEX_UNSET) { + // All children are empty. + return C.INDEX_UNSET; + } + } + return getFirstWindowIndexByChildIndex(lastChildIndex) + + getTimelineByChildIndex(lastChildIndex).getLastWindowIndex(shuffleModeEnabled); + } + + @Override + public int getFirstWindowIndex(boolean shuffleModeEnabled) { + if (childCount == 0) { + return C.INDEX_UNSET; + } + if (isAtomic) { + shuffleModeEnabled = false; + } + // Find first non-empty child. + int firstChildIndex = shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0; + while (getTimelineByChildIndex(firstChildIndex).isEmpty()) { + firstChildIndex = getNextChildIndex(firstChildIndex, shuffleModeEnabled); + if (firstChildIndex == C.INDEX_UNSET) { + // All children are empty. + return C.INDEX_UNSET; + } + } + return getFirstWindowIndexByChildIndex(firstChildIndex) + + getTimelineByChildIndex(firstChildIndex).getFirstWindowIndex(shuffleModeEnabled); + } + + @Override + public final Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + int childIndex = getChildIndexByWindowIndex(windowIndex); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); + getTimelineByChildIndex(childIndex) + .getWindow(windowIndex - firstWindowIndexInChild, window, defaultPositionProjectionUs); + Object childUid = getChildUidByChildIndex(childIndex); + // Don't create new objects if the child is using SINGLE_WINDOW_UID. + window.uid = + Window.SINGLE_WINDOW_UID.equals(window.uid) + ? childUid + : getConcatenatedUid(childUid, window.uid); + window.firstPeriodIndex += firstPeriodIndexInChild; + window.lastPeriodIndex += firstPeriodIndexInChild; + return window; + } + + @Override + public final Period getPeriodByUid(Object uid, Period period) { + Object childUid = getChildTimelineUidFromConcatenatedUid(uid); + Object periodUid = getChildPeriodUidFromConcatenatedUid(uid); + int childIndex = getChildIndexByChildUid(childUid); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + getTimelineByChildIndex(childIndex).getPeriodByUid(periodUid, period); + period.windowIndex += firstWindowIndexInChild; + period.uid = uid; + return period; + } + + @Override + public final Period getPeriod(int periodIndex, Period period, boolean setIds) { + int childIndex = getChildIndexByPeriodIndex(periodIndex); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); + getTimelineByChildIndex(childIndex) + .getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds); + period.windowIndex += firstWindowIndexInChild; + if (setIds) { + period.uid = + getConcatenatedUid( + getChildUidByChildIndex(childIndex), Assertions.checkNotNull(period.uid)); + } + return period; + } + + @Override + public final int getIndexOfPeriod(Object uid) { + if (!(uid instanceof Pair)) { + return C.INDEX_UNSET; + } + Object childUid = getChildTimelineUidFromConcatenatedUid(uid); + Object periodUid = getChildPeriodUidFromConcatenatedUid(uid); + int childIndex = getChildIndexByChildUid(childUid); + if (childIndex == C.INDEX_UNSET) { + return C.INDEX_UNSET; + } + int periodIndexInChild = getTimelineByChildIndex(childIndex).getIndexOfPeriod(periodUid); + return periodIndexInChild == C.INDEX_UNSET + ? C.INDEX_UNSET + : getFirstPeriodIndexByChildIndex(childIndex) + periodIndexInChild; + } + + @Override + public final Object getUidOfPeriod(int periodIndex) { + int childIndex = getChildIndexByPeriodIndex(periodIndex); + int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); + Object periodUidInChild = + getTimelineByChildIndex(childIndex).getUidOfPeriod(periodIndex - firstPeriodIndexInChild); + return getConcatenatedUid(getChildUidByChildIndex(childIndex), periodUidInChild); + } + + /** + * Returns the index of the child timeline containing the given period index. + * + * @param periodIndex A valid period index within the bounds of the timeline. + */ + protected abstract int getChildIndexByPeriodIndex(int periodIndex); + + /** + * Returns the index of the child timeline containing the given window index. + * + * @param windowIndex A valid window index within the bounds of the timeline. + */ + protected abstract int getChildIndexByWindowIndex(int windowIndex); + + /** + * Returns the index of the child timeline with the given UID or {@link C#INDEX_UNSET} if not + * found. + * + * @param childUid A child UID. + * @return Index of child timeline or {@link C#INDEX_UNSET} if UID was not found. + */ + protected abstract int getChildIndexByChildUid(Object childUid); + + /** + * Returns the child timeline for the child with the given index. + * + * @param childIndex A valid child index within the bounds of the timeline. + */ + protected abstract Timeline getTimelineByChildIndex(int childIndex); + + /** + * Returns the first period index belonging to the child timeline with the given index. + * + * @param childIndex A valid child index within the bounds of the timeline. + */ + protected abstract int getFirstPeriodIndexByChildIndex(int childIndex); + + /** + * Returns the first window index belonging to the child timeline with the given index. + * + * @param childIndex A valid child index within the bounds of the timeline. + */ + protected abstract int getFirstWindowIndexByChildIndex(int childIndex); + + /** + * Returns the UID of the child timeline with the given index. + * + * @param childIndex A valid child index within the bounds of the timeline. + */ + protected abstract Object getChildUidByChildIndex(int childIndex); + + private int getNextChildIndex(int childIndex, boolean shuffleModeEnabled) { + return shuffleModeEnabled + ? shuffleOrder.getNextIndex(childIndex) + : childIndex < childCount - 1 ? childIndex + 1 : C.INDEX_UNSET; + } + + private int getPreviousChildIndex(int childIndex, boolean shuffleModeEnabled) { + return shuffleModeEnabled + ? shuffleOrder.getPreviousIndex(childIndex) + : childIndex > 0 ? childIndex - 1 : C.INDEX_UNSET; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java index 3dabc5263363..dba911f622fb 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java @@ -15,301 +15,10 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source; -import android.os.Handler; -import android.os.SystemClock; -import org.mozilla.thirdparty.com.google.android.exoplayer2.C; -import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; -import java.io.IOException; - /** - * Interface for callbacks to be notified of adaptive {@link MediaSource} events. + * Interface for callbacks to be notified of {@link MediaSource} events. + * + * @deprecated Use {@link MediaSourceEventListener}. */ -public interface AdaptiveMediaSourceEventListener { - - /** - * Called when a load begins. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began. - */ - void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs); - - /** - * Called when a load ends. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load ended. - * @param loadDurationMs The duration of the load. - * @param bytesLoaded The number of bytes that were loaded. - */ - void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded); - - /** - * Called when a load is canceled. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load was - * canceled. - * @param loadDurationMs The duration of the load up to the point at which it was canceled. - * @param bytesLoaded The number of bytes that were loaded prior to cancelation. - */ - void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded); - - /** - * Called when a load error occurs. - *

- * The error may or may not have resulted in the load being canceled, as indicated by the - * {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will - * not be called in addition to this method. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the error - * occurred. - * @param loadDurationMs The duration of the load up to the point at which the error occurred. - * @param bytesLoaded The number of bytes that were loaded prior to the error. - * @param error The load error. - * @param wasCanceled Whether the load was canceled as a result of the error. - */ - void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded, - IOException error, boolean wasCanceled); - - /** - * Called when data is removed from the back of a media buffer, typically so that it can be - * re-buffered in a different format. - * - * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @param mediaStartTimeMs The start time of the media being discarded. - * @param mediaEndTimeMs The end time of the media being discarded. - */ - void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs); - - /** - * Called when a downstream format change occurs (i.e. when the format of the media being read - * from one or more {@link SampleStream}s provided by the source changes). - * - * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaTimeMs The media time at which the change occurred. - */ - void onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, long mediaTimeMs); - - /** - * Dispatches events to a {@link AdaptiveMediaSourceEventListener}. - */ - final class EventDispatcher { - - private final Handler handler; - private final AdaptiveMediaSourceEventListener listener; - private final long mediaTimeOffsetMs; - - public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener) { - this(handler, listener, 0); - } - - public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener, - long mediaTimeOffsetMs) { - this.handler = listener != null ? Assertions.checkNotNull(handler) : null; - this.listener = listener; - this.mediaTimeOffsetMs = mediaTimeOffsetMs; - } - - public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) { - return new EventDispatcher(handler, listener, mediaTimeOffsetMs); - } - - public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { - loadStarted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs); - } - - public void loadStarted(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadStarted(dataSpec, dataType, trackType, trackFormat, trackSelectionReason, - trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs); - } - }); - } - } - - public void loadCompleted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs, - long loadDurationMs, long bytesLoaded) { - loadCompleted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - - public void loadCompleted(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs, - final long loadDurationMs, final long bytesLoaded) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadCompleted(dataSpec, dataType, trackType, trackFormat, - trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - }); - } - } - - public void loadCanceled(DataSpec dataSpec, int dataType, long elapsedRealtimeMs, - long loadDurationMs, long bytesLoaded) { - loadCanceled(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - - public void loadCanceled(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs, - final long loadDurationMs, final long bytesLoaded) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadCanceled(dataSpec, dataType, trackType, trackFormat, - trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - }); - } - } - - public void loadError(DataSpec dataSpec, int dataType, long elapsedRealtimeMs, - long loadDurationMs, long bytesLoaded, IOException error, boolean wasCanceled) { - loadError(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded, - error, wasCanceled); - } - - public void loadError(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs, - final long loadDurationMs, final long bytesLoaded, final IOException error, - final boolean wasCanceled) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadError(dataSpec, dataType, trackType, trackFormat, trackSelectionReason, - trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded, - error, wasCanceled); - } - }); - } - } - - public void upstreamDiscarded(final int trackType, final long mediaStartTimeUs, - final long mediaEndTimeUs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onUpstreamDiscarded(trackType, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs)); - } - }); - } - } - - public void downstreamFormatChanged(final int trackType, final Format trackFormat, - final int trackSelectionReason, final Object trackSelectionData, - final long mediaTimeUs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onDownstreamFormatChanged(trackType, trackFormat, trackSelectionReason, - trackSelectionData, adjustMediaTime(mediaTimeUs)); - } - }); - } - } - - private long adjustMediaTime(long mediaTimeUs) { - long mediaTimeMs = C.usToMs(mediaTimeUs); - return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; - } - - } - -} +@Deprecated +public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener {} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BaseMediaSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BaseMediaSource.java new file mode 100644 index 000000000000..f9ca6ff311e7 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BaseMediaSource.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.util.ArrayList; +import java.util.HashSet; + +/** + * Base {@link MediaSource} implementation to handle parallel reuse and to keep a list of {@link + * MediaSourceEventListener}s. + * + *

Whenever an implementing subclass needs to provide a new timeline, it must call {@link + * #refreshSourceInfo(Timeline)} to notify all listeners. + */ +public abstract class BaseMediaSource implements MediaSource { + + private final ArrayList mediaSourceCallers; + private final HashSet enabledMediaSourceCallers; + private final MediaSourceEventListener.EventDispatcher eventDispatcher; + + @Nullable private Looper looper; + @Nullable private Timeline timeline; + + public BaseMediaSource() { + mediaSourceCallers = new ArrayList<>(/* initialCapacity= */ 1); + enabledMediaSourceCallers = new HashSet<>(/* initialCapacity= */ 1); + eventDispatcher = new MediaSourceEventListener.EventDispatcher(); + } + + /** + * Starts source preparation and enables the source, see {@link #prepareSource(MediaSourceCaller, + * TransferListener)}. This method is called at most once until the next call to {@link + * #releaseSourceInternal()}. + * + * @param mediaTransferListener The transfer listener which should be informed of any media data + * transfers. May be null if no listener is available. Note that this listener should usually + * be only informed of transfers related to the media loads and not of auxiliary loads for + * manifests and other data. + */ + protected abstract void prepareSourceInternal(@Nullable TransferListener mediaTransferListener); + + /** Enables the source, see {@link #enable(MediaSourceCaller)}. */ + protected void enableInternal() {} + + /** Disables the source, see {@link #disable(MediaSourceCaller)}. */ + protected void disableInternal() {} + + /** + * Releases the source, see {@link #releaseSource(MediaSourceCaller)}. This method is called + * exactly once after each call to {@link #prepareSourceInternal(TransferListener)}. + */ + protected abstract void releaseSourceInternal(); + + /** + * Updates timeline and manifest and notifies all listeners of the update. + * + * @param timeline The new {@link Timeline}. + */ + protected final void refreshSourceInfo(Timeline timeline) { + this.timeline = timeline; + for (MediaSourceCaller caller : mediaSourceCallers) { + caller.onSourceInfoRefreshed(/* source= */ this, timeline); + } + } + + /** + * Returns a {@link MediaSourceEventListener.EventDispatcher} which dispatches all events to the + * registered listeners with the specified media period id. + * + * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events. May be null, if + * the events do not belong to a specific media period. + * @return An event dispatcher with pre-configured media period id. + */ + protected final MediaSourceEventListener.EventDispatcher createEventDispatcher( + @Nullable MediaPeriodId mediaPeriodId) { + return eventDispatcher.withParameters( + /* windowIndex= */ 0, mediaPeriodId, /* mediaTimeOffsetMs= */ 0); + } + + /** + * Returns a {@link MediaSourceEventListener.EventDispatcher} which dispatches all events to the + * registered listeners with the specified media period id and time offset. + * + * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events. + * @param mediaTimeOffsetMs The offset to be added to all media times, in milliseconds. + * @return An event dispatcher with pre-configured media period id and time offset. + */ + protected final MediaSourceEventListener.EventDispatcher createEventDispatcher( + MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { + Assertions.checkArgument(mediaPeriodId != null); + return eventDispatcher.withParameters(/* windowIndex= */ 0, mediaPeriodId, mediaTimeOffsetMs); + } + + /** + * Returns a {@link MediaSourceEventListener.EventDispatcher} which dispatches all events to the + * registered listeners with the specified window index, media period id and time offset. + * + * @param windowIndex The timeline window index to be reported with the events. + * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events. May be null, if + * the events do not belong to a specific media period. + * @param mediaTimeOffsetMs The offset to be added to all media times, in milliseconds. + * @return An event dispatcher with pre-configured media period id and time offset. + */ + protected final MediaSourceEventListener.EventDispatcher createEventDispatcher( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { + return eventDispatcher.withParameters(windowIndex, mediaPeriodId, mediaTimeOffsetMs); + } + + /** Returns whether the source is enabled. */ + protected final boolean isEnabled() { + return !enabledMediaSourceCallers.isEmpty(); + } + + @Override + public final void addEventListener(Handler handler, MediaSourceEventListener eventListener) { + eventDispatcher.addEventListener(handler, eventListener); + } + + @Override + public final void removeEventListener(MediaSourceEventListener eventListener) { + eventDispatcher.removeEventListener(eventListener); + } + + @Override + public final void prepareSource( + MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) { + Looper looper = Looper.myLooper(); + Assertions.checkArgument(this.looper == null || this.looper == looper); + Timeline timeline = this.timeline; + mediaSourceCallers.add(caller); + if (this.looper == null) { + this.looper = looper; + enabledMediaSourceCallers.add(caller); + prepareSourceInternal(mediaTransferListener); + } else if (timeline != null) { + enable(caller); + caller.onSourceInfoRefreshed(/* source= */ this, timeline); + } + } + + @Override + public final void enable(MediaSourceCaller caller) { + Assertions.checkNotNull(looper); + boolean wasDisabled = enabledMediaSourceCallers.isEmpty(); + enabledMediaSourceCallers.add(caller); + if (wasDisabled) { + enableInternal(); + } + } + + @Override + public final void disable(MediaSourceCaller caller) { + boolean wasEnabled = !enabledMediaSourceCallers.isEmpty(); + enabledMediaSourceCallers.remove(caller); + if (wasEnabled && enabledMediaSourceCallers.isEmpty()) { + disableInternal(); + } + } + + @Override + public final void releaseSource(MediaSourceCaller caller) { + mediaSourceCallers.remove(caller); + if (mediaSourceCallers.isEmpty()) { + looper = null; + timeline = null; + enabledMediaSourceCallers.clear(); + releaseSourceInternal(); + } else { + disable(caller); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 041d8c0c155d..7467d946ccab 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -15,12 +15,18 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Wraps a {@link MediaPeriod} and clips its {@link SampleStream}s to provide a subsequence of their @@ -33,44 +39,51 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb */ public final MediaPeriod mediaPeriod; - private MediaPeriod.Callback callback; - private long startUs; - private long endUs; - private ClippingSampleStream[] sampleStreams; - private boolean pendingInitialDiscontinuity; + @Nullable private MediaPeriod.Callback callback; + private @NullableType ClippingSampleStream[] sampleStreams; + private long pendingInitialDiscontinuityPositionUs; + /* package */ long startUs; + /* package */ long endUs; /** - * Creates a new clipping media period that provides a clipped view of the specified - * {@link MediaPeriod}'s sample streams. - *

- * The clipping start/end positions must be specified by calling {@link #setClipping(long, long)} - * on the playback thread before preparation completes. + * Creates a new clipping media period that provides a clipped view of the specified {@link + * MediaPeriod}'s sample streams. + * + *

If the start point is guaranteed to be a key frame, pass {@code false} to {@code + * enableInitialPositionDiscontinuity} to suppress an initial discontinuity when the period is + * first read from. * * @param mediaPeriod The media period to clip. + * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled. + * @param startUs The clipping start time, in microseconds. + * @param endUs The clipping end time, in microseconds, or {@link C#TIME_END_OF_SOURCE} to + * indicate the end of the period. */ - public ClippingMediaPeriod(MediaPeriod mediaPeriod) { + public ClippingMediaPeriod( + MediaPeriod mediaPeriod, boolean enableInitialDiscontinuity, long startUs, long endUs) { this.mediaPeriod = mediaPeriod; - startUs = C.TIME_UNSET; - endUs = C.TIME_UNSET; sampleStreams = new ClippingSampleStream[0]; + pendingInitialDiscontinuityPositionUs = enableInitialDiscontinuity ? startUs : C.TIME_UNSET; + this.startUs = startUs; + this.endUs = endUs; } /** - * Sets the clipping start/end times for this period, in microseconds. + * Updates the clipping start/end times for this period, in microseconds. * * @param startUs The clipping start time, in microseconds. * @param endUs The clipping end time, in microseconds, or {@link C#TIME_END_OF_SOURCE} to * indicate the end of the period. */ - public void setClipping(long startUs, long endUs) { + public void updateClipping(long startUs, long endUs) { this.startUs = startUs; this.endUs = endUs; } @Override - public void prepare(MediaPeriod.Callback callback) { + public void prepare(MediaPeriod.Callback callback, long positionUs) { this.callback = callback; - mediaPeriod.prepare(this); + mediaPeriod.prepare(this, positionUs); } @Override @@ -84,48 +97,60 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { sampleStreams = new ClippingSampleStream[streams.length]; - SampleStream[] internalStreams = new SampleStream[streams.length]; + @NullableType SampleStream[] childStreams = new SampleStream[streams.length]; for (int i = 0; i < streams.length; i++) { sampleStreams[i] = (ClippingSampleStream) streams[i]; - internalStreams[i] = sampleStreams[i] != null ? sampleStreams[i].stream : null; + childStreams[i] = sampleStreams[i] != null ? sampleStreams[i].childStream : null; } - long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags, - internalStreams, streamResetFlags, positionUs + startUs); - Assertions.checkState(enablePositionUs == positionUs + startUs - || (enablePositionUs >= startUs - && (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs))); + long enablePositionUs = + mediaPeriod.selectTracks( + selections, mayRetainStreamFlags, childStreams, streamResetFlags, positionUs); + pendingInitialDiscontinuityPositionUs = + isPendingInitialDiscontinuity() + && positionUs == startUs + && shouldKeepInitialDiscontinuity(startUs, selections) + ? enablePositionUs + : C.TIME_UNSET; + Assertions.checkState( + enablePositionUs == positionUs + || (enablePositionUs >= startUs + && (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs))); for (int i = 0; i < streams.length; i++) { - if (internalStreams[i] == null) { + if (childStreams[i] == null) { sampleStreams[i] = null; - } else if (streams[i] == null || sampleStreams[i].stream != internalStreams[i]) { - sampleStreams[i] = new ClippingSampleStream(this, internalStreams[i], startUs, endUs, - pendingInitialDiscontinuity); + } else if (sampleStreams[i] == null || sampleStreams[i].childStream != childStreams[i]) { + sampleStreams[i] = new ClippingSampleStream(childStreams[i]); } streams[i] = sampleStreams[i]; } - return enablePositionUs - startUs; + return enablePositionUs; } @Override - public void discardBuffer(long positionUs) { - mediaPeriod.discardBuffer(positionUs + startUs); + public void discardBuffer(long positionUs, boolean toKeyframe) { + mediaPeriod.discardBuffer(positionUs, toKeyframe); + } + + @Override + public void reevaluateBuffer(long positionUs) { + mediaPeriod.reevaluateBuffer(positionUs); } @Override public long readDiscontinuity() { - if (pendingInitialDiscontinuity) { - for (ClippingSampleStream sampleStream : sampleStreams) { - if (sampleStream != null) { - sampleStream.clearPendingDiscontinuity(); - } - } - pendingInitialDiscontinuity = false; - // Always read an initial discontinuity, using mediaPeriod's discontinuity if set. - long discontinuityUs = readDiscontinuity(); - return discontinuityUs != C.TIME_UNSET ? discontinuityUs : 0; + if (isPendingInitialDiscontinuity()) { + long initialDiscontinuityUs = pendingInitialDiscontinuityPositionUs; + pendingInitialDiscontinuityPositionUs = C.TIME_UNSET; + // Always read an initial discontinuity from the child, and use it if set. + long childDiscontinuityUs = readDiscontinuity(); + return childDiscontinuityUs != C.TIME_UNSET ? childDiscontinuityUs : initialDiscontinuityUs; } long discontinuityUs = mediaPeriod.readDiscontinuity(); if (discontinuityUs == C.TIME_UNSET) { @@ -133,7 +158,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } Assertions.checkState(discontinuityUs >= startUs); Assertions.checkState(endUs == C.TIME_END_OF_SOURCE || discontinuityUs <= endUs); - return discontinuityUs - startUs; + return discontinuityUs; } @Override @@ -143,20 +168,32 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb || (endUs != C.TIME_END_OF_SOURCE && bufferedPositionUs >= endUs)) { return C.TIME_END_OF_SOURCE; } - return Math.max(0, bufferedPositionUs - startUs); + return bufferedPositionUs; } @Override public long seekToUs(long positionUs) { + pendingInitialDiscontinuityPositionUs = C.TIME_UNSET; for (ClippingSampleStream sampleStream : sampleStreams) { if (sampleStream != null) { sampleStream.clearSentEos(); } } - long seekUs = mediaPeriod.seekToUs(positionUs + startUs); - Assertions.checkState(seekUs == positionUs + startUs - || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs))); - return seekUs - startUs; + long seekUs = mediaPeriod.seekToUs(positionUs); + Assertions.checkState( + seekUs == positionUs + || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs))); + return seekUs; + } + + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + if (positionUs == startUs) { + // Never adjust seeks to the start of the clipped view. + return startUs; + } + SeekParameters clippedSeekParameters = clipSeekParameters(positionUs, seekParameters); + return mediaPeriod.getAdjustedSeekPositionUs(positionUs, clippedSeekParameters); } @Override @@ -166,19 +203,54 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb || (endUs != C.TIME_END_OF_SOURCE && nextLoadPositionUs >= endUs)) { return C.TIME_END_OF_SOURCE; } - return nextLoadPositionUs - startUs; + return nextLoadPositionUs; } @Override public boolean continueLoading(long positionUs) { - return mediaPeriod.continueLoading(positionUs + startUs); + return mediaPeriod.continueLoading(positionUs); + } + + @Override + public boolean isLoading() { + return mediaPeriod.isLoading(); } // MediaPeriod.Callback implementation. @Override public void onPrepared(MediaPeriod mediaPeriod) { - Assertions.checkState(startUs != C.TIME_UNSET && endUs != C.TIME_UNSET); + Assertions.checkNotNull(callback).onPrepared(this); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + Assertions.checkNotNull(callback).onContinueLoadingRequested(this); + } + + /* package */ boolean isPendingInitialDiscontinuity() { + return pendingInitialDiscontinuityPositionUs != C.TIME_UNSET; + } + + private SeekParameters clipSeekParameters(long positionUs, SeekParameters seekParameters) { + long toleranceBeforeUs = + Util.constrainValue( + seekParameters.toleranceBeforeUs, /* min= */ 0, /* max= */ positionUs - startUs); + long toleranceAfterUs = + Util.constrainValue( + seekParameters.toleranceAfterUs, + /* min= */ 0, + /* max= */ endUs == C.TIME_END_OF_SOURCE ? Long.MAX_VALUE : endUs - positionUs); + if (toleranceBeforeUs == seekParameters.toleranceBeforeUs + && toleranceAfterUs == seekParameters.toleranceAfterUs) { + return seekParameters; + } else { + return new SeekParameters(toleranceBeforeUs, toleranceAfterUs); + } + } + + private static boolean shouldKeepInitialDiscontinuity( + long startUs, @NullableType TrackSelection[] selections) { // If the clipping start position is non-zero, the clipping sample streams will adjust // timestamps on buffers they read from the unclipped sample streams. These adjusted buffer // timestamps can be negative, because sample streams provide buffers starting at a key-frame, @@ -186,39 +258,32 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb // negative timestamp, its offset timestamp can jump backwards compared to the last timestamp // read in the previous period. Renderer implementations may not allow this, so we signal a // discontinuity which resets the renderers before they read the clipping sample stream. - pendingInitialDiscontinuity = startUs != 0; - callback.onPrepared(this); - } - - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - callback.onContinueLoadingRequested(this); + // However, for audio-only track selections we assume to have random access seek behaviour and + // do not need an initial discontinuity to reset the renderer. + if (startUs != 0) { + for (TrackSelection trackSelection : selections) { + if (trackSelection != null) { + Format selectedFormat = trackSelection.getSelectedFormat(); + if (!MimeTypes.isAudio(selectedFormat.sampleMimeType)) { + return true; + } + } + } + } + return false; } /** * Wraps a {@link SampleStream} and clips its samples. */ - private static final class ClippingSampleStream implements SampleStream { + private final class ClippingSampleStream implements SampleStream { - private final MediaPeriod mediaPeriod; - private final SampleStream stream; - private final long startUs; - private final long endUs; + public final SampleStream childStream; - private boolean pendingDiscontinuity; private boolean sentEos; - public ClippingSampleStream(MediaPeriod mediaPeriod, SampleStream stream, long startUs, - long endUs, boolean pendingDiscontinuity) { - this.mediaPeriod = mediaPeriod; - this.stream = stream; - this.startUs = startUs; - this.endUs = endUs; - this.pendingDiscontinuity = pendingDiscontinuity; - } - - public void clearPendingDiscontinuity() { - pendingDiscontinuity = false; + public ClippingSampleStream(SampleStream childStream) { + this.childStream = childStream; } public void clearSentEos() { @@ -227,45 +292,54 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public boolean isReady() { - return stream.isReady(); + return !isPendingInitialDiscontinuity() && childStream.isReady(); } @Override public void maybeThrowError() throws IOException { - stream.maybeThrowError(); + childStream.maybeThrowError(); } @Override public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { - if (pendingDiscontinuity) { + if (isPendingInitialDiscontinuity()) { return C.RESULT_NOTHING_READ; } if (sentEos) { buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; } - int result = stream.readData(formatHolder, buffer, requireFormat); - // TODO: Clear gapless playback metadata if a format was read (if applicable). - if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ - && buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ - && mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { + int result = childStream.readData(formatHolder, buffer, requireFormat); + if (result == C.RESULT_FORMAT_READ) { + Format format = Assertions.checkNotNull(formatHolder.format); + if (format.encoderDelay != 0 || format.encoderPadding != 0) { + // Clear gapless playback metadata if the start/end points don't match the media. + int encoderDelay = startUs != 0 ? 0 : format.encoderDelay; + int encoderPadding = endUs != C.TIME_END_OF_SOURCE ? 0 : format.encoderPadding; + formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding); + } + return C.RESULT_FORMAT_READ; + } + if (endUs != C.TIME_END_OF_SOURCE + && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) + || (result == C.RESULT_NOTHING_READ + && getBufferedPositionUs() == C.TIME_END_OF_SOURCE + && !buffer.waitingForKeys))) { buffer.clear(); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); sentEos = true; return C.RESULT_BUFFER_READ; } - if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) { - buffer.timeUs -= startUs; - } return result; } @Override - public void skipData(long positionUs) { - stream.skipData(startUs + positionUs); + public int skipData(long positionUs) { + if (isPendingInitialDiscontinuity()) { + return C.RESULT_NOTHING_READ; + } + return childStream.skipData(positionUs); } - } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaSource.java index 2c057d4a93c4..373076957db7 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -15,64 +15,204 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; -import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer; import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; /** * {@link MediaSource} that wraps a source and clips its timeline based on specified start/end - * positions. The wrapped source may only have a single period/window and it must not be dynamic - * (live). + * positions. The wrapped source must consist of a single period. */ -public final class ClippingMediaSource implements MediaSource, MediaSource.Listener { +public final class ClippingMediaSource extends CompositeMediaSource { + + /** Thrown when a {@link ClippingMediaSource} cannot clip its wrapped source. */ + public static final class IllegalClippingException extends IOException { + + /** + * The reason clipping failed. One of {@link #REASON_INVALID_PERIOD_COUNT}, {@link + * #REASON_NOT_SEEKABLE_TO_START} or {@link #REASON_START_EXCEEDS_END}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({REASON_INVALID_PERIOD_COUNT, REASON_NOT_SEEKABLE_TO_START, REASON_START_EXCEEDS_END}) + public @interface Reason {} + /** The wrapped source doesn't consist of a single period. */ + public static final int REASON_INVALID_PERIOD_COUNT = 0; + /** The wrapped source is not seekable and a non-zero clipping start position was specified. */ + public static final int REASON_NOT_SEEKABLE_TO_START = 1; + /** The wrapped source ends before the specified clipping start position. */ + public static final int REASON_START_EXCEEDS_END = 2; + + /** The reason clipping failed. */ + public final @Reason int reason; + + /** + * @param reason The reason clipping failed. + */ + public IllegalClippingException(@Reason int reason) { + super("Illegal clipping: " + getReasonDescription(reason)); + this.reason = reason; + } + + private static String getReasonDescription(@Reason int reason) { + switch (reason) { + case REASON_INVALID_PERIOD_COUNT: + return "invalid period count"; + case REASON_NOT_SEEKABLE_TO_START: + return "not seekable to start"; + case REASON_START_EXCEEDS_END: + return "start exceeds end"; + default: + return "unknown"; + } + } + } private final MediaSource mediaSource; private final long startUs; private final long endUs; + private final boolean enableInitialDiscontinuity; + private final boolean allowDynamicClippingUpdates; + private final boolean relativeToDefaultPosition; private final ArrayList mediaPeriods; + private final Timeline.Window window; - private MediaSource.Listener sourceListener; - private ClippingTimeline clippingTimeline; + @Nullable private ClippingTimeline clippingTimeline; + @Nullable private IllegalClippingException clippingError; + private long periodStartUs; + private long periodEndUs; + + /** + * Creates a new clipping source that wraps the specified source and provides samples between the + * specified start and end position. + * + * @param mediaSource The single-period source to wrap. + * @param startPositionUs The start position within {@code mediaSource}'s window at which to start + * providing samples, in microseconds. + * @param endPositionUs The end position within {@code mediaSource}'s window at which to stop + * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples + * from the specified start point up to the end of the source. Specifying a position that + * exceeds the {@code mediaSource}'s duration will also result in the end of the source not + * being clipped. + */ + public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) { + this( + mediaSource, + startPositionUs, + endPositionUs, + /* enableInitialDiscontinuity= */ true, + /* allowDynamicClippingUpdates= */ false, + /* relativeToDefaultPosition= */ false); + } + + /** + * Creates a new clipping source that wraps the specified source and provides samples from the + * default position for the specified duration. + * + * @param mediaSource The single-period source to wrap. + * @param durationUs The duration from the default position in the window in {@code mediaSource}'s + * timeline at which to stop providing samples. Specifying a duration that exceeds the {@code + * mediaSource}'s duration will result in the end of the source not being clipped. + */ + public ClippingMediaSource(MediaSource mediaSource, long durationUs) { + this( + mediaSource, + /* startPositionUs= */ 0, + /* endPositionUs= */ durationUs, + /* enableInitialDiscontinuity= */ true, + /* allowDynamicClippingUpdates= */ false, + /* relativeToDefaultPosition= */ true); + } /** * Creates a new clipping source that wraps the specified source. * - * @param mediaSource The single-period, non-dynamic source to wrap. - * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to - * start providing samples, in microseconds. - * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop - * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples - * from the specified start point up to the end of the source. + *

If the start point is guaranteed to be a key frame, pass {@code false} to {@code + * enableInitialPositionDiscontinuity} to suppress an initial discontinuity when a period is first + * read from. + * + *

For live streams, if the clipping positions should move with the live window, pass {@code + * true} to {@code allowDynamicClippingUpdates}. Otherwise, the live stream ends when the playback + * reaches {@code endPositionUs} in the last reported live window at the time a media period was + * created. + * + * @param mediaSource The single-period source to wrap. + * @param startPositionUs The start position at which to start providing samples, in microseconds. + * If {@code relativeToDefaultPosition} is {@code false}, this position is relative to the + * start of the window in {@code mediaSource}'s timeline. If {@code relativeToDefaultPosition} + * is {@code true}, this position is relative to the default position in the window in {@code + * mediaSource}'s timeline. + * @param endPositionUs The end position at which to stop providing samples, in microseconds. + * Specify {@link C#TIME_END_OF_SOURCE} to provide samples from the specified start point up + * to the end of the source. Specifying a position that exceeds the {@code mediaSource}'s + * duration will also result in the end of the source not being clipped. If {@code + * relativeToDefaultPosition} is {@code false}, the specified position is relative to the + * start of the window in {@code mediaSource}'s timeline. If {@code relativeToDefaultPosition} + * is {@code true}, this position is relative to the default position in the window in {@code + * mediaSource}'s timeline. + * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled. + * @param allowDynamicClippingUpdates Whether the clipping of active media periods moves with a + * live window. If {@code false}, playback ends when it reaches {@code endPositionUs} in the + * last reported live window at the time a media period was created. + * @param relativeToDefaultPosition Whether {@code startPositionUs} and {@code endPositionUs} are + * relative to the default position in the window in {@code mediaSource}'s timeline. */ - public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) { + public ClippingMediaSource( + MediaSource mediaSource, + long startPositionUs, + long endPositionUs, + boolean enableInitialDiscontinuity, + boolean allowDynamicClippingUpdates, + boolean relativeToDefaultPosition) { Assertions.checkArgument(startPositionUs >= 0); this.mediaSource = Assertions.checkNotNull(mediaSource); startUs = startPositionUs; endUs = endPositionUs; + this.enableInitialDiscontinuity = enableInitialDiscontinuity; + this.allowDynamicClippingUpdates = allowDynamicClippingUpdates; + this.relativeToDefaultPosition = relativeToDefaultPosition; mediaPeriods = new ArrayList<>(); + window = new Timeline.Window(); } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - this.sourceListener = listener; - mediaSource.prepareSource(player, false, this); + @Nullable + public Object getTag() { + return mediaSource.getTag(); + } + + @Override + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + prepareChildSource(/* id= */ null, mediaSource); } @Override public void maybeThrowSourceInfoRefreshError() throws IOException { - mediaSource.maybeThrowSourceInfoRefreshError(); + if (clippingError != null) { + throw clippingError; + } + super.maybeThrowSourceInfoRefreshError(); } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod( - mediaSource.createPeriod(index, allocator, startUs + positionUs)); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + ClippingMediaPeriod mediaPeriod = + new ClippingMediaPeriod( + mediaSource.createPeriod(id, allocator, startPositionUs), + enableInitialDiscontinuity, + periodStartUs, + periodEndUs); mediaPeriods.add(mediaPeriod); - mediaPeriod.setClipping(clippingTimeline.startUs, clippingTimeline.endUs); return mediaPeriod; } @@ -80,36 +220,87 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste public void releasePeriod(MediaPeriod mediaPeriod) { Assertions.checkState(mediaPeriods.remove(mediaPeriod)); mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); - } - - @Override - public void releaseSource() { - mediaSource.releaseSource(); - } - - // MediaSource.Listener implementation. - - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - clippingTimeline = new ClippingTimeline(timeline, startUs, endUs); - sourceListener.onSourceInfoRefreshed(clippingTimeline, manifest); - long startUs = clippingTimeline.startUs; - long endUs = clippingTimeline.endUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE - : clippingTimeline.endUs; - int count = mediaPeriods.size(); - for (int i = 0; i < count; i++) { - mediaPeriods.get(i).setClipping(startUs, endUs); + if (mediaPeriods.isEmpty() && !allowDynamicClippingUpdates) { + refreshClippedTimeline(Assertions.checkNotNull(clippingTimeline).timeline); } } + @Override + protected void releaseSourceInternal() { + super.releaseSourceInternal(); + clippingError = null; + clippingTimeline = null; + } + + @Override + protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) { + if (clippingError != null) { + return; + } + refreshClippedTimeline(timeline); + } + + private void refreshClippedTimeline(Timeline timeline) { + long windowStartUs; + long windowEndUs; + timeline.getWindow(/* windowIndex= */ 0, window); + long windowPositionInPeriodUs = window.getPositionInFirstPeriodUs(); + if (clippingTimeline == null || mediaPeriods.isEmpty() || allowDynamicClippingUpdates) { + windowStartUs = startUs; + windowEndUs = endUs; + if (relativeToDefaultPosition) { + long windowDefaultPositionUs = window.getDefaultPositionUs(); + windowStartUs += windowDefaultPositionUs; + windowEndUs += windowDefaultPositionUs; + } + periodStartUs = windowPositionInPeriodUs + windowStartUs; + periodEndUs = + endUs == C.TIME_END_OF_SOURCE + ? C.TIME_END_OF_SOURCE + : windowPositionInPeriodUs + windowEndUs; + int count = mediaPeriods.size(); + for (int i = 0; i < count; i++) { + mediaPeriods.get(i).updateClipping(periodStartUs, periodEndUs); + } + } else { + // Keep window fixed at previous period position. + windowStartUs = periodStartUs - windowPositionInPeriodUs; + windowEndUs = + endUs == C.TIME_END_OF_SOURCE + ? C.TIME_END_OF_SOURCE + : periodEndUs - windowPositionInPeriodUs; + } + try { + clippingTimeline = new ClippingTimeline(timeline, windowStartUs, windowEndUs); + } catch (IllegalClippingException e) { + clippingError = e; + return; + } + refreshSourceInfo(clippingTimeline); + } + + @Override + protected long getMediaTimeForChildMediaTime(Void id, long mediaTimeMs) { + if (mediaTimeMs == C.TIME_UNSET) { + return C.TIME_UNSET; + } + long startMs = C.usToMs(startUs); + long clippedTimeMs = Math.max(0, mediaTimeMs - startMs); + if (endUs != C.TIME_END_OF_SOURCE) { + clippedTimeMs = Math.min(C.usToMs(endUs) - startMs, clippedTimeMs); + } + return clippedTimeMs; + } + /** * Provides a clipped view of a specified timeline. */ - private static final class ClippingTimeline extends Timeline { + private static final class ClippingTimeline extends ForwardingTimeline { - private final Timeline timeline; private final long startUs; private final long endUs; + private final long durationUs; + private final boolean isDynamic; /** * Creates a new clipping timeline that wraps the specified timeline. @@ -118,35 +309,43 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste * @param startUs The number of microseconds to clip from the start of {@code timeline}. * @param endUs The end position in microseconds for the clipped timeline relative to the start * of {@code timeline}, or {@link C#TIME_END_OF_SOURCE} to clip no samples from the end. + * @throws IllegalClippingException If the timeline could not be clipped. */ - public ClippingTimeline(Timeline timeline, long startUs, long endUs) { - Assertions.checkArgument(timeline.getWindowCount() == 1); - Assertions.checkArgument(timeline.getPeriodCount() == 1); - Window window = timeline.getWindow(0, new Window(), false); - Assertions.checkArgument(!window.isDynamic); - long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : endUs; - if (window.durationUs != C.TIME_UNSET) { - Assertions.checkArgument(startUs == 0 || window.isSeekable); - Assertions.checkArgument(resolvedEndUs <= window.durationUs); - Assertions.checkArgument(startUs <= resolvedEndUs); + public ClippingTimeline(Timeline timeline, long startUs, long endUs) + throws IllegalClippingException { + super(timeline); + if (timeline.getPeriodCount() != 1) { + throw new IllegalClippingException(IllegalClippingException.REASON_INVALID_PERIOD_COUNT); + } + Window window = timeline.getWindow(0, new Window()); + startUs = Math.max(0, startUs); + long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : Math.max(0, endUs); + if (window.durationUs != C.TIME_UNSET) { + if (resolvedEndUs > window.durationUs) { + resolvedEndUs = window.durationUs; + } + if (startUs != 0 && !window.isSeekable) { + throw new IllegalClippingException(IllegalClippingException.REASON_NOT_SEEKABLE_TO_START); + } + if (startUs > resolvedEndUs) { + throw new IllegalClippingException(IllegalClippingException.REASON_START_EXCEEDS_END); + } } - Period period = timeline.getPeriod(0, new Period()); - Assertions.checkArgument(period.getPositionInWindowUs() == 0); - this.timeline = timeline; this.startUs = startUs; this.endUs = resolvedEndUs; + durationUs = resolvedEndUs == C.TIME_UNSET ? C.TIME_UNSET : (resolvedEndUs - startUs); + isDynamic = + window.isDynamic + && (resolvedEndUs == C.TIME_UNSET + || (window.durationUs != C.TIME_UNSET && resolvedEndUs == window.durationUs)); } @Override - public int getWindowCount() { - return 1; - } - - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - window = timeline.getWindow(0, window, setIds, defaultPositionProjectionUs); - window.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET; + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + timeline.getWindow(/* windowIndex= */ 0, window, /* defaultPositionProjectionUs= */ 0); + window.positionInFirstPeriodUs += startUs; + window.durationUs = durationUs; + window.isDynamic = isDynamic; if (window.defaultPositionUs != C.TIME_UNSET) { window.defaultPositionUs = Math.max(window.defaultPositionUs, startUs); window.defaultPositionUs = endUs == C.TIME_UNSET ? window.defaultPositionUs @@ -163,23 +362,14 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste return window; } - @Override - public int getPeriodCount() { - return 1; - } - @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { - period = timeline.getPeriod(0, period, setIds); - period.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET; - return period; + timeline.getPeriod(/* periodIndex= */ 0, period, setIds); + long positionInClippedWindowUs = period.getPositionInWindowUs() - startUs; + long periodDurationUs = + durationUs == C.TIME_UNSET ? C.TIME_UNSET : durationUs - positionInClippedWindowUs; + return period.set( + period.id, period.uid, /* windowIndex= */ 0, periodDurationUs, positionInClippedWindowUs); } - - @Override - public int getIndexOfPeriod(Object uid) { - return timeline.getIndexOfPeriod(uid); - } - } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeMediaSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeMediaSource.java new file mode 100644 index 000000000000..ed46b8ee9463 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeMediaSource.java @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +import android.os.Handler; +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.HashMap; + +/** + * Composite {@link MediaSource} consisting of multiple child sources. + * + * @param The type of the id used to identify prepared child sources. + */ +public abstract class CompositeMediaSource extends BaseMediaSource { + + private final HashMap childSources; + + @Nullable private Handler eventHandler; + @Nullable private TransferListener mediaTransferListener; + + /** Creates composite media source without child sources. */ + protected CompositeMediaSource() { + childSources = new HashMap<>(); + } + + @Override + @CallSuper + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + this.mediaTransferListener = mediaTransferListener; + eventHandler = new Handler(); + } + + @Override + @CallSuper + public void maybeThrowSourceInfoRefreshError() throws IOException { + for (MediaSourceAndListener childSource : childSources.values()) { + childSource.mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + + @Override + @CallSuper + protected void enableInternal() { + for (MediaSourceAndListener childSource : childSources.values()) { + childSource.mediaSource.enable(childSource.caller); + } + } + + @Override + @CallSuper + protected void disableInternal() { + for (MediaSourceAndListener childSource : childSources.values()) { + childSource.mediaSource.disable(childSource.caller); + } + } + + @Override + @CallSuper + protected void releaseSourceInternal() { + for (MediaSourceAndListener childSource : childSources.values()) { + childSource.mediaSource.releaseSource(childSource.caller); + childSource.mediaSource.removeEventListener(childSource.eventListener); + } + childSources.clear(); + } + + /** + * Called when the source info of a child source has been refreshed. + * + * @param id The unique id used to prepare the child source. + * @param mediaSource The child source whose source info has been refreshed. + * @param timeline The timeline of the child source. + */ + protected abstract void onChildSourceInfoRefreshed( + T id, MediaSource mediaSource, Timeline timeline); + + /** + * Prepares a child source. + * + *

{@link #onChildSourceInfoRefreshed(Object, MediaSource, Timeline)} will be called when the + * child source updates its timeline with the same {@code id} passed to this method. + * + *

Any child sources that aren't explicitly released with {@link #releaseChildSource(Object)} + * will be released in {@link #releaseSourceInternal()}. + * + * @param id A unique id to identify the child source preparation. Null is allowed as an id. + * @param mediaSource The child {@link MediaSource}. + */ + protected final void prepareChildSource(final T id, MediaSource mediaSource) { + Assertions.checkArgument(!childSources.containsKey(id)); + MediaSourceCaller caller = + (source, timeline) -> onChildSourceInfoRefreshed(id, source, timeline); + MediaSourceEventListener eventListener = new ForwardingEventListener(id); + childSources.put(id, new MediaSourceAndListener(mediaSource, caller, eventListener)); + mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener); + mediaSource.prepareSource(caller, mediaTransferListener); + if (!isEnabled()) { + mediaSource.disable(caller); + } + } + + /** + * Enables a child source. + * + * @param id The unique id used to prepare the child source. + */ + protected final void enableChildSource(final T id) { + MediaSourceAndListener enabledChild = Assertions.checkNotNull(childSources.get(id)); + enabledChild.mediaSource.enable(enabledChild.caller); + } + + /** + * Disables a child source. + * + * @param id The unique id used to prepare the child source. + */ + protected final void disableChildSource(final T id) { + MediaSourceAndListener disabledChild = Assertions.checkNotNull(childSources.get(id)); + disabledChild.mediaSource.disable(disabledChild.caller); + } + + /** + * Releases a child source. + * + * @param id The unique id used to prepare the child source. + */ + protected final void releaseChildSource(T id) { + MediaSourceAndListener removedChild = Assertions.checkNotNull(childSources.remove(id)); + removedChild.mediaSource.releaseSource(removedChild.caller); + removedChild.mediaSource.removeEventListener(removedChild.eventListener); + } + + /** + * Returns the window index in the composite source corresponding to the specified window index in + * a child source. The default implementation does not change the window index. + * + * @param id The unique id used to prepare the child source. + * @param windowIndex A window index of the child source. + * @return The corresponding window index in the composite source. + */ + protected int getWindowIndexForChildWindowIndex(T id, int windowIndex) { + return windowIndex; + } + + /** + * Returns the {@link MediaPeriodId} in the composite source corresponding to the specified {@link + * MediaPeriodId} in a child source. The default implementation does not change the media period + * id. + * + * @param id The unique id used to prepare the child source. + * @param mediaPeriodId A {@link MediaPeriodId} of the child source. + * @return The corresponding {@link MediaPeriodId} in the composite source. Null if no + * corresponding media period id can be determined. + */ + protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + T id, MediaPeriodId mediaPeriodId) { + return mediaPeriodId; + } + + /** + * Returns the media time in the composite source corresponding to the specified media time in a + * child source. The default implementation does not change the media time. + * + * @param id The unique id used to prepare the child source. + * @param mediaTimeMs A media time of the child source, in milliseconds. + * @return The corresponding media time in the composite source, in milliseconds. + */ + protected long getMediaTimeForChildMediaTime(@Nullable T id, long mediaTimeMs) { + return mediaTimeMs; + } + + /** + * Returns whether {@link MediaSourceEventListener#onMediaPeriodCreated(int, MediaPeriodId)} and + * {@link MediaSourceEventListener#onMediaPeriodReleased(int, MediaPeriodId)} events of the given + * media period should be reported. The default implementation is to always report these events. + * + * @param mediaPeriodId A {@link MediaPeriodId} in the composite media source. + * @return Whether create and release events for this media period should be reported. + */ + protected boolean shouldDispatchCreateOrReleaseEvent(MediaPeriodId mediaPeriodId) { + return true; + } + + private static final class MediaSourceAndListener { + + public final MediaSource mediaSource; + public final MediaSourceCaller caller; + public final MediaSourceEventListener eventListener; + + public MediaSourceAndListener( + MediaSource mediaSource, MediaSourceCaller caller, MediaSourceEventListener eventListener) { + this.mediaSource = mediaSource; + this.caller = caller; + this.eventListener = eventListener; + } + } + + private final class ForwardingEventListener implements MediaSourceEventListener { + + private final T id; + private EventDispatcher eventDispatcher; + + public ForwardingEventListener(T id) { + this.eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); + this.id = id; + } + + @Override + public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + if (shouldDispatchCreateOrReleaseEvent( + Assertions.checkNotNull(eventDispatcher.mediaPeriodId))) { + eventDispatcher.mediaPeriodCreated(); + } + } + } + + @Override + public void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + if (shouldDispatchCreateOrReleaseEvent( + Assertions.checkNotNull(eventDispatcher.mediaPeriodId))) { + eventDispatcher.mediaPeriodReleased(); + } + } + } + + @Override + public void onLoadStarted( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventData, + MediaLoadData mediaLoadData) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.loadStarted(loadEventData, maybeUpdateMediaLoadData(mediaLoadData)); + } + } + + @Override + public void onLoadCompleted( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventData, + MediaLoadData mediaLoadData) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.loadCompleted(loadEventData, maybeUpdateMediaLoadData(mediaLoadData)); + } + } + + @Override + public void onLoadCanceled( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventData, + MediaLoadData mediaLoadData) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.loadCanceled(loadEventData, maybeUpdateMediaLoadData(mediaLoadData)); + } + } + + @Override + public void onLoadError( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventData, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.loadError( + loadEventData, maybeUpdateMediaLoadData(mediaLoadData), error, wasCanceled); + } + } + + @Override + public void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.readingStarted(); + } + } + + @Override + public void onUpstreamDiscarded( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.upstreamDiscarded(maybeUpdateMediaLoadData(mediaLoadData)); + } + } + + @Override + public void onDownstreamFormatChanged( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { + if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { + eventDispatcher.downstreamFormatChanged(maybeUpdateMediaLoadData(mediaLoadData)); + } + } + + /** Updates the event dispatcher and returns whether the event should be dispatched. */ + private boolean maybeUpdateEventDispatcher( + int childWindowIndex, @Nullable MediaPeriodId childMediaPeriodId) { + MediaPeriodId mediaPeriodId = null; + if (childMediaPeriodId != null) { + mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId); + if (mediaPeriodId == null) { + // Media period not found. Ignore event. + return false; + } + } + int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex); + if (eventDispatcher.windowIndex != windowIndex + || !Util.areEqual(eventDispatcher.mediaPeriodId, mediaPeriodId)) { + eventDispatcher = + createEventDispatcher(windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0); + } + return true; + } + + private MediaLoadData maybeUpdateMediaLoadData(MediaLoadData mediaLoadData) { + long mediaStartTimeMs = getMediaTimeForChildMediaTime(id, mediaLoadData.mediaStartTimeMs); + long mediaEndTimeMs = getMediaTimeForChildMediaTime(id, mediaLoadData.mediaEndTimeMs); + if (mediaStartTimeMs == mediaLoadData.mediaStartTimeMs + && mediaEndTimeMs == mediaLoadData.mediaEndTimeMs) { + return mediaLoadData; + } + return new MediaLoadData( + mediaLoadData.dataType, + mediaLoadData.trackType, + mediaLoadData.trackFormat, + mediaLoadData.trackSelectionReason, + mediaLoadData.trackSelectionData, + mediaStartTimeMs, + mediaEndTimeMs); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java index 019e77ac3fa9..9a72903528ea 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java @@ -20,16 +20,28 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.C; /** * A {@link SequenceableLoader} that encapsulates multiple other {@link SequenceableLoader}s. */ -public final class CompositeSequenceableLoader implements SequenceableLoader { +public class CompositeSequenceableLoader implements SequenceableLoader { - private final SequenceableLoader[] loaders; + protected final SequenceableLoader[] loaders; public CompositeSequenceableLoader(SequenceableLoader[] loaders) { this.loaders = loaders; } @Override - public long getNextLoadPositionUs() { + public final long getBufferedPositionUs() { + long bufferedPositionUs = Long.MAX_VALUE; + for (SequenceableLoader loader : loaders) { + long loaderBufferedPositionUs = loader.getBufferedPositionUs(); + if (loaderBufferedPositionUs != C.TIME_END_OF_SOURCE) { + bufferedPositionUs = Math.min(bufferedPositionUs, loaderBufferedPositionUs); + } + } + return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs; + } + + @Override + public final long getNextLoadPositionUs() { long nextLoadPositionUs = Long.MAX_VALUE; for (SequenceableLoader loader : loaders) { long loaderNextLoadPositionUs = loader.getNextLoadPositionUs(); @@ -40,6 +52,13 @@ public final class CompositeSequenceableLoader implements SequenceableLoader { return nextLoadPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : nextLoadPositionUs; } + @Override + public final void reevaluateBuffer(long positionUs) { + for (SequenceableLoader loader : loaders) { + loader.reevaluateBuffer(positionUs); + } + } + @Override public boolean continueLoading(long positionUs) { boolean madeProgress = false; @@ -51,7 +70,11 @@ public final class CompositeSequenceableLoader implements SequenceableLoader { break; } for (SequenceableLoader loader : loaders) { - if (loader.getNextLoadPositionUs() == nextLoadPositionUs) { + long loaderNextLoadPositionUs = loader.getNextLoadPositionUs(); + boolean isLoaderBehind = + loaderNextLoadPositionUs != C.TIME_END_OF_SOURCE + && loaderNextLoadPositionUs <= positionUs; + if (loaderNextLoadPositionUs == nextLoadPositionUs || isLoaderBehind) { madeProgressThisIteration |= loader.continueLoading(positionUs); } } @@ -60,4 +83,13 @@ public final class CompositeSequenceableLoader implements SequenceableLoader { return madeProgress; } + @Override + public boolean isLoading() { + for (SequenceableLoader loader : loaders) { + if (loader.isLoading()) { + return true; + } + } + return false; + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java new file mode 100644 index 000000000000..1ac76d616742 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +/** + * A factory to create composite {@link SequenceableLoader}s. + */ +public interface CompositeSequenceableLoaderFactory { + + /** + * Creates a composite {@link SequenceableLoader}. + * + * @param loaders The sub-loaders that make up the {@link SequenceableLoader} to be built. + * @return A composite {@link SequenceableLoader} that comprises the given loaders. + */ + SequenceableLoader createCompositeSequenceableLoader(SequenceableLoader... loaders); + +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index c376d61e5d19..aa6f486473cc 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -15,231 +15,1003 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source; -import android.util.Pair; +import android.os.Handler; +import android.os.Message; +import androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; -import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer; import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; import java.util.Map; +import java.util.Set; /** - * Concatenates multiple {@link MediaSource}s. It is valid for the same {@link MediaSource} instance - * to be present more than once in the concatenation. + * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified + * during playback. It is valid for the same {@link MediaSource} instance to be present more than + * once in the concatenation. Access to this class is thread-safe. */ -public final class ConcatenatingMediaSource implements MediaSource { +public final class ConcatenatingMediaSource extends CompositeMediaSource { - private final MediaSource[] mediaSources; - private final Timeline[] timelines; - private final Object[] manifests; - private final Map sourceIndexByMediaPeriod; - private final boolean[] duplicateFlags; + private static final int MSG_ADD = 0; + private static final int MSG_REMOVE = 1; + private static final int MSG_MOVE = 2; + private static final int MSG_SET_SHUFFLE_ORDER = 3; + private static final int MSG_UPDATE_TIMELINE = 4; + private static final int MSG_ON_COMPLETION = 5; - private Listener listener; - private ConcatenatedTimeline timeline; + // Accessed on any thread. + @GuardedBy("this") + private final List mediaSourcesPublic; + + @GuardedBy("this") + private final Set pendingOnCompletionActions; + + @GuardedBy("this") + @Nullable + private Handler playbackThreadHandler; + + // Accessed on the playback thread only. + private final List mediaSourceHolders; + private final Map mediaSourceByMediaPeriod; + private final Map mediaSourceByUid; + private final Set enabledMediaSourceHolders; + private final boolean isAtomic; + private final boolean useLazyPreparation; + + private boolean timelineUpdateScheduled; + private Set nextTimelineUpdateOnCompletionActions; + private ShuffleOrder shuffleOrder; /** * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same * {@link MediaSource} instance to be present more than once in the array. */ public ConcatenatingMediaSource(MediaSource... mediaSources) { - this.mediaSources = mediaSources; - timelines = new Timeline[mediaSources.length]; - manifests = new Object[mediaSources.length]; - sourceIndexByMediaPeriod = new HashMap<>(); - duplicateFlags = buildDuplicateFlags(mediaSources); + this(/* isAtomic= */ false, mediaSources); + } + + /** + * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated + * as a single item for repeating and shuffling. + * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link + * MediaSource} instance to be present more than once in the array. + */ + public ConcatenatingMediaSource(boolean isAtomic, MediaSource... mediaSources) { + this(isAtomic, new DefaultShuffleOrder(0), mediaSources); + } + + /** + * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated + * as a single item for repeating and shuffling. + * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. + * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link + * MediaSource} instance to be present more than once in the array. + */ + public ConcatenatingMediaSource( + boolean isAtomic, ShuffleOrder shuffleOrder, MediaSource... mediaSources) { + this(isAtomic, /* useLazyPreparation= */ false, shuffleOrder, mediaSources); + } + + /** + * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated + * as a single item for repeating and shuffling. + * @param useLazyPreparation Whether playlist items are prepared lazily. If false, all manifest + * loads and other initial preparation steps happen immediately. If true, these initial + * preparations are triggered only when the player starts buffering the media. + * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. + * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link + * MediaSource} instance to be present more than once in the array. + */ + @SuppressWarnings("initialization") + public ConcatenatingMediaSource( + boolean isAtomic, + boolean useLazyPreparation, + ShuffleOrder shuffleOrder, + MediaSource... mediaSources) { + for (MediaSource mediaSource : mediaSources) { + Assertions.checkNotNull(mediaSource); + } + this.shuffleOrder = shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder; + this.mediaSourceByMediaPeriod = new IdentityHashMap<>(); + this.mediaSourceByUid = new HashMap<>(); + this.mediaSourcesPublic = new ArrayList<>(); + this.mediaSourceHolders = new ArrayList<>(); + this.nextTimelineUpdateOnCompletionActions = new HashSet<>(); + this.pendingOnCompletionActions = new HashSet<>(); + this.enabledMediaSourceHolders = new HashSet<>(); + this.isAtomic = isAtomic; + this.useLazyPreparation = useLazyPreparation; + addMediaSources(Arrays.asList(mediaSources)); + } + + /** + * Appends a {@link MediaSource} to the playlist. + * + * @param mediaSource The {@link MediaSource} to be added to the list. + */ + public synchronized void addMediaSource(MediaSource mediaSource) { + addMediaSource(mediaSourcesPublic.size(), mediaSource); + } + + /** + * Appends a {@link MediaSource} to the playlist and executes a custom action on completion. + * + * @param mediaSource The {@link MediaSource} to be added to the list. + * @param handler The {@link Handler} to run {@code onCompletionAction}. + * @param onCompletionAction A {@link Runnable} which is executed immediately after the media + * source has been added to the playlist. + */ + public synchronized void addMediaSource( + MediaSource mediaSource, Handler handler, Runnable onCompletionAction) { + addMediaSource(mediaSourcesPublic.size(), mediaSource, handler, onCompletionAction); + } + + /** + * Adds a {@link MediaSource} to the playlist. + * + * @param index The index at which the new {@link MediaSource} will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSource The {@link MediaSource} to be added to the list. + */ + public synchronized void addMediaSource(int index, MediaSource mediaSource) { + addPublicMediaSources( + index, + Collections.singletonList(mediaSource), + /* handler= */ null, + /* onCompletionAction= */ null); + } + + /** + * Adds a {@link MediaSource} to the playlist and executes a custom action on completion. + * + * @param index The index at which the new {@link MediaSource} will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSource The {@link MediaSource} to be added to the list. + * @param handler The {@link Handler} to run {@code onCompletionAction}. + * @param onCompletionAction A {@link Runnable} which is executed immediately after the media + * source has been added to the playlist. + */ + public synchronized void addMediaSource( + int index, MediaSource mediaSource, Handler handler, Runnable onCompletionAction) { + addPublicMediaSources( + index, Collections.singletonList(mediaSource), handler, onCompletionAction); + } + + /** + * Appends multiple {@link MediaSource}s to the playlist. + * + * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media + * sources are added in the order in which they appear in this collection. + */ + public synchronized void addMediaSources(Collection mediaSources) { + addPublicMediaSources( + mediaSourcesPublic.size(), + mediaSources, + /* handler= */ null, + /* onCompletionAction= */ null); + } + + /** + * Appends multiple {@link MediaSource}s to the playlist and executes a custom action on + * completion. + * + * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media + * sources are added in the order in which they appear in this collection. + * @param handler The {@link Handler} to run {@code onCompletionAction}. + * @param onCompletionAction A {@link Runnable} which is executed immediately after the media + * sources have been added to the playlist. + */ + public synchronized void addMediaSources( + Collection mediaSources, Handler handler, Runnable onCompletionAction) { + addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, onCompletionAction); + } + + /** + * Adds multiple {@link MediaSource}s to the playlist. + * + * @param index The index at which the new {@link MediaSource}s will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media + * sources are added in the order in which they appear in this collection. + */ + public synchronized void addMediaSources(int index, Collection mediaSources) { + addPublicMediaSources(index, mediaSources, /* handler= */ null, /* onCompletionAction= */ null); + } + + /** + * Adds multiple {@link MediaSource}s to the playlist and executes a custom action on completion. + * + * @param index The index at which the new {@link MediaSource}s will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media + * sources are added in the order in which they appear in this collection. + * @param handler The {@link Handler} to run {@code onCompletionAction}. + * @param onCompletionAction A {@link Runnable} which is executed immediately after the media + * sources have been added to the playlist. + */ + public synchronized void addMediaSources( + int index, + Collection mediaSources, + Handler handler, + Runnable onCompletionAction) { + addPublicMediaSources(index, mediaSources, handler, onCompletionAction); + } + + /** + * Removes a {@link MediaSource} from the playlist. + * + *

Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int, + * int)} instead. + * + *

Note: If you want to remove a set of contiguous sources, it's preferable to use {@link + * #removeMediaSourceRange(int, int)} instead. + * + * @param index The index at which the media source will be removed. This index must be in the + * range of 0 <= index < {@link #getSize()}. + * @return The removed {@link MediaSource}. + */ + public synchronized MediaSource removeMediaSource(int index) { + MediaSource removedMediaSource = getMediaSource(index); + removePublicMediaSources(index, index + 1, /* handler= */ null, /* onCompletionAction= */ null); + return removedMediaSource; + } + + /** + * Removes a {@link MediaSource} from the playlist and executes a custom action on completion. + * + *

Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int, + * int, Handler, Runnable)} instead. + * + *

Note: If you want to remove a set of contiguous sources, it's preferable to use {@link + * #removeMediaSourceRange(int, int, Handler, Runnable)} instead. + * + * @param index The index at which the media source will be removed. This index must be in the + * range of 0 <= index < {@link #getSize()}. + * @param handler The {@link Handler} to run {@code onCompletionAction}. + * @param onCompletionAction A {@link Runnable} which is executed immediately after the media + * source has been removed from the playlist. + * @return The removed {@link MediaSource}. + */ + public synchronized MediaSource removeMediaSource( + int index, Handler handler, Runnable onCompletionAction) { + MediaSource removedMediaSource = getMediaSource(index); + removePublicMediaSources(index, index + 1, handler, onCompletionAction); + return removedMediaSource; + } + + /** + * Removes a range of {@link MediaSource}s from the playlist, by specifying an initial index + * (included) and a final index (excluded). + * + *

Note: when specified range is empty, no actual media source is removed and no exception is + * thrown. + * + * @param fromIndex The initial range index, pointing to the first media source that will be + * removed. This index must be in the range of 0 <= index <= {@link #getSize()}. + * @param toIndex The final range index, pointing to the first media source that will be left + * untouched. This index must be in the range of 0 <= index <= {@link #getSize()}. + * @throws IndexOutOfBoundsException When the range is malformed, i.e. {@code fromIndex} < 0, + * {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex} + */ + public synchronized void removeMediaSourceRange(int fromIndex, int toIndex) { + removePublicMediaSources( + fromIndex, toIndex, /* handler= */ null, /* onCompletionAction= */ null); + } + + /** + * Removes a range of {@link MediaSource}s from the playlist, by specifying an initial index + * (included) and a final index (excluded), and executes a custom action on completion. + * + *

Note: when specified range is empty, no actual media source is removed and no exception is + * thrown. + * + * @param fromIndex The initial range index, pointing to the first media source that will be + * removed. This index must be in the range of 0 <= index <= {@link #getSize()}. + * @param toIndex The final range index, pointing to the first media source that will be left + * untouched. This index must be in the range of 0 <= index <= {@link #getSize()}. + * @param handler The {@link Handler} to run {@code onCompletionAction}. + * @param onCompletionAction A {@link Runnable} which is executed immediately after the media + * source range has been removed from the playlist. + * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0, + * {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex} + */ + public synchronized void removeMediaSourceRange( + int fromIndex, int toIndex, Handler handler, Runnable onCompletionAction) { + removePublicMediaSources(fromIndex, toIndex, handler, onCompletionAction); + } + + /** + * Moves an existing {@link MediaSource} within the playlist. + * + * @param currentIndex The current index of the media source in the playlist. This index must be + * in the range of 0 <= index < {@link #getSize()}. + * @param newIndex The target index of the media source in the playlist. This index must be in the + * range of 0 <= index < {@link #getSize()}. + */ + public synchronized void moveMediaSource(int currentIndex, int newIndex) { + movePublicMediaSource( + currentIndex, newIndex, /* handler= */ null, /* onCompletionAction= */ null); + } + + /** + * Moves an existing {@link MediaSource} within the playlist and executes a custom action on + * completion. + * + * @param currentIndex The current index of the media source in the playlist. This index must be + * in the range of 0 <= index < {@link #getSize()}. + * @param newIndex The target index of the media source in the playlist. This index must be in the + * range of 0 <= index < {@link #getSize()}. + * @param handler The {@link Handler} to run {@code onCompletionAction}. + * @param onCompletionAction A {@link Runnable} which is executed immediately after the media + * source has been moved. + */ + public synchronized void moveMediaSource( + int currentIndex, int newIndex, Handler handler, Runnable onCompletionAction) { + movePublicMediaSource(currentIndex, newIndex, handler, onCompletionAction); + } + + /** Clears the playlist. */ + public synchronized void clear() { + removeMediaSourceRange(0, getSize()); + } + + /** + * Clears the playlist and executes a custom action on completion. + * + * @param handler The {@link Handler} to run {@code onCompletionAction}. + * @param onCompletionAction A {@link Runnable} which is executed immediately after the playlist + * has been cleared. + */ + public synchronized void clear(Handler handler, Runnable onCompletionAction) { + removeMediaSourceRange(0, getSize(), handler, onCompletionAction); + } + + /** Returns the number of media sources in the playlist. */ + public synchronized int getSize() { + return mediaSourcesPublic.size(); + } + + /** + * Returns the {@link MediaSource} at a specified index. + * + * @param index An index in the range of 0 <= index <= {@link #getSize()}. + * @return The {@link MediaSource} at this index. + */ + public synchronized MediaSource getMediaSource(int index) { + return mediaSourcesPublic.get(index).mediaSource; + } + + /** + * Sets a new shuffle order to use when shuffling the child media sources. + * + * @param shuffleOrder A {@link ShuffleOrder}. + */ + public synchronized void setShuffleOrder(ShuffleOrder shuffleOrder) { + setPublicShuffleOrder(shuffleOrder, /* handler= */ null, /* onCompletionAction= */ null); + } + + /** + * Sets a new shuffle order to use when shuffling the child media sources. + * + * @param shuffleOrder A {@link ShuffleOrder}. + * @param handler The {@link Handler} to run {@code onCompletionAction}. + * @param onCompletionAction A {@link Runnable} which is executed immediately after the shuffle + * order has been changed. + */ + public synchronized void setShuffleOrder( + ShuffleOrder shuffleOrder, Handler handler, Runnable onCompletionAction) { + setPublicShuffleOrder(shuffleOrder, handler, onCompletionAction); + } + + // CompositeMediaSource implementation. + + @Override + @Nullable + public Object getTag() { + return null; } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - this.listener = listener; - for (int i = 0; i < mediaSources.length; i++) { - if (!duplicateFlags[i]) { - final int index = i; - mediaSources[i].prepareSource(player, false, new Listener() { - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - handleSourceInfoRefreshed(index, timeline, manifest); - } - }); - } + protected synchronized void prepareSourceInternal( + @Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + playbackThreadHandler = new Handler(/* callback= */ this::handleMessage); + if (mediaSourcesPublic.isEmpty()) { + updateTimelineAndScheduleOnCompletionActions(); + } else { + shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); + addMediaSourcesInternal(0, mediaSourcesPublic); + scheduleTimelineUpdate(); } } + @SuppressWarnings("MissingSuperCall") @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - for (int i = 0; i < mediaSources.length; i++) { - if (!duplicateFlags[i]) { - mediaSources[i].maybeThrowSourceInfoRefreshError(); - } - } + protected void enableInternal() { + // Suppress enabling all child sources here as they can be lazily enabled when creating periods. } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - int sourceIndex = timeline.getSourceIndexForPeriod(index); - int periodIndexInSource = index - timeline.getFirstPeriodIndexInSource(sourceIndex); - MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIndexInSource, allocator, - positionUs); - sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid); + MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(getChildPeriodUid(id.periodUid)); + MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid); + if (holder == null) { + // Stale event. The media source has already been removed. + holder = new MediaSourceHolder(new DummyMediaSource(), useLazyPreparation); + holder.isRemoved = true; + prepareChildSource(holder, holder.mediaSource); + } + enableMediaSource(holder); + holder.activeMediaPeriodIds.add(childMediaPeriodId); + MediaPeriod mediaPeriod = + holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); + mediaSourceByMediaPeriod.put(mediaPeriod, holder); + disableUnusedMediaSources(); return mediaPeriod; } @Override public void releasePeriod(MediaPeriod mediaPeriod) { - int sourceIndex = sourceIndexByMediaPeriod.get(mediaPeriod); - sourceIndexByMediaPeriod.remove(mediaPeriod); - mediaSources[sourceIndex].releasePeriod(mediaPeriod); + MediaSourceHolder holder = + Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod)); + holder.mediaSource.releasePeriod(mediaPeriod); + holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id); + if (!mediaSourceByMediaPeriod.isEmpty()) { + disableUnusedMediaSources(); + } + maybeReleaseChildSource(holder); } @Override - public void releaseSource() { - for (int i = 0; i < mediaSources.length; i++) { - if (!duplicateFlags[i]) { - mediaSources[i].releaseSource(); + protected void disableInternal() { + super.disableInternal(); + enabledMediaSourceHolders.clear(); + } + + @Override + protected synchronized void releaseSourceInternal() { + super.releaseSourceInternal(); + mediaSourceHolders.clear(); + enabledMediaSourceHolders.clear(); + mediaSourceByUid.clear(); + shuffleOrder = shuffleOrder.cloneAndClear(); + if (playbackThreadHandler != null) { + playbackThreadHandler.removeCallbacksAndMessages(null); + playbackThreadHandler = null; + } + timelineUpdateScheduled = false; + nextTimelineUpdateOnCompletionActions.clear(); + dispatchOnCompletionActions(pendingOnCompletionActions); + } + + @Override + protected void onChildSourceInfoRefreshed( + MediaSourceHolder mediaSourceHolder, MediaSource mediaSource, Timeline timeline) { + updateMediaSourceInternal(mediaSourceHolder, timeline); + } + + @Override + @Nullable + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + MediaSourceHolder mediaSourceHolder, MediaPeriodId mediaPeriodId) { + for (int i = 0; i < mediaSourceHolder.activeMediaPeriodIds.size(); i++) { + // Ensure the reported media period id has the same window sequence number as the one created + // by this media source. Otherwise it does not belong to this child source. + if (mediaSourceHolder.activeMediaPeriodIds.get(i).windowSequenceNumber + == mediaPeriodId.windowSequenceNumber) { + Object periodUid = getPeriodUid(mediaSourceHolder, mediaPeriodId.periodUid); + return mediaPeriodId.copyWithPeriodUid(periodUid); + } + } + return null; + } + + @Override + protected int getWindowIndexForChildWindowIndex( + MediaSourceHolder mediaSourceHolder, int windowIndex) { + return windowIndex + mediaSourceHolder.firstWindowIndexInChild; + } + + // Internal methods. Called from any thread. + + @GuardedBy("this") + private void addPublicMediaSources( + int index, + Collection mediaSources, + @Nullable Handler handler, + @Nullable Runnable onCompletionAction) { + Assertions.checkArgument((handler == null) == (onCompletionAction == null)); + Handler playbackThreadHandler = this.playbackThreadHandler; + for (MediaSource mediaSource : mediaSources) { + Assertions.checkNotNull(mediaSource); + } + List mediaSourceHolders = new ArrayList<>(mediaSources.size()); + for (MediaSource mediaSource : mediaSources) { + mediaSourceHolders.add(new MediaSourceHolder(mediaSource, useLazyPreparation)); + } + mediaSourcesPublic.addAll(index, mediaSourceHolders); + if (playbackThreadHandler != null && !mediaSources.isEmpty()) { + HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction); + playbackThreadHandler + .obtainMessage(MSG_ADD, new MessageData<>(index, mediaSourceHolders, callbackAction)) + .sendToTarget(); + } else if (onCompletionAction != null && handler != null) { + handler.post(onCompletionAction); + } + } + + @GuardedBy("this") + private void removePublicMediaSources( + int fromIndex, + int toIndex, + @Nullable Handler handler, + @Nullable Runnable onCompletionAction) { + Assertions.checkArgument((handler == null) == (onCompletionAction == null)); + Handler playbackThreadHandler = this.playbackThreadHandler; + Util.removeRange(mediaSourcesPublic, fromIndex, toIndex); + if (playbackThreadHandler != null) { + HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction); + playbackThreadHandler + .obtainMessage(MSG_REMOVE, new MessageData<>(fromIndex, toIndex, callbackAction)) + .sendToTarget(); + } else if (onCompletionAction != null && handler != null) { + handler.post(onCompletionAction); + } + } + + @GuardedBy("this") + private void movePublicMediaSource( + int currentIndex, + int newIndex, + @Nullable Handler handler, + @Nullable Runnable onCompletionAction) { + Assertions.checkArgument((handler == null) == (onCompletionAction == null)); + Handler playbackThreadHandler = this.playbackThreadHandler; + mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); + if (playbackThreadHandler != null) { + HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction); + playbackThreadHandler + .obtainMessage(MSG_MOVE, new MessageData<>(currentIndex, newIndex, callbackAction)) + .sendToTarget(); + } else if (onCompletionAction != null && handler != null) { + handler.post(onCompletionAction); + } + } + + @GuardedBy("this") + private void setPublicShuffleOrder( + ShuffleOrder shuffleOrder, @Nullable Handler handler, @Nullable Runnable onCompletionAction) { + Assertions.checkArgument((handler == null) == (onCompletionAction == null)); + Handler playbackThreadHandler = this.playbackThreadHandler; + if (playbackThreadHandler != null) { + int size = getSize(); + if (shuffleOrder.getLength() != size) { + shuffleOrder = + shuffleOrder + .cloneAndClear() + .cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size); + } + HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction); + playbackThreadHandler + .obtainMessage( + MSG_SET_SHUFFLE_ORDER, + new MessageData<>(/* index= */ 0, shuffleOrder, callbackAction)) + .sendToTarget(); + } else { + this.shuffleOrder = + shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder; + if (onCompletionAction != null && handler != null) { + handler.post(onCompletionAction); } } } - private void handleSourceInfoRefreshed(int sourceFirstIndex, Timeline sourceTimeline, - Object sourceManifest) { - // Set the timeline and manifest. - timelines[sourceFirstIndex] = sourceTimeline; - manifests[sourceFirstIndex] = sourceManifest; - // Also set the timeline and manifest for any duplicate entries of the same source. - for (int i = sourceFirstIndex + 1; i < mediaSources.length; i++) { - if (mediaSources[i] == mediaSources[sourceFirstIndex]) { - timelines[i] = sourceTimeline; - manifests[i] = sourceManifest; - } + @GuardedBy("this") + @Nullable + private HandlerAndRunnable createOnCompletionAction( + @Nullable Handler handler, @Nullable Runnable runnable) { + if (handler == null || runnable == null) { + return null; } - for (Timeline timeline : timelines) { - if (timeline == null) { - // Don't invoke the listener until all sources have timelines. - return; - } - } - timeline = new ConcatenatedTimeline(timelines.clone()); - listener.onSourceInfoRefreshed(timeline, manifests.clone()); + HandlerAndRunnable handlerAndRunnable = new HandlerAndRunnable(handler, runnable); + pendingOnCompletionActions.add(handlerAndRunnable); + return handlerAndRunnable; } - private static boolean[] buildDuplicateFlags(MediaSource[] mediaSources) { - boolean[] duplicateFlags = new boolean[mediaSources.length]; - IdentityHashMap sources = new IdentityHashMap<>(mediaSources.length); - for (int i = 0; i < mediaSources.length; i++) { - MediaSource source = mediaSources[i]; - if (!sources.containsKey(source)) { - sources.put(source, null); - } else { - duplicateFlags[i] = true; - } + // Internal methods. Called on the playback thread. + + @SuppressWarnings("unchecked") + private boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_ADD: + MessageData> addMessage = + (MessageData>) Util.castNonNull(msg.obj); + shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size()); + addMediaSourcesInternal(addMessage.index, addMessage.customData); + scheduleTimelineUpdate(addMessage.onCompletionAction); + break; + case MSG_REMOVE: + MessageData removeMessage = (MessageData) Util.castNonNull(msg.obj); + int fromIndex = removeMessage.index; + int toIndex = removeMessage.customData; + if (fromIndex == 0 && toIndex == shuffleOrder.getLength()) { + shuffleOrder = shuffleOrder.cloneAndClear(); + } else { + shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndex); + } + for (int index = toIndex - 1; index >= fromIndex; index--) { + removeMediaSourceInternal(index); + } + scheduleTimelineUpdate(removeMessage.onCompletionAction); + break; + case MSG_MOVE: + MessageData moveMessage = (MessageData) Util.castNonNull(msg.obj); + shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1); + shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1); + moveMediaSourceInternal(moveMessage.index, moveMessage.customData); + scheduleTimelineUpdate(moveMessage.onCompletionAction); + break; + case MSG_SET_SHUFFLE_ORDER: + MessageData shuffleOrderMessage = + (MessageData) Util.castNonNull(msg.obj); + shuffleOrder = shuffleOrderMessage.customData; + scheduleTimelineUpdate(shuffleOrderMessage.onCompletionAction); + break; + case MSG_UPDATE_TIMELINE: + updateTimelineAndScheduleOnCompletionActions(); + break; + case MSG_ON_COMPLETION: + Set actions = (Set) Util.castNonNull(msg.obj); + dispatchOnCompletionActions(actions); + break; + default: + throw new IllegalStateException(); } - return duplicateFlags; + return true; } - /** - * A {@link Timeline} that is the concatenation of one or more {@link Timeline}s. - */ - private static final class ConcatenatedTimeline extends Timeline { + private void scheduleTimelineUpdate() { + scheduleTimelineUpdate(/* onCompletionAction= */ null); + } + private void scheduleTimelineUpdate(@Nullable HandlerAndRunnable onCompletionAction) { + if (!timelineUpdateScheduled) { + getPlaybackThreadHandlerOnPlaybackThread().obtainMessage(MSG_UPDATE_TIMELINE).sendToTarget(); + timelineUpdateScheduled = true; + } + if (onCompletionAction != null) { + nextTimelineUpdateOnCompletionActions.add(onCompletionAction); + } + } + + private void updateTimelineAndScheduleOnCompletionActions() { + timelineUpdateScheduled = false; + Set onCompletionActions = nextTimelineUpdateOnCompletionActions; + nextTimelineUpdateOnCompletionActions = new HashSet<>(); + refreshSourceInfo(new ConcatenatedTimeline(mediaSourceHolders, shuffleOrder, isAtomic)); + getPlaybackThreadHandlerOnPlaybackThread() + .obtainMessage(MSG_ON_COMPLETION, onCompletionActions) + .sendToTarget(); + } + + @SuppressWarnings("GuardedBy") + private Handler getPlaybackThreadHandlerOnPlaybackThread() { + // Write access to this value happens on the playback thread only, so playback thread reads + // don't need to be synchronized. + return Assertions.checkNotNull(playbackThreadHandler); + } + + private synchronized void dispatchOnCompletionActions( + Set onCompletionActions) { + for (HandlerAndRunnable pendingAction : onCompletionActions) { + pendingAction.dispatch(); + } + pendingOnCompletionActions.removeAll(onCompletionActions); + } + + private void addMediaSourcesInternal( + int index, Collection mediaSourceHolders) { + for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { + addMediaSourceInternal(index++, mediaSourceHolder); + } + } + + private void addMediaSourceInternal(int newIndex, MediaSourceHolder newMediaSourceHolder) { + if (newIndex > 0) { + MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1); + Timeline previousTimeline = previousHolder.mediaSource.getTimeline(); + newMediaSourceHolder.reset( + newIndex, previousHolder.firstWindowIndexInChild + previousTimeline.getWindowCount()); + } else { + newMediaSourceHolder.reset(newIndex, /* firstWindowIndexInChild= */ 0); + } + Timeline newTimeline = newMediaSourceHolder.mediaSource.getTimeline(); + correctOffsets(newIndex, /* childIndexUpdate= */ 1, newTimeline.getWindowCount()); + mediaSourceHolders.add(newIndex, newMediaSourceHolder); + mediaSourceByUid.put(newMediaSourceHolder.uid, newMediaSourceHolder); + prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource); + if (isEnabled() && mediaSourceByMediaPeriod.isEmpty()) { + enabledMediaSourceHolders.add(newMediaSourceHolder); + } else { + disableChildSource(newMediaSourceHolder); + } + } + + private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) { + if (mediaSourceHolder == null) { + throw new IllegalArgumentException(); + } + if (mediaSourceHolder.childIndex + 1 < mediaSourceHolders.size()) { + MediaSourceHolder nextHolder = mediaSourceHolders.get(mediaSourceHolder.childIndex + 1); + int windowOffsetUpdate = + timeline.getWindowCount() + - (nextHolder.firstWindowIndexInChild - mediaSourceHolder.firstWindowIndexInChild); + if (windowOffsetUpdate != 0) { + correctOffsets( + mediaSourceHolder.childIndex + 1, /* childIndexUpdate= */ 0, windowOffsetUpdate); + } + } + scheduleTimelineUpdate(); + } + + private void removeMediaSourceInternal(int index) { + MediaSourceHolder holder = mediaSourceHolders.remove(index); + mediaSourceByUid.remove(holder.uid); + Timeline oldTimeline = holder.mediaSource.getTimeline(); + correctOffsets(index, /* childIndexUpdate= */ -1, -oldTimeline.getWindowCount()); + holder.isRemoved = true; + maybeReleaseChildSource(holder); + } + + private void moveMediaSourceInternal(int currentIndex, int newIndex) { + int startIndex = Math.min(currentIndex, newIndex); + int endIndex = Math.max(currentIndex, newIndex); + int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild; + mediaSourceHolders.add(newIndex, mediaSourceHolders.remove(currentIndex)); + for (int i = startIndex; i <= endIndex; i++) { + MediaSourceHolder holder = mediaSourceHolders.get(i); + holder.childIndex = i; + holder.firstWindowIndexInChild = windowOffset; + windowOffset += holder.mediaSource.getTimeline().getWindowCount(); + } + } + + private void correctOffsets(int startIndex, int childIndexUpdate, int windowOffsetUpdate) { + // TODO: Replace window index with uid in reporting to get rid of this inefficient method and + // the childIndex and firstWindowIndexInChild variables. + for (int i = startIndex; i < mediaSourceHolders.size(); i++) { + MediaSourceHolder holder = mediaSourceHolders.get(i); + holder.childIndex += childIndexUpdate; + holder.firstWindowIndexInChild += windowOffsetUpdate; + } + } + + private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) { + // Release if the source has been removed from the playlist and no periods are still active. + if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) { + enabledMediaSourceHolders.remove(mediaSourceHolder); + releaseChildSource(mediaSourceHolder); + } + } + + private void enableMediaSource(MediaSourceHolder mediaSourceHolder) { + enabledMediaSourceHolders.add(mediaSourceHolder); + enableChildSource(mediaSourceHolder); + } + + private void disableUnusedMediaSources() { + Iterator iterator = enabledMediaSourceHolders.iterator(); + while (iterator.hasNext()) { + MediaSourceHolder holder = iterator.next(); + if (holder.activeMediaPeriodIds.isEmpty()) { + disableChildSource(holder); + iterator.remove(); + } + } + } + + /** Return uid of media source holder from period uid of concatenated source. */ + private static Object getMediaSourceHolderUid(Object periodUid) { + return ConcatenatedTimeline.getChildTimelineUidFromConcatenatedUid(periodUid); + } + + /** Return uid of child period from period uid of concatenated source. */ + private static Object getChildPeriodUid(Object periodUid) { + return ConcatenatedTimeline.getChildPeriodUidFromConcatenatedUid(periodUid); + } + + private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) { + return ConcatenatedTimeline.getConcatenatedUid(holder.uid, childPeriodUid); + } + + /** Data class to hold playlist media sources together with meta data needed to process them. */ + /* package */ static final class MediaSourceHolder { + + public final MaskingMediaSource mediaSource; + public final Object uid; + public final List activeMediaPeriodIds; + + public int childIndex; + public int firstWindowIndexInChild; + public boolean isRemoved; + + public MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation) { + this.mediaSource = new MaskingMediaSource(mediaSource, useLazyPreparation); + this.activeMediaPeriodIds = new ArrayList<>(); + this.uid = new Object(); + } + + public void reset(int childIndex, int firstWindowIndexInChild) { + this.childIndex = childIndex; + this.firstWindowIndexInChild = firstWindowIndexInChild; + this.isRemoved = false; + this.activeMediaPeriodIds.clear(); + } + } + + /** Message used to post actions from app thread to playback thread. */ + private static final class MessageData { + + public final int index; + public final T customData; + @Nullable public final HandlerAndRunnable onCompletionAction; + + public MessageData(int index, T customData, @Nullable HandlerAndRunnable onCompletionAction) { + this.index = index; + this.customData = customData; + this.onCompletionAction = onCompletionAction; + } + } + + /** Timeline exposing concatenated timelines of playlist media sources. */ + private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline { + + private final int windowCount; + private final int periodCount; + private final int[] firstPeriodInChildIndices; + private final int[] firstWindowInChildIndices; private final Timeline[] timelines; - private final int[] sourcePeriodOffsets; - private final int[] sourceWindowOffsets; + private final Object[] uids; + private final HashMap childIndexByUid; - public ConcatenatedTimeline(Timeline[] timelines) { - int[] sourcePeriodOffsets = new int[timelines.length]; - int[] sourceWindowOffsets = new int[timelines.length]; - long periodCount = 0; + public ConcatenatedTimeline( + Collection mediaSourceHolders, + ShuffleOrder shuffleOrder, + boolean isAtomic) { + super(isAtomic, shuffleOrder); + int childCount = mediaSourceHolders.size(); + firstPeriodInChildIndices = new int[childCount]; + firstWindowInChildIndices = new int[childCount]; + timelines = new Timeline[childCount]; + uids = new Object[childCount]; + childIndexByUid = new HashMap<>(); + int index = 0; int windowCount = 0; - for (int i = 0; i < timelines.length; i++) { - Timeline timeline = timelines[i]; - periodCount += timeline.getPeriodCount(); - Assertions.checkState(periodCount <= Integer.MAX_VALUE, - "ConcatenatingMediaSource children contain too many periods"); - sourcePeriodOffsets[i] = (int) periodCount; - windowCount += timeline.getWindowCount(); - sourceWindowOffsets[i] = windowCount; + int periodCount = 0; + for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { + timelines[index] = mediaSourceHolder.mediaSource.getTimeline(); + firstWindowInChildIndices[index] = windowCount; + firstPeriodInChildIndices[index] = periodCount; + windowCount += timelines[index].getWindowCount(); + periodCount += timelines[index].getPeriodCount(); + uids[index] = mediaSourceHolder.uid; + childIndexByUid.put(uids[index], index++); } - this.timelines = timelines; - this.sourcePeriodOffsets = sourcePeriodOffsets; - this.sourceWindowOffsets = sourceWindowOffsets; + this.windowCount = windowCount; + this.periodCount = periodCount; + } + + @Override + protected int getChildIndexByPeriodIndex(int periodIndex) { + return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false); + } + + @Override + protected int getChildIndexByWindowIndex(int windowIndex) { + return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false); + } + + @Override + protected int getChildIndexByChildUid(Object childUid) { + Integer index = childIndexByUid.get(childUid); + return index == null ? C.INDEX_UNSET : index; + } + + @Override + protected Timeline getTimelineByChildIndex(int childIndex) { + return timelines[childIndex]; + } + + @Override + protected int getFirstPeriodIndexByChildIndex(int childIndex) { + return firstPeriodInChildIndices[childIndex]; + } + + @Override + protected int getFirstWindowIndexByChildIndex(int childIndex) { + return firstWindowInChildIndices[childIndex]; + } + + @Override + protected Object getChildUidByChildIndex(int childIndex) { + return uids[childIndex]; } @Override public int getWindowCount() { - return sourceWindowOffsets[sourceWindowOffsets.length - 1]; - } - - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - int sourceIndex = getSourceIndexForWindow(windowIndex); - int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex); - int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex); - timelines[sourceIndex].getWindow(windowIndex - firstWindowIndexInSource, window, setIds, - defaultPositionProjectionUs); - window.firstPeriodIndex += firstPeriodIndexInSource; - window.lastPeriodIndex += firstPeriodIndexInSource; - return window; + return windowCount; } @Override public int getPeriodCount() { - return sourcePeriodOffsets[sourcePeriodOffsets.length - 1]; + return periodCount; } - - @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - int sourceIndex = getSourceIndexForPeriod(periodIndex); - int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex); - int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex); - timelines[sourceIndex].getPeriod(periodIndex - firstPeriodIndexInSource, period, setIds); - period.windowIndex += firstWindowIndexInSource; - if (setIds) { - period.uid = Pair.create(sourceIndex, period.uid); - } - return period; - } - - @Override - public int getIndexOfPeriod(Object uid) { - if (!(uid instanceof Pair)) { - return C.INDEX_UNSET; - } - Pair sourceIndexAndPeriodId = (Pair) uid; - if (!(sourceIndexAndPeriodId.first instanceof Integer)) { - return C.INDEX_UNSET; - } - int sourceIndex = (Integer) sourceIndexAndPeriodId.first; - Object periodId = sourceIndexAndPeriodId.second; - if (sourceIndex < 0 || sourceIndex >= timelines.length) { - return C.INDEX_UNSET; - } - int periodIndexInSource = timelines[sourceIndex].getIndexOfPeriod(periodId); - return periodIndexInSource == C.INDEX_UNSET ? C.INDEX_UNSET - : getFirstPeriodIndexInSource(sourceIndex) + periodIndexInSource; - } - - private int getSourceIndexForPeriod(int periodIndex) { - return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex, true, false) + 1; - } - - private int getFirstPeriodIndexInSource(int sourceIndex) { - return sourceIndex == 0 ? 0 : sourcePeriodOffsets[sourceIndex - 1]; - } - - private int getSourceIndexForWindow(int windowIndex) { - return Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1; - } - - private int getFirstWindowIndexInSource(int sourceIndex) { - return sourceIndex == 0 ? 0 : sourceWindowOffsets[sourceIndex - 1]; - } - } + /** Dummy media source which does nothing and does not support creating periods. */ + private static final class DummyMediaSource extends BaseMediaSource { + + @Override + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + // Do nothing. + } + + @Override + @Nullable + public Object getTag() { + return null; + } + + @Override + protected void releaseSourceInternal() { + // Do nothing. + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + // Do nothing. + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + throw new UnsupportedOperationException(); + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + // Do nothing. + } + } + + private static final class HandlerAndRunnable { + + private final Handler handler; + private final Runnable runnable; + + public HandlerAndRunnable(Handler handler, Runnable runnable) { + this.handler = handler; + this.runnable = runnable; + } + + public void dispatch() { + handler.post(runnable); + } + } } + diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java new file mode 100644 index 000000000000..237510bea3f1 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +/** + * Default implementation of {@link CompositeSequenceableLoaderFactory}. + */ +public final class DefaultCompositeSequenceableLoaderFactory + implements CompositeSequenceableLoaderFactory { + + @Override + public SequenceableLoader createCompositeSequenceableLoader(SequenceableLoader... loaders) { + return new CompositeSequenceableLoader(loaders); + } + +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java new file mode 100644 index 000000000000..c25750247ff2 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +/** + * @deprecated Use {@link MediaSourceEventListener} interface directly for selective overrides as + * all methods are implemented as no-op default methods. + */ +@Deprecated +public abstract class DefaultMediaSourceEventListener implements MediaSourceEventListener {} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/EmptySampleStream.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/EmptySampleStream.java index 2b7cde8b238f..398c6b91fcdd 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/EmptySampleStream.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/EmptySampleStream.java @@ -43,8 +43,8 @@ public final class EmptySampleStream implements SampleStream { } @Override - public void skipData(long positionUs) { - // Do nothing. + public int skipData(long positionUs) { + return 0; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java deleted file mode 100644 index 5c349c65f87e..000000000000 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ /dev/null @@ -1,737 +0,0 @@ -/* - * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; - -import android.net.Uri; -import android.os.Handler; -import android.util.SparseArray; -import org.mozilla.thirdparty.com.google.android.exoplayer2.C; -import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; -import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; -import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultExtractorInput; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultTrackOutput; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultTrackOutput.UpstreamFormatChangedListener; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; -import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.Loadable; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ConditionVariable; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; -import java.io.EOFException; -import java.io.IOException; - -/** - * A {@link MediaPeriod} that extracts data using an {@link Extractor}. - */ -/* package */ final class ExtractorMediaPeriod implements MediaPeriod, ExtractorOutput, - Loader.Callback, UpstreamFormatChangedListener { - - /** - * When the source's duration is unknown, it is calculated by adding this value to the largest - * sample timestamp seen when buffering completes. - */ - private static final long DEFAULT_LAST_SAMPLE_DURATION_US = 10000; - - private final Uri uri; - private final DataSource dataSource; - private final int minLoadableRetryCount; - private final Handler eventHandler; - private final ExtractorMediaSource.EventListener eventListener; - private final MediaSource.Listener sourceListener; - private final Allocator allocator; - private final String customCacheKey; - private final Loader loader; - private final ExtractorHolder extractorHolder; - private final ConditionVariable loadCondition; - private final Runnable maybeFinishPrepareRunnable; - private final Runnable onContinueLoadingRequestedRunnable; - private final Handler handler; - private final SparseArray sampleQueues; - - private Callback callback; - private SeekMap seekMap; - private boolean tracksBuilt; - private boolean prepared; - - private boolean seenFirstTrackSelection; - private boolean notifyReset; - private int enabledTrackCount; - private TrackGroupArray tracks; - private long durationUs; - private boolean[] trackEnabledStates; - private boolean[] trackIsAudioVideoFlags; - private boolean haveAudioVideoTracks; - private long length; - - private long lastSeekPositionUs; - private long pendingResetPositionUs; - - private int extractedSamplesCountAtStartOfLoad; - private boolean loadingFinished; - private boolean released; - - /** - * @param uri The {@link Uri} of the media stream. - * @param dataSource The data source to read the media. - * @param extractors The extractors to use to read the data source. - * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @param eventHandler A handler for events. May be null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param sourceListener A listener to notify when the timeline has been loaded. - * @param allocator An {@link Allocator} from which to obtain media buffer allocations. - * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache - * indexing. May be null. - */ - public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors, - int minLoadableRetryCount, Handler eventHandler, - ExtractorMediaSource.EventListener eventListener, MediaSource.Listener sourceListener, - Allocator allocator, String customCacheKey) { - this.uri = uri; - this.dataSource = dataSource; - this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - this.sourceListener = sourceListener; - this.allocator = allocator; - this.customCacheKey = customCacheKey; - loader = new Loader("Loader:ExtractorMediaPeriod"); - extractorHolder = new ExtractorHolder(extractors, this); - loadCondition = new ConditionVariable(); - maybeFinishPrepareRunnable = new Runnable() { - @Override - public void run() { - maybeFinishPrepare(); - } - }; - onContinueLoadingRequestedRunnable = new Runnable() { - @Override - public void run() { - if (!released) { - callback.onContinueLoadingRequested(ExtractorMediaPeriod.this); - } - } - }; - handler = new Handler(); - - pendingResetPositionUs = C.TIME_UNSET; - sampleQueues = new SparseArray<>(); - length = C.LENGTH_UNSET; - } - - public void release() { - final ExtractorHolder extractorHolder = this.extractorHolder; - loader.release(new Runnable() { - @Override - public void run() { - extractorHolder.release(); - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).disable(); - } - } - }); - handler.removeCallbacksAndMessages(null); - released = true; - } - - @Override - public void prepare(Callback callback) { - this.callback = callback; - loadCondition.open(); - startLoading(); - } - - @Override - public void maybeThrowPrepareError() throws IOException { - maybeThrowError(); - } - - @Override - public TrackGroupArray getTrackGroups() { - return tracks; - } - - @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - Assertions.checkState(prepared); - // Disable old tracks. - for (int i = 0; i < selections.length; i++) { - if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { - int track = ((SampleStreamImpl) streams[i]).track; - Assertions.checkState(trackEnabledStates[track]); - enabledTrackCount--; - trackEnabledStates[track] = false; - sampleQueues.valueAt(track).disable(); - streams[i] = null; - } - } - // Enable new tracks. - boolean selectedNewTracks = false; - for (int i = 0; i < selections.length; i++) { - if (streams[i] == null && selections[i] != null) { - TrackSelection selection = selections[i]; - Assertions.checkState(selection.length() == 1); - Assertions.checkState(selection.getIndexInTrackGroup(0) == 0); - int track = tracks.indexOf(selection.getTrackGroup()); - Assertions.checkState(!trackEnabledStates[track]); - enabledTrackCount++; - trackEnabledStates[track] = true; - streams[i] = new SampleStreamImpl(track); - streamResetFlags[i] = true; - selectedNewTracks = true; - } - } - if (!seenFirstTrackSelection) { - // At the time of the first track selection all queues will be enabled, so we need to disable - // any that are no longer required. - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - if (!trackEnabledStates[i]) { - sampleQueues.valueAt(i).disable(); - } - } - } - if (enabledTrackCount == 0) { - notifyReset = false; - if (loader.isLoading()) { - loader.cancelLoading(); - } - } else if (seenFirstTrackSelection ? selectedNewTracks : positionUs != 0) { - positionUs = seekToUs(positionUs); - // We'll need to reset renderers consuming from all streams due to the seek. - for (int i = 0; i < streams.length; i++) { - if (streams[i] != null) { - streamResetFlags[i] = true; - } - } - } - seenFirstTrackSelection = true; - return positionUs; - } - - @Override - public void discardBuffer(long positionUs) { - // Do nothing. - } - - @Override - public boolean continueLoading(long playbackPositionUs) { - if (loadingFinished || (prepared && enabledTrackCount == 0)) { - return false; - } - boolean continuedLoading = loadCondition.open(); - if (!loader.isLoading()) { - startLoading(); - continuedLoading = true; - } - return continuedLoading; - } - - @Override - public long getNextLoadPositionUs() { - return enabledTrackCount == 0 ? C.TIME_END_OF_SOURCE : getBufferedPositionUs(); - } - - @Override - public long readDiscontinuity() { - if (notifyReset) { - notifyReset = false; - return lastSeekPositionUs; - } - return C.TIME_UNSET; - } - - @Override - public long getBufferedPositionUs() { - if (loadingFinished) { - return C.TIME_END_OF_SOURCE; - } else if (isPendingReset()) { - return pendingResetPositionUs; - } - long largestQueuedTimestampUs; - if (haveAudioVideoTracks) { - // Ignore non-AV tracks, which may be sparse or poorly interleaved. - largestQueuedTimestampUs = Long.MAX_VALUE; - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - if (trackIsAudioVideoFlags[i]) { - largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs, - sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); - } - } - } else { - largestQueuedTimestampUs = getLargestQueuedTimestampUs(); - } - return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs - : largestQueuedTimestampUs; - } - - @Override - public long seekToUs(long positionUs) { - // Treat all seeks into non-seekable media as being to t=0. - positionUs = seekMap.isSeekable() ? positionUs : 0; - lastSeekPositionUs = positionUs; - int trackCount = sampleQueues.size(); - // If we're not pending a reset, see if we can seek within the sample queues. - boolean seekInsideBuffer = !isPendingReset(); - for (int i = 0; seekInsideBuffer && i < trackCount; i++) { - if (trackEnabledStates[i]) { - seekInsideBuffer = sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs, false); - } - } - // If we failed to seek within the sample queues, we need to restart. - if (!seekInsideBuffer) { - pendingResetPositionUs = positionUs; - loadingFinished = false; - if (loader.isLoading()) { - loader.cancelLoading(); - } else { - for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).reset(trackEnabledStates[i]); - } - } - } - notifyReset = false; - return positionUs; - } - - // SampleStream methods. - - /* package */ boolean isReady(int track) { - return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(track).isEmpty()); - } - - /* package */ void maybeThrowError() throws IOException { - loader.maybeThrowError(); - } - - /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer, - boolean formatRequired) { - if (notifyReset || isPendingReset()) { - return C.RESULT_NOTHING_READ; - } - - return sampleQueues.valueAt(track).readData(formatHolder, buffer, formatRequired, - loadingFinished, lastSeekPositionUs); - } - - /* package */ void skipData(int track, long positionUs) { - DefaultTrackOutput sampleQueue = sampleQueues.valueAt(track); - if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - sampleQueue.skipAll(); - } else { - sampleQueue.skipToKeyframeBefore(positionUs, true); - } - } - - // Loader.Callback implementation. - - @Override - public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { - copyLengthFromLoader(loadable); - loadingFinished = true; - if (durationUs == C.TIME_UNSET) { - long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); - durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0 - : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; - sourceListener.onSourceInfoRefreshed( - new SinglePeriodTimeline(durationUs, seekMap.isSeekable()), null); - } - callback.onContinueLoadingRequested(this); - } - - @Override - public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { - copyLengthFromLoader(loadable); - if (!released && enabledTrackCount > 0) { - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).reset(trackEnabledStates[i]); - } - callback.onContinueLoadingRequested(this); - } - } - - @Override - public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, IOException error) { - copyLengthFromLoader(loadable); - notifyLoadError(error); - if (isLoadableExceptionFatal(error)) { - return Loader.DONT_RETRY_FATAL; - } - int extractedSamplesCount = getExtractedSamplesCount(); - boolean madeProgress = extractedSamplesCount > extractedSamplesCountAtStartOfLoad; - configureRetry(loadable); // May reset the sample queues. - extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); - return madeProgress ? Loader.RETRY_RESET_ERROR_COUNT : Loader.RETRY; - } - - // ExtractorOutput implementation. Called by the loading thread. - - @Override - public TrackOutput track(int id, int type) { - DefaultTrackOutput trackOutput = sampleQueues.get(id); - if (trackOutput == null) { - trackOutput = new DefaultTrackOutput(allocator); - trackOutput.setUpstreamFormatChangeListener(this); - sampleQueues.put(id, trackOutput); - } - return trackOutput; - } - - @Override - public void endTracks() { - tracksBuilt = true; - handler.post(maybeFinishPrepareRunnable); - } - - @Override - public void seekMap(SeekMap seekMap) { - this.seekMap = seekMap; - handler.post(maybeFinishPrepareRunnable); - } - - // UpstreamFormatChangedListener implementation. Called by the loading thread. - - @Override - public void onUpstreamFormatChanged(Format format) { - handler.post(maybeFinishPrepareRunnable); - } - - // Internal methods. - - private void maybeFinishPrepare() { - if (released || prepared || seekMap == null || !tracksBuilt) { - return; - } - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - if (sampleQueues.valueAt(i).getUpstreamFormat() == null) { - return; - } - } - loadCondition.close(); - TrackGroup[] trackArray = new TrackGroup[trackCount]; - trackIsAudioVideoFlags = new boolean[trackCount]; - trackEnabledStates = new boolean[trackCount]; - durationUs = seekMap.getDurationUs(); - for (int i = 0; i < trackCount; i++) { - Format trackFormat = sampleQueues.valueAt(i).getUpstreamFormat(); - trackArray[i] = new TrackGroup(trackFormat); - String mimeType = trackFormat.sampleMimeType; - boolean isAudioVideo = MimeTypes.isVideo(mimeType) || MimeTypes.isAudio(mimeType); - trackIsAudioVideoFlags[i] = isAudioVideo; - haveAudioVideoTracks |= isAudioVideo; - } - tracks = new TrackGroupArray(trackArray); - prepared = true; - sourceListener.onSourceInfoRefreshed( - new SinglePeriodTimeline(durationUs, seekMap.isSeekable()), null); - callback.onPrepared(this); - } - - private void copyLengthFromLoader(ExtractingLoadable loadable) { - if (length == C.LENGTH_UNSET) { - length = loadable.length; - } - } - - private void startLoading() { - ExtractingLoadable loadable = new ExtractingLoadable(uri, dataSource, extractorHolder, - loadCondition); - if (prepared) { - Assertions.checkState(isPendingReset()); - if (durationUs != C.TIME_UNSET && pendingResetPositionUs >= durationUs) { - loadingFinished = true; - pendingResetPositionUs = C.TIME_UNSET; - return; - } - loadable.setLoadPosition(seekMap.getPosition(pendingResetPositionUs), pendingResetPositionUs); - pendingResetPositionUs = C.TIME_UNSET; - } - extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); - - int minRetryCount = minLoadableRetryCount; - if (minRetryCount == ExtractorMediaSource.MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA) { - // We assume on-demand before we're prepared. - minRetryCount = !prepared || length != C.LENGTH_UNSET - || (seekMap != null && seekMap.getDurationUs() != C.TIME_UNSET) - ? ExtractorMediaSource.DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND - : ExtractorMediaSource.DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE; - } - loader.startLoading(loadable, this, minRetryCount); - } - - private void configureRetry(ExtractingLoadable loadable) { - if (length != C.LENGTH_UNSET - || (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. - } else { - // We're playing a stream of unknown length and duration. Assume it's live, and - // therefore that the data at the uri is a continuously shifting window of the latest - // available media. For this case there's no way to continue loading from where a - // previous load finished, so it's necessary to load from the start whenever commencing - // a new load. - lastSeekPositionUs = 0; - notifyReset = prepared; - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).reset(!prepared || trackEnabledStates[i]); - } - loadable.setLoadPosition(0, 0); - } - } - - private int getExtractedSamplesCount() { - int extractedSamplesCount = 0; - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - extractedSamplesCount += sampleQueues.valueAt(i).getWriteIndex(); - } - return extractedSamplesCount; - } - - private long getLargestQueuedTimestampUs() { - long largestQueuedTimestampUs = Long.MIN_VALUE; - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, - sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); - } - return largestQueuedTimestampUs; - } - - private boolean isPendingReset() { - return pendingResetPositionUs != C.TIME_UNSET; - } - - private boolean isLoadableExceptionFatal(IOException e) { - return e instanceof UnrecognizedInputFormatException; - } - - private void notifyLoadError(final IOException error) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onLoadError(error); - } - }); - } - } - - private final class SampleStreamImpl implements SampleStream { - - private final int track; - - public SampleStreamImpl(int track) { - this.track = track; - } - - @Override - public boolean isReady() { - return ExtractorMediaPeriod.this.isReady(track); - } - - @Override - public void maybeThrowError() throws IOException { - ExtractorMediaPeriod.this.maybeThrowError(); - } - - @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, - boolean formatRequired) { - return ExtractorMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired); - } - - @Override - public void skipData(long positionUs) { - ExtractorMediaPeriod.this.skipData(track, positionUs); - } - - } - - /** - * Loads the media stream and extracts sample data from it. - */ - /* package */ final class ExtractingLoadable implements Loadable { - - /** - * The number of bytes that should be loaded between each each invocation of - * {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. - */ - private static final int CONTINUE_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024; - - private final Uri uri; - private final DataSource dataSource; - private final ExtractorHolder extractorHolder; - private final ConditionVariable loadCondition; - private final PositionHolder positionHolder; - - private volatile boolean loadCanceled; - - private boolean pendingExtractorSeek; - private long seekTimeUs; - private long length; - - public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder, - ConditionVariable loadCondition) { - this.uri = Assertions.checkNotNull(uri); - this.dataSource = Assertions.checkNotNull(dataSource); - this.extractorHolder = Assertions.checkNotNull(extractorHolder); - this.loadCondition = loadCondition; - this.positionHolder = new PositionHolder(); - this.pendingExtractorSeek = true; - this.length = C.LENGTH_UNSET; - } - - public void setLoadPosition(long position, long timeUs) { - positionHolder.position = position; - seekTimeUs = timeUs; - pendingExtractorSeek = true; - } - - @Override - public void cancelLoad() { - loadCanceled = true; - } - - @Override - public boolean isLoadCanceled() { - return loadCanceled; - } - - @Override - public void load() throws IOException, InterruptedException { - int result = Extractor.RESULT_CONTINUE; - while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - ExtractorInput input = null; - try { - long position = positionHolder.position; - length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey)); - if (length != C.LENGTH_UNSET) { - length += position; - } - input = new DefaultExtractorInput(dataSource, position, length); - Extractor extractor = extractorHolder.selectExtractor(input, dataSource.getUri()); - if (pendingExtractorSeek) { - extractor.seek(position, seekTimeUs); - pendingExtractorSeek = false; - } - while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - loadCondition.block(); - result = extractor.read(input, positionHolder); - if (input.getPosition() > position + CONTINUE_LOADING_CHECK_INTERVAL_BYTES) { - position = input.getPosition(); - loadCondition.close(); - handler.post(onContinueLoadingRequestedRunnable); - } - } - } finally { - if (result == Extractor.RESULT_SEEK) { - result = Extractor.RESULT_CONTINUE; - } else if (input != null) { - positionHolder.position = input.getPosition(); - } - Util.closeQuietly(dataSource); - } - } - } - - } - - /** - * Stores a list of extractors and a selected extractor when the format has been detected. - */ - private static final class ExtractorHolder { - - private final Extractor[] extractors; - private final ExtractorOutput extractorOutput; - private Extractor extractor; - - /** - * Creates a holder that will select an extractor and initialize it using the specified output. - * - * @param extractors One or more extractors to choose from. - * @param extractorOutput The output that will be used to initialize the selected extractor. - */ - public ExtractorHolder(Extractor[] extractors, ExtractorOutput extractorOutput) { - this.extractors = extractors; - this.extractorOutput = extractorOutput; - } - - /** - * Returns an initialized extractor for reading {@code input}, and returns the same extractor on - * later calls. - * - * @param input The {@link ExtractorInput} from which data should be read. - * @param uri The {@link Uri} of the data. - * @return An initialized extractor for reading {@code input}. - * @throws UnrecognizedInputFormatException Thrown if the input format could not be detected. - * @throws IOException Thrown if the input could not be read. - * @throws InterruptedException Thrown if the thread was interrupted. - */ - public Extractor selectExtractor(ExtractorInput input, Uri uri) - throws IOException, InterruptedException { - if (extractor != null) { - return extractor; - } - for (Extractor extractor : extractors) { - try { - if (extractor.sniff(input)) { - this.extractor = extractor; - break; - } - } catch (EOFException e) { - // Do nothing. - } finally { - input.resetPeekPosition(); - } - } - if (extractor == null) { - throw new UnrecognizedInputFormatException("None of the available extractors (" - + Util.getCommaDelimitedSimpleClassNames(extractors) + ") could read the stream.", uri); - } - extractor.init(extractorOutput); - return extractor; - } - - public void release() { - if (extractor != null) { - extractor.release(); - extractor = null; - } - } - - } - -} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 7544ae7b7f96..3b72f51c447b 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -17,37 +17,40 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; -import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player; import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.IOException; -/** - * Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}. - *

- * If the possible input stream container formats are known, pass a factory that instantiates - * extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to - * use the default extractors. When reading a new stream, the first {@link Extractor} in the array - * of extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will - * be used to extract samples from the input stream. - *

- * Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking. - */ -public final class ExtractorMediaSource implements MediaSource, MediaSource.Listener { +/** @deprecated Use {@link ProgressiveMediaSource} instead. */ +@Deprecated +@SuppressWarnings("deprecation") +public final class ExtractorMediaSource extends CompositeMediaSource { - /** - * Listener of {@link ExtractorMediaSource} events. - */ + /** @deprecated Use {@link MediaSourceEventListener} instead. */ + @Deprecated public interface EventListener { /** * Called when an error occurs loading media data. + *

+ * This method being called does not indicate that playback has failed, or that it will fail. + * The player may be able to recover from the error and continue. Hence applications should + * not implement this method to display a user visible error or initiate an application + * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement + * such behavior). This method is called to provide the application with an opportunity to log + * the error if it wishes to do so. * * @param error The load error. */ @@ -55,35 +58,184 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List } - /** - * The default minimum number of times to retry loading prior to failing for on-demand streams. - */ - public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND = 3; + /** @deprecated Use {@link ProgressiveMediaSource.Factory} instead. */ + @Deprecated + public static final class Factory implements MediaSourceFactory { + + private final DataSource.Factory dataSourceFactory; + + @Nullable private ExtractorsFactory extractorsFactory; + @Nullable private String customCacheKey; + @Nullable private Object tag; + private LoadErrorHandlingPolicy loadErrorHandlingPolicy; + private int continueLoadingCheckIntervalBytes; + private boolean isCreateCalled; + + /** + * Creates a new factory for {@link ExtractorMediaSource}s. + * + * @param dataSourceFactory A factory for {@link DataSource}s to read the media. + */ + public Factory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = dataSourceFactory; + loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); + continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; + } + + /** + * Sets the factory for {@link Extractor}s to process the media stream. The default value is an + * instance of {@link DefaultExtractorsFactory}. + * + * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the + * possible formats are known, pass a factory that instantiates extractors for those + * formats. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) { + Assertions.checkState(!isCreateCalled); + this.extractorsFactory = extractorsFactory; + return this; + } + + /** + * Sets the custom key that uniquely identifies the original stream. Used for cache indexing. + * The default value is {@code null}. + * + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for + * cache indexing. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setCustomCacheKey(String customCacheKey) { + Assertions.checkState(!isCreateCalled); + this.customCacheKey = customCacheKey; + return this; + } + + /** + * Sets a tag for the media source which will be published in the {@link + * com.google.android.exoplayer2.Timeline} of the source as {@link + * com.google.android.exoplayer2.Timeline.Window#tag}. + * + * @param tag A tag for the media source. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setTag(Object tag) { + Assertions.checkState(!isCreateCalled); + this.tag = tag; + return this; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. See {@link + * #setLoadErrorHandlingPolicy} for the default value. + * + *

Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with + * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int) + * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)} + * + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead. + */ + @Deprecated + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)); + } + + /** + * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link + * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. + * + *

Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}. + * + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + Assertions.checkState(!isCreateCalled); + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + return this; + } + + /** + * Sets the number of bytes that should be loaded between each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. The default value is + * {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}. + * + * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between + * each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { + Assertions.checkState(!isCreateCalled); + this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; + return this; + } + + /** @deprecated Use {@link ProgressiveMediaSource.Factory#setDrmSessionManager} instead. */ + @Override + @Deprecated + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + throw new UnsupportedOperationException(); + } + + /** + * Returns a new {@link ExtractorMediaSource} using the current parameters. + * + * @param uri The {@link Uri}. + * @return The new {@link ExtractorMediaSource}. + */ + @Override + public ExtractorMediaSource createMediaSource(Uri uri) { + isCreateCalled = true; + if (extractorsFactory == null) { + extractorsFactory = new DefaultExtractorsFactory(); + } + return new ExtractorMediaSource( + uri, + dataSourceFactory, + extractorsFactory, + loadErrorHandlingPolicy, + customCacheKey, + continueLoadingCheckIntervalBytes, + tag); + } + + /** + * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, + * MediaSourceEventListener)} instead. + */ + @Deprecated + public ExtractorMediaSource createMediaSource( + Uri uri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) { + ExtractorMediaSource mediaSource = createMediaSource(uri); + if (eventHandler != null && eventListener != null) { + mediaSource.addEventListener(eventHandler, eventListener); + } + return mediaSource; + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_OTHER}; + } + } /** - * The default minimum number of times to retry loading prior to failing for live streams. + * @deprecated Use {@link ProgressiveMediaSource#DEFAULT_LOADING_CHECK_INTERVAL_BYTES} instead. */ - public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE = 6; + @Deprecated + public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = + ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES; - /** - * Value for {@code minLoadableRetryCount} that causes the loader to retry - * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE} times for live streams and - * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND} for on-demand streams. - */ - public static final int MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA = -1; - - private final Uri uri; - private final DataSource.Factory dataSourceFactory; - private final ExtractorsFactory extractorsFactory; - private final int minLoadableRetryCount; - private final Handler eventHandler; - private final EventListener eventListener; - private final Timeline.Period period; - private final String customCacheKey; - - private MediaSource.Listener sourceListener; - private Timeline timeline; - private boolean timelineHasDuration; + private final ProgressiveMediaSource progressiveMediaSource; /** * @param uri The {@link Uri} of the media stream. @@ -93,11 +245,16 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Factory} instead. */ - public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, - ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) { - this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler, - eventListener, null); + @Deprecated + public ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + @Nullable Handler eventHandler, + @Nullable EventListener eventListener) { + this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null); } /** @@ -110,12 +267,24 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List * @param eventListener A listener of events. May be null if delivery of events is not required. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache * indexing. May be null. + * @deprecated Use {@link Factory} instead. */ - public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, - ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener, - String customCacheKey) { - this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler, - eventListener, customCacheKey); + @Deprecated + public ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + @Nullable Handler eventHandler, + @Nullable EventListener eventListener, + @Nullable String customCacheKey) { + this( + uri, + dataSourceFactory, + extractorsFactory, + eventHandler, + eventListener, + customCacheKey, + DEFAULT_LOADING_CHECK_INTERVAL_BYTES); } /** @@ -124,68 +293,102 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the * possible formats are known, pass a factory that instantiates extractors for those formats. * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors. - * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache * indexing. May be null. + * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each + * invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + * @deprecated Use {@link Factory} instead. */ - public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, - ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, - EventListener eventListener, String customCacheKey) { - this.uri = uri; - this.dataSourceFactory = dataSourceFactory; - this.extractorsFactory = extractorsFactory; - this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - this.customCacheKey = customCacheKey; - period = new Timeline.Period(); + @Deprecated + public ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + @Nullable Handler eventHandler, + @Nullable EventListener eventListener, + @Nullable String customCacheKey, + int continueLoadingCheckIntervalBytes) { + this( + uri, + dataSourceFactory, + extractorsFactory, + new DefaultLoadErrorHandlingPolicy(), + customCacheKey, + continueLoadingCheckIntervalBytes, + /* tag= */ null); + if (eventListener != null && eventHandler != null) { + addEventListener(eventHandler, new EventListenerWrapper(eventListener)); + } + } + + private ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy, + @Nullable String customCacheKey, + int continueLoadingCheckIntervalBytes, + @Nullable Object tag) { + progressiveMediaSource = + new ProgressiveMediaSource( + uri, + dataSourceFactory, + extractorsFactory, + DrmSessionManager.getDummyDrmSessionManager(), + loadableLoadErrorHandlingPolicy, + customCacheKey, + continueLoadingCheckIntervalBytes, + tag); } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - sourceListener = listener; - timeline = new SinglePeriodTimeline(C.TIME_UNSET, false); - listener.onSourceInfoRefreshed(timeline, null); + @Nullable + public Object getTag() { + return progressiveMediaSource.getTag(); } @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - // Do nothing. + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + prepareChildSource(/* id= */ null, progressiveMediaSource); } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - Assertions.checkArgument(index == 0); - return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(), - extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener, - this, allocator, customCacheKey); + protected void onChildSourceInfoRefreshed( + @Nullable Void id, MediaSource mediaSource, Timeline timeline) { + refreshSourceInfo(timeline); + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + return progressiveMediaSource.createPeriod(id, allocator, startPositionUs); } @Override public void releasePeriod(MediaPeriod mediaPeriod) { - ((ExtractorMediaPeriod) mediaPeriod).release(); + progressiveMediaSource.releasePeriod(mediaPeriod); } - @Override - public void releaseSource() { - sourceListener = null; - } + @Deprecated + private static final class EventListenerWrapper implements MediaSourceEventListener { - // MediaSource.Listener implementation. + private final EventListener eventListener; - @Override - public void onSourceInfoRefreshed(Timeline newTimeline, Object manifest) { - long newTimelineDurationUs = newTimeline.getPeriod(0, period).getDurationUs(); - boolean newTimelineHasDuration = newTimelineDurationUs != C.TIME_UNSET; - if (timelineHasDuration && !newTimelineHasDuration) { - // Suppress source info changes that would make the duration unknown when it is already known. - return; + public EventListenerWrapper(EventListener eventListener) { + this.eventListener = Assertions.checkNotNull(eventListener); } - timeline = newTimeline; - timelineHasDuration = newTimelineHasDuration; - sourceListener.onSourceInfoRefreshed(timeline, null); - } + @Override + public void onLoadError( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + eventListener.onLoadError(error); + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ForwardingTimeline.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ForwardingTimeline.java new file mode 100644 index 000000000000..ce985708d03c --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ForwardingTimeline.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; + +/** + * An overridable {@link Timeline} implementation forwarding all methods to another timeline. + */ +public abstract class ForwardingTimeline extends Timeline { + + protected final Timeline timeline; + + public ForwardingTimeline(Timeline timeline) { + this.timeline = timeline; + } + + @Override + public int getWindowCount() { + return timeline.getWindowCount(); + } + + @Override + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { + return timeline.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); + } + + @Override + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { + return timeline.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); + } + + @Override + public int getLastWindowIndex(boolean shuffleModeEnabled) { + return timeline.getLastWindowIndex(shuffleModeEnabled); + } + + @Override + public int getFirstWindowIndex(boolean shuffleModeEnabled) { + return timeline.getFirstWindowIndex(shuffleModeEnabled); + } + + @Override + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + return timeline.getWindow(windowIndex, window, defaultPositionProjectionUs); + } + + @Override + public int getPeriodCount() { + return timeline.getPeriodCount(); + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + return timeline.getPeriod(periodIndex, period, setIds); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return timeline.getIndexOfPeriod(uid); + } + + @Override + public Object getUidOfPeriod(int periodIndex) { + return timeline.getUidOfPeriod(periodIndex); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/IcyDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/IcyDataSource.java new file mode 100644 index 000000000000..b35525743aca --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/IcyDataSource.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +import android.net.Uri; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * Splits ICY stream metadata out from a stream. + * + *

Note: {@link #open(DataSpec)} and {@link #close()} are not supported. This implementation is + * intended to wrap upstream {@link DataSource} instances that are opened and closed directly. + */ +/* package */ final class IcyDataSource implements DataSource { + + public interface Listener { + + /** + * Called when ICY stream metadata has been split from the stream. + * + * @param metadata The stream metadata in binary form. + */ + void onIcyMetadata(ParsableByteArray metadata); + } + + private final DataSource upstream; + private final int metadataIntervalBytes; + private final Listener listener; + private final byte[] metadataLengthByteHolder; + private int bytesUntilMetadata; + + /** + * @param upstream The upstream {@link DataSource}. + * @param metadataIntervalBytes The interval between ICY stream metadata, in bytes. + * @param listener A listener to which stream metadata is delivered. + */ + public IcyDataSource(DataSource upstream, int metadataIntervalBytes, Listener listener) { + Assertions.checkArgument(metadataIntervalBytes > 0); + this.upstream = upstream; + this.metadataIntervalBytes = metadataIntervalBytes; + this.listener = listener; + metadataLengthByteHolder = new byte[1]; + bytesUntilMetadata = metadataIntervalBytes; + } + + @Override + public void addTransferListener(TransferListener transferListener) { + upstream.addTransferListener(transferListener); + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + if (bytesUntilMetadata == 0) { + if (readMetadata()) { + bytesUntilMetadata = metadataIntervalBytes; + } else { + return C.RESULT_END_OF_INPUT; + } + } + int bytesRead = upstream.read(buffer, offset, Math.min(bytesUntilMetadata, readLength)); + if (bytesRead != C.RESULT_END_OF_INPUT) { + bytesUntilMetadata -= bytesRead; + } + return bytesRead; + } + + @Nullable + @Override + public Uri getUri() { + return upstream.getUri(); + } + + @Override + public Map> getResponseHeaders() { + return upstream.getResponseHeaders(); + } + + @Override + public void close() throws IOException { + throw new UnsupportedOperationException(); + } + + /** + * Reads an ICY stream metadata block, passing it to {@link #listener} unless the block is empty. + * + * @return True if the block was extracted, including if its length byte indicated a length of + * zero. False if the end of the stream was reached. + * @throws IOException If an error occurs reading from the wrapped {@link DataSource}. + */ + private boolean readMetadata() throws IOException { + int bytesRead = upstream.read(metadataLengthByteHolder, 0, 1); + if (bytesRead == C.RESULT_END_OF_INPUT) { + return false; + } + int metadataLength = (metadataLengthByteHolder[0] & 0xFF) << 4; + if (metadataLength == 0) { + return true; + } + + int offset = 0; + int lengthRemaining = metadataLength; + byte[] metadata = new byte[metadataLength]; + while (lengthRemaining > 0) { + bytesRead = upstream.read(metadata, offset, lengthRemaining); + if (bytesRead == C.RESULT_END_OF_INPUT) { + return false; + } + offset += bytesRead; + lengthRemaining -= bytesRead; + } + + // Discard trailing zero bytes. + while (metadataLength > 0 && metadata[metadataLength - 1] == 0) { + metadataLength--; + } + + if (metadataLength > 0) { + listener.onIcyMetadata(new ParsableByteArray(metadata, metadataLength)); + } + return true; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/LoopingMediaSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/LoopingMediaSource.java index dc05e4621719..880bfd6a4fa1 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -15,36 +15,34 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source; -import android.util.Log; -import android.util.Pair; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player; import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; -import java.io.IOException; +import java.util.HashMap; +import java.util.Map; /** - * Loops a {@link MediaSource}. + * Loops a {@link MediaSource} a specified number of times. + * + *

Note: To loop a {@link MediaSource} indefinitely, it is usually better to use {@link + * ExoPlayer#setRepeatMode(int)} instead of this class. */ -public final class LoopingMediaSource implements MediaSource { - - /** - * The maximum number of periods that can be exposed by the source. The value of this constant is - * large enough to cause indefinite looping in practice (the total duration of the looping source - * will be approximately five years if the duration of each period is one second). - */ - public static final int MAX_EXPOSED_PERIODS = 157680000; - - private static final String TAG = "LoopingMediaSource"; +public final class LoopingMediaSource extends CompositeMediaSource { private final MediaSource childSource; private final int loopCount; - - private int childPeriodCount; + private final Map childMediaPeriodIdToMediaPeriodId; + private final Map mediaPeriodToChildMediaPeriodId; /** - * Loops the provided source indefinitely. + * Loops the provided source indefinitely. Note that it is usually better to use + * {@link ExoPlayer#setRepeatMode(int)}. * * @param childSource The {@link MediaSource} to loop. */ @@ -56,48 +54,69 @@ public final class LoopingMediaSource implements MediaSource { * Loops the provided source a specified number of times. * * @param childSource The {@link MediaSource} to loop. - * @param loopCount The desired number of loops. Must be strictly positive. The actual number of - * loops will be capped at the maximum that can achieved without causing the number of - * periods exposed by the source to exceed {@link #MAX_EXPOSED_PERIODS}. + * @param loopCount The desired number of loops. Must be strictly positive. */ public LoopingMediaSource(MediaSource childSource, int loopCount) { Assertions.checkArgument(loopCount > 0); this.childSource = childSource; this.loopCount = loopCount; + childMediaPeriodIdToMediaPeriodId = new HashMap<>(); + mediaPeriodToChildMediaPeriodId = new HashMap<>(); } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, final Listener listener) { - childSource.prepareSource(player, false, new Listener() { - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - childPeriodCount = timeline.getPeriodCount(); - listener.onSourceInfoRefreshed(new LoopingTimeline(timeline, loopCount), manifest); - } - }); + @Nullable + public Object getTag() { + return childSource.getTag(); } @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - childSource.maybeThrowSourceInfoRefreshError(); + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + prepareChildSource(/* id= */ null, childSource); } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - return childSource.createPeriod(index % childPeriodCount, allocator, positionUs); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + if (loopCount == Integer.MAX_VALUE) { + return childSource.createPeriod(id, allocator, startPositionUs); + } + Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid); + MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid); + childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id); + MediaPeriod mediaPeriod = + childSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); + mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId); + return mediaPeriod; } @Override public void releasePeriod(MediaPeriod mediaPeriod) { childSource.releasePeriod(mediaPeriod); + MediaPeriodId childMediaPeriodId = mediaPeriodToChildMediaPeriodId.remove(mediaPeriod); + if (childMediaPeriodId != null) { + childMediaPeriodIdToMediaPeriodId.remove(childMediaPeriodId); + } } @Override - public void releaseSource() { - childSource.releaseSource(); + protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) { + Timeline loopingTimeline = + loopCount != Integer.MAX_VALUE + ? new LoopingTimeline(timeline, loopCount) + : new InfinitelyLoopingTimeline(timeline); + refreshSourceInfo(loopingTimeline); } - private static final class LoopingTimeline extends Timeline { + @Override + protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + Void id, MediaPeriodId mediaPeriodId) { + return loopCount != Integer.MAX_VALUE + ? childMediaPeriodIdToMediaPeriodId.get(mediaPeriodId) + : mediaPeriodId; + } + + private static final class LoopingTimeline extends AbstractConcatenatedTimeline { private final Timeline childTimeline; private final int childPeriodCount; @@ -105,19 +124,14 @@ public final class LoopingMediaSource implements MediaSource { private final int loopCount; public LoopingTimeline(Timeline childTimeline, int loopCount) { + super(/* isAtomic= */ false, new UnshuffledShuffleOrder(loopCount)); this.childTimeline = childTimeline; childPeriodCount = childTimeline.getPeriodCount(); childWindowCount = childTimeline.getWindowCount(); - // This is the maximum number of loops that can be performed without exceeding - // MAX_EXPOSED_PERIODS periods. - int maxLoopCount = MAX_EXPOSED_PERIODS / childPeriodCount; - if (loopCount > maxLoopCount) { - if (loopCount != Integer.MAX_VALUE) { - Log.w(TAG, "Capped loops to avoid overflow: " + loopCount + " -> " + maxLoopCount); - } - this.loopCount = maxLoopCount; - } else { - this.loopCount = loopCount; + this.loopCount = loopCount; + if (childPeriodCount > 0) { + Assertions.checkState(loopCount <= Integer.MAX_VALUE / childPeriodCount, + "LoopingMediaSource contains too many periods"); } } @@ -126,45 +140,73 @@ public final class LoopingMediaSource implements MediaSource { return childWindowCount * loopCount; } - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - childTimeline.getWindow(windowIndex % childWindowCount, window, setIds, - defaultPositionProjectionUs); - int periodIndexOffset = (windowIndex / childWindowCount) * childPeriodCount; - window.firstPeriodIndex += periodIndexOffset; - window.lastPeriodIndex += periodIndexOffset; - return window; - } - @Override public int getPeriodCount() { return childPeriodCount * loopCount; } @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - childTimeline.getPeriod(periodIndex % childPeriodCount, period, setIds); - int loopCount = (periodIndex / childPeriodCount); - period.windowIndex += loopCount * childWindowCount; - if (setIds) { - period.uid = Pair.create(loopCount, period.uid); - } - return period; + protected int getChildIndexByPeriodIndex(int periodIndex) { + return periodIndex / childPeriodCount; } @Override - public int getIndexOfPeriod(Object uid) { - if (!(uid instanceof Pair)) { + protected int getChildIndexByWindowIndex(int windowIndex) { + return windowIndex / childWindowCount; + } + + @Override + protected int getChildIndexByChildUid(Object childUid) { + if (!(childUid instanceof Integer)) { return C.INDEX_UNSET; } - Pair loopCountAndChildUid = (Pair) uid; - if (!(loopCountAndChildUid.first instanceof Integer)) { - return C.INDEX_UNSET; - } - int loopCount = (Integer) loopCountAndChildUid.first; - int periodIndexOffset = loopCount * childPeriodCount; - return childTimeline.getIndexOfPeriod(loopCountAndChildUid.second) + periodIndexOffset; + return (Integer) childUid; + } + + @Override + protected Timeline getTimelineByChildIndex(int childIndex) { + return childTimeline; + } + + @Override + protected int getFirstPeriodIndexByChildIndex(int childIndex) { + return childIndex * childPeriodCount; + } + + @Override + protected int getFirstWindowIndexByChildIndex(int childIndex) { + return childIndex * childWindowCount; + } + + @Override + protected Object getChildUidByChildIndex(int childIndex) { + return childIndex; + } + + } + + private static final class InfinitelyLoopingTimeline extends ForwardingTimeline { + + public InfinitelyLoopingTimeline(Timeline timeline) { + super(timeline); + } + + @Override + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { + int childNextWindowIndex = timeline.getNextWindowIndex(windowIndex, repeatMode, + shuffleModeEnabled); + return childNextWindowIndex == C.INDEX_UNSET ? getFirstWindowIndex(shuffleModeEnabled) + : childNextWindowIndex; + } + + @Override + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { + int childPreviousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, repeatMode, + shuffleModeEnabled); + return childPreviousWindowIndex == C.INDEX_UNSET ? getLastWindowIndex(shuffleModeEnabled) + : childPreviousWindowIndex; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaPeriod.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaPeriod.java new file mode 100644 index 000000000000..4fe7b137b65d --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaPeriod.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import java.io.IOException; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** + * Media period that wraps a media source and defers calling its {@link + * MediaSource#createPeriod(MediaPeriodId, Allocator, long)} method until {@link + * #createPeriod(MediaPeriodId)} has been called. This is useful if you need to return a media + * period immediately but the media source that should create it is not yet prepared. + */ +public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callback { + + /** Listener for preparation errors. */ + public interface PrepareErrorListener { + + /** + * Called the first time an error occurs while refreshing source info or preparing the period. + */ + void onPrepareError(MediaPeriodId mediaPeriodId, IOException exception); + } + + /** The {@link MediaSource} which will create the actual media period. */ + public final MediaSource mediaSource; + /** The {@link MediaPeriodId} used to create the masking media period. */ + public final MediaPeriodId id; + + private final Allocator allocator; + + @Nullable private MediaPeriod mediaPeriod; + @Nullable private Callback callback; + private long preparePositionUs; + @Nullable private PrepareErrorListener listener; + private boolean notifiedPrepareError; + private long preparePositionOverrideUs; + + /** + * Creates a new masking media period. + * + * @param mediaSource The media source to wrap. + * @param id The identifier used to create the masking media period. + * @param allocator The allocator used to create the media period. + * @param preparePositionUs The expected start position, in microseconds. + */ + public MaskingMediaPeriod( + MediaSource mediaSource, MediaPeriodId id, Allocator allocator, long preparePositionUs) { + this.id = id; + this.allocator = allocator; + this.mediaSource = mediaSource; + this.preparePositionUs = preparePositionUs; + preparePositionOverrideUs = C.TIME_UNSET; + } + + /** + * Sets a listener for preparation errors. + * + * @param listener An listener to be notified of media period preparation errors. If a listener is + * set, {@link #maybeThrowPrepareError()} will not throw but will instead pass the first + * preparation error (if any) to the listener. + */ + public void setPrepareErrorListener(PrepareErrorListener listener) { + this.listener = listener; + } + + /** Returns the position at which the masking media period was prepared, in microseconds. */ + public long getPreparePositionUs() { + return preparePositionUs; + } + + /** + * Overrides the default prepare position at which to prepare the media period. This value is only + * used if called before {@link #createPeriod(MediaPeriodId)}. + * + * @param preparePositionUs The default prepare position to use, in microseconds. + */ + public void overridePreparePositionUs(long preparePositionUs) { + preparePositionOverrideUs = preparePositionUs; + } + + /** + * Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator, long)} on the wrapped source + * then prepares it if {@link #prepare(Callback, long)} has been called. Call {@link + * #releasePeriod()} to release the period. + * + * @param id The identifier that should be used to create the media period from the media source. + */ + public void createPeriod(MediaPeriodId id) { + long preparePositionUs = getPreparePositionWithOverride(this.preparePositionUs); + mediaPeriod = mediaSource.createPeriod(id, allocator, preparePositionUs); + if (callback != null) { + mediaPeriod.prepare(this, preparePositionUs); + } + } + + /** + * Releases the period. + */ + public void releasePeriod() { + if (mediaPeriod != null) { + mediaSource.releasePeriod(mediaPeriod); + } + } + + @Override + public void prepare(Callback callback, long preparePositionUs) { + this.callback = callback; + if (mediaPeriod != null) { + mediaPeriod.prepare(this, getPreparePositionWithOverride(this.preparePositionUs)); + } + } + + @Override + public void maybeThrowPrepareError() throws IOException { + try { + if (mediaPeriod != null) { + mediaPeriod.maybeThrowPrepareError(); + } else { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + } catch (final IOException e) { + if (listener == null) { + throw e; + } + if (!notifiedPrepareError) { + notifiedPrepareError = true; + listener.onPrepareError(id, e); + } + } + } + + @Override + public TrackGroupArray getTrackGroups() { + return castNonNull(mediaPeriod).getTrackGroups(); + } + + @Override + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { + if (preparePositionOverrideUs != C.TIME_UNSET && positionUs == preparePositionUs) { + positionUs = preparePositionOverrideUs; + preparePositionOverrideUs = C.TIME_UNSET; + } + return castNonNull(mediaPeriod) + .selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs); + } + + @Override + public void discardBuffer(long positionUs, boolean toKeyframe) { + castNonNull(mediaPeriod).discardBuffer(positionUs, toKeyframe); + } + + @Override + public long readDiscontinuity() { + return castNonNull(mediaPeriod).readDiscontinuity(); + } + + @Override + public long getBufferedPositionUs() { + return castNonNull(mediaPeriod).getBufferedPositionUs(); + } + + @Override + public long seekToUs(long positionUs) { + return castNonNull(mediaPeriod).seekToUs(positionUs); + } + + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return castNonNull(mediaPeriod).getAdjustedSeekPositionUs(positionUs, seekParameters); + } + + @Override + public long getNextLoadPositionUs() { + return castNonNull(mediaPeriod).getNextLoadPositionUs(); + } + + @Override + public void reevaluateBuffer(long positionUs) { + castNonNull(mediaPeriod).reevaluateBuffer(positionUs); + } + + @Override + public boolean continueLoading(long positionUs) { + return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); + } + + @Override + public boolean isLoading() { + return mediaPeriod != null && mediaPeriod.isLoading(); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + castNonNull(callback).onContinueLoadingRequested(this); + } + + // MediaPeriod.Callback implementation + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + castNonNull(callback).onPrepared(this); + } + + private long getPreparePositionWithOverride(long preparePositionUs) { + return preparePositionOverrideUs != C.TIME_UNSET + ? preparePositionOverrideUs + : preparePositionUs; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaSource.java new file mode 100644 index 000000000000..8c867a8c26c5 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaSource.java @@ -0,0 +1,353 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +import android.util.Pair; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline.Window; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * A {@link MediaSource} that masks the {@link Timeline} with a placeholder until the actual media + * structure is known. + */ +public final class MaskingMediaSource extends CompositeMediaSource { + + private final MediaSource mediaSource; + private final boolean useLazyPreparation; + private final Timeline.Window window; + private final Timeline.Period period; + + private MaskingTimeline timeline; + @Nullable private MaskingMediaPeriod unpreparedMaskingMediaPeriod; + @Nullable private EventDispatcher unpreparedMaskingMediaPeriodEventDispatcher; + private boolean hasStartedPreparing; + private boolean isPrepared; + + /** + * Creates the masking media source. + * + * @param mediaSource A {@link MediaSource}. + * @param useLazyPreparation Whether the {@code mediaSource} is prepared lazily. If false, all + * manifest loads and other initial preparation steps happen immediately. If true, these + * initial preparations are triggered only when the player starts buffering the media. + */ + public MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation) { + this.mediaSource = mediaSource; + this.useLazyPreparation = useLazyPreparation; + window = new Timeline.Window(); + period = new Timeline.Period(); + timeline = MaskingTimeline.createWithDummyTimeline(mediaSource.getTag()); + } + + /** Returns the {@link Timeline}. */ + public Timeline getTimeline() { + return timeline; + } + + @Override + public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + if (!useLazyPreparation) { + hasStartedPreparing = true; + prepareChildSource(/* id= */ null, mediaSource); + } + } + + @Nullable + @Override + public Object getTag() { + return mediaSource.getTag(); + } + + @Override + @SuppressWarnings("MissingSuperCall") + public void maybeThrowSourceInfoRefreshError() throws IOException { + // Do nothing. Source info refresh errors will be thrown when calling + // MaskingMediaPeriod.maybeThrowPrepareError. + } + + @Override + public MaskingMediaPeriod createPeriod( + MediaPeriodId id, Allocator allocator, long startPositionUs) { + MaskingMediaPeriod mediaPeriod = + new MaskingMediaPeriod(mediaSource, id, allocator, startPositionUs); + if (isPrepared) { + MediaPeriodId idInSource = id.copyWithPeriodUid(getInternalPeriodUid(id.periodUid)); + mediaPeriod.createPeriod(idInSource); + } else { + // We should have at most one media period while source is unprepared because the duration is + // unset and we don't load beyond periods with unset duration. We need to figure out how to + // handle the prepare positions of multiple deferred media periods, should that ever change. + unpreparedMaskingMediaPeriod = mediaPeriod; + unpreparedMaskingMediaPeriodEventDispatcher = + createEventDispatcher(/* windowIndex= */ 0, id, /* mediaTimeOffsetMs= */ 0); + unpreparedMaskingMediaPeriodEventDispatcher.mediaPeriodCreated(); + if (!hasStartedPreparing) { + hasStartedPreparing = true; + prepareChildSource(/* id= */ null, mediaSource); + } + } + return mediaPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + ((MaskingMediaPeriod) mediaPeriod).releasePeriod(); + if (mediaPeriod == unpreparedMaskingMediaPeriod) { + Assertions.checkNotNull(unpreparedMaskingMediaPeriodEventDispatcher).mediaPeriodReleased(); + unpreparedMaskingMediaPeriodEventDispatcher = null; + unpreparedMaskingMediaPeriod = null; + } + } + + @Override + public void releaseSourceInternal() { + isPrepared = false; + hasStartedPreparing = false; + super.releaseSourceInternal(); + } + + @Override + protected void onChildSourceInfoRefreshed( + Void id, MediaSource mediaSource, Timeline newTimeline) { + if (isPrepared) { + timeline = timeline.cloneWithUpdatedTimeline(newTimeline); + } else if (newTimeline.isEmpty()) { + timeline = + MaskingTimeline.createWithRealTimeline( + newTimeline, Window.SINGLE_WINDOW_UID, MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID); + } else { + // Determine first period and the start position. + // This will be: + // 1. The default window start position if no deferred period has been created yet. + // 2. The non-zero prepare position of the deferred period under the assumption that this is + // a non-zero initial seek position in the window. + // 3. The default window start position if the deferred period has a prepare position of zero + // under the assumption that the prepare position of zero was used because it's the + // default position of the DummyTimeline window. Note that this will override an + // intentional seek to zero for a window with a non-zero default position. This is + // unlikely to be a problem as a non-zero default position usually only occurs for live + // playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions + // anyway. + newTimeline.getWindow(/* windowIndex= */ 0, window); + long windowStartPositionUs = window.getDefaultPositionUs(); + if (unpreparedMaskingMediaPeriod != null) { + long periodPreparePositionUs = unpreparedMaskingMediaPeriod.getPreparePositionUs(); + if (periodPreparePositionUs != 0) { + windowStartPositionUs = periodPreparePositionUs; + } + } + Object windowUid = window.uid; + Pair periodPosition = + newTimeline.getPeriodPosition( + window, period, /* windowIndex= */ 0, windowStartPositionUs); + Object periodUid = periodPosition.first; + long periodPositionUs = periodPosition.second; + timeline = MaskingTimeline.createWithRealTimeline(newTimeline, windowUid, periodUid); + if (unpreparedMaskingMediaPeriod != null) { + MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod; + maskingPeriod.overridePreparePositionUs(periodPositionUs); + MediaPeriodId idInSource = + maskingPeriod.id.copyWithPeriodUid(getInternalPeriodUid(maskingPeriod.id.periodUid)); + maskingPeriod.createPeriod(idInSource); + } + } + isPrepared = true; + refreshSourceInfo(this.timeline); + } + + @Nullable + @Override + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + Void id, MediaPeriodId mediaPeriodId) { + return mediaPeriodId.copyWithPeriodUid(getExternalPeriodUid(mediaPeriodId.periodUid)); + } + + @Override + protected boolean shouldDispatchCreateOrReleaseEvent(MediaPeriodId mediaPeriodId) { + // Suppress create and release events for the period created while the source was still + // unprepared, as we send these events from this class. + return unpreparedMaskingMediaPeriod == null + || !mediaPeriodId.equals(unpreparedMaskingMediaPeriod.id); + } + + private Object getInternalPeriodUid(Object externalPeriodUid) { + return externalPeriodUid.equals(MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID) + ? timeline.replacedInternalPeriodUid + : externalPeriodUid; + } + + private Object getExternalPeriodUid(Object internalPeriodUid) { + return timeline.replacedInternalPeriodUid.equals(internalPeriodUid) + ? MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID + : internalPeriodUid; + } + + /** + * Timeline used as placeholder for an unprepared media source. After preparation, a + * MaskingTimeline is used to keep the originally assigned dummy period ID. + */ + private static final class MaskingTimeline extends ForwardingTimeline { + + public static final Object DUMMY_EXTERNAL_PERIOD_UID = new Object(); + + private final Object replacedInternalWindowUid; + private final Object replacedInternalPeriodUid; + + /** + * Returns an instance with a dummy timeline using the provided window tag. + * + * @param windowTag A window tag. + */ + public static MaskingTimeline createWithDummyTimeline(@Nullable Object windowTag) { + return new MaskingTimeline( + new DummyTimeline(windowTag), Window.SINGLE_WINDOW_UID, DUMMY_EXTERNAL_PERIOD_UID); + } + + /** + * Returns an instance with a real timeline, replacing the provided period ID with the already + * assigned dummy period ID. + * + * @param timeline The real timeline. + * @param firstWindowUid The window UID in the timeline which will be replaced by the already + * assigned {@link Window#SINGLE_WINDOW_UID}. + * @param firstPeriodUid The period UID in the timeline which will be replaced by the already + * assigned {@link #DUMMY_EXTERNAL_PERIOD_UID}. + */ + public static MaskingTimeline createWithRealTimeline( + Timeline timeline, Object firstWindowUid, Object firstPeriodUid) { + return new MaskingTimeline(timeline, firstWindowUid, firstPeriodUid); + } + + private MaskingTimeline( + Timeline timeline, Object replacedInternalWindowUid, Object replacedInternalPeriodUid) { + super(timeline); + this.replacedInternalWindowUid = replacedInternalWindowUid; + this.replacedInternalPeriodUid = replacedInternalPeriodUid; + } + + /** + * Returns a copy with an updated timeline. This keeps the existing period replacement. + * + * @param timeline The new timeline. + */ + public MaskingTimeline cloneWithUpdatedTimeline(Timeline timeline) { + return new MaskingTimeline(timeline, replacedInternalWindowUid, replacedInternalPeriodUid); + } + + /** Returns the wrapped timeline. */ + public Timeline getTimeline() { + return timeline; + } + + @Override + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + timeline.getWindow(windowIndex, window, defaultPositionProjectionUs); + if (Util.areEqual(window.uid, replacedInternalWindowUid)) { + window.uid = Window.SINGLE_WINDOW_UID; + } + return window; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + timeline.getPeriod(periodIndex, period, setIds); + if (Util.areEqual(period.uid, replacedInternalPeriodUid)) { + period.uid = DUMMY_EXTERNAL_PERIOD_UID; + } + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + return timeline.getIndexOfPeriod( + DUMMY_EXTERNAL_PERIOD_UID.equals(uid) ? replacedInternalPeriodUid : uid); + } + + @Override + public Object getUidOfPeriod(int periodIndex) { + Object uid = timeline.getUidOfPeriod(periodIndex); + return Util.areEqual(uid, replacedInternalPeriodUid) ? DUMMY_EXTERNAL_PERIOD_UID : uid; + } + } + + /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ + private static final class DummyTimeline extends Timeline { + + @Nullable private final Object tag; + + public DummyTimeline(@Nullable Object tag) { + this.tag = tag; + } + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + return window.set( + Window.SINGLE_WINDOW_UID, + tag, + /* manifest= */ null, + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + /* isSeekable= */ false, + // Dynamic window to indicate pending timeline updates. + /* isDynamic= */ true, + /* isLive= */ false, + /* defaultPositionUs= */ 0, + /* durationUs= */ C.TIME_UNSET, + /* firstPeriodIndex= */ 0, + /* lastPeriodIndex= */ 0, + /* positionInFirstPeriodUs= */ 0); + } + + @Override + public int getPeriodCount() { + return 1; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + return period.set( + /* id= */ 0, + /* uid= */ MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID, + /* windowIndex= */ 0, + /* durationUs = */ C.TIME_UNSET, + /* positionInWindowUs= */ 0); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return uid == MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID ? 0 : C.INDEX_UNSET; + } + + @Override + public Object getUidOfPeriod(int periodIndex) { + return MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaPeriod.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaPeriod.java index 402aa6a5939d..3effcec90406 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaPeriod.java @@ -16,12 +16,21 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.source; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters; import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; import java.io.IOException; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** - * A source of a single period of media. + * Loads media corresponding to a {@link Timeline.Period}, and allows that media to be read. All + * methods are called on the player's internal playback thread, as described in the + * {@link ExoPlayer} Javadoc. */ public interface MediaPeriod extends SequenceableLoader { @@ -32,37 +41,37 @@ public interface MediaPeriod extends SequenceableLoader { /** * Called when preparation completes. - *

- * Called on the playback thread. After invoking this method, the {@link MediaPeriod} can expect - * for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[], long)} to be - * called with the initial track selection. + * + *

Called on the playback thread. After invoking this method, the {@link MediaPeriod} can + * expect for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[], + * long)} to be called with the initial track selection. * * @param mediaPeriod The prepared {@link MediaPeriod}. */ void onPrepared(MediaPeriod mediaPeriod); - } /** * Prepares this media period asynchronously. - *

- * {@code callback.onPrepared} is called when preparation completes. If preparation fails, + * + *

{@code callback.onPrepared} is called when preparation completes. If preparation fails, * {@link #maybeThrowPrepareError()} will throw an {@link IOException}. - *

- * If preparation succeeds and results in a source timeline change (e.g. the period duration - * becoming known), {@link MediaSource.Listener#onSourceInfoRefreshed(Timeline, Object)} will be + * + *

If preparation succeeds and results in a source timeline change (e.g. the period duration + * becoming known), {@link MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} will be * called before {@code callback.onPrepared}. * * @param callback Callback to receive updates from this period, including being notified when * preparation completes. + * @param positionUs The expected starting position, in microseconds. */ - void prepare(Callback callback); + void prepare(Callback callback, long positionUs); /** * Throws an error that's preventing the period from becoming prepared. Does nothing if no such * error exists. - *

- * This method should only be called before the period has completed preparation. + * + *

This method is only called before the period has completed preparation. * * @throws IOException The underlying error. */ @@ -70,87 +79,134 @@ public interface MediaPeriod extends SequenceableLoader { /** * Returns the {@link TrackGroup}s exposed by the period. - *

- * This method should only be called after the period has been prepared. + * + *

This method is only called after the period has been prepared. * * @return The {@link TrackGroup}s. */ TrackGroupArray getTrackGroups(); + /** + * Returns a list of {@link StreamKey StreamKeys} which allow to filter the media in this period + * to load only the parts needed to play the provided {@link TrackSelection TrackSelections}. + * + *

This method is only called after the period has been prepared. + * + * @param trackSelections The {@link TrackSelection TrackSelections} describing the tracks for + * which stream keys are requested. + * @return The corresponding {@link StreamKey StreamKeys} for the selected tracks, or an empty + * list if filtering is not possible and the entire media needs to be loaded to play the + * selected tracks. + */ + default List getStreamKeys(List trackSelections) { + return Collections.emptyList(); + } + /** * Performs a track selection. - *

- * The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags} - * indicating whether the existing {@code SampleStream} can be retained for each selection, and + * + *

The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags} + * indicating whether the existing {@link SampleStream} can be retained for each selection, and * the existing {@code stream}s themselves. The call will update {@code streams} to reflect the * provided selections, clearing, setting and replacing entries as required. If an existing sample * stream is retained but with the requirement that the consuming renderer be reset, then the * corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set * if a new sample stream is created. - *

- * This method should only be called after the period has been prepared. + * + *

Note that previously passed {@link TrackSelection TrackSelections} are no longer valid, and + * any references to them must be updated to point to the new selections. + * + *

This method is only called after the period has been prepared. * * @param selections The renderer track selections. * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained - * for each selection. A {@code true} value indicates that the selection is unchanged, and - * that the caller does not require that the sample stream be recreated. + * for each track selection. A {@code true} value indicates that the selection is equivalent + * to the one that was previously passed, and that the caller does not require that the sample + * stream be recreated. If a retained sample stream holds any references to the track + * selection then they must be updated to point to the new selection. * @param streams The existing sample streams, which will be updated to reflect the provided * selections. * @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that * have been retained but with the requirement that the consuming renderer be reset. - * @param positionUs The current playback position in microseconds. + * @param positionUs The current playback position in microseconds. If playback of this period has + * not yet started, the value will be the starting position. * @return The actual position at which the tracks were enabled, in microseconds. */ - long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs); + long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs); /** * Discards buffered media up to the specified position. * + *

This method is only called after the period has been prepared. + * * @param positionUs The position in microseconds. + * @param toKeyframe If true then for each track discards samples up to the keyframe before or at + * the specified position, rather than any sample before or at that position. */ - void discardBuffer(long positionUs); + void discardBuffer(long positionUs, boolean toKeyframe); /** * Attempts to read a discontinuity. - *

- * After this method has returned a value other than {@link C#TIME_UNSET}, all - * {@link SampleStream}s provided by the period are guaranteed to start from a key frame. + * + *

After this method has returned a value other than {@link C#TIME_UNSET}, all {@link + * SampleStream}s provided by the period are guaranteed to start from a key frame. + * + *

This method is only called after the period has been prepared and before reading from any + * {@link SampleStream}s provided by the period. * * @return If a discontinuity was read then the playback position in microseconds after the * discontinuity. Else {@link C#TIME_UNSET}. */ long readDiscontinuity(); - /** - * Returns an estimate of the position up to which data is buffered for the enabled tracks. - *

- * This method should only be called when at least one track is selected. - * - * @return An estimate of the absolute position in microseconds up to which data is buffered, or - * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered. - */ - long getBufferedPositionUs(); - /** * Attempts to seek to the specified position in microseconds. - *

- * After this method has been called, all {@link SampleStream}s provided by the period are + * + *

After this method has been called, all {@link SampleStream}s provided by the period are * guaranteed to start from a key frame. - *

- * This method should only be called when at least one track is selected. + * + *

This method is only called when at least one track is selected. * * @param positionUs The seek position in microseconds. * @return The actual position to which the period was seeked, in microseconds. */ long seekToUs(long positionUs); + /** + * Returns the position to which a seek will be performed, given the specified seek position and + * {@link SeekParameters}. + * + *

This method is only called after the period has been prepared. + * + * @param positionUs The seek position in microseconds. + * @param seekParameters Parameters that control how the seek is performed. Implementations may + * apply seek parameters on a best effort basis. + * @return The actual position to which a seek will be performed, in microseconds. + */ + long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters); + // SequenceableLoader interface. Overridden to provide more specific documentation. + /** + * Returns an estimate of the position up to which data is buffered for the enabled tracks. + * + *

This method is only called when at least one track is selected. + * + * @return An estimate of the absolute position in microseconds up to which data is buffered, or + * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered. + */ + @Override + long getBufferedPositionUs(); + /** * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished. - *

- * This method should only be called after the period has been prepared. It may be called when no + * + *

This method is only called after the period has been prepared. It may be called when no * tracks are selected. */ @Override @@ -158,19 +214,38 @@ public interface MediaPeriod extends SequenceableLoader { /** * Attempts to continue loading. - *

- * This method may be called both during and after the period has been prepared. - *

- * A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the - * {@link Callback} passed to {@link #prepare(Callback)} to request that this method be called - * when the period is permitted to continue loading data. A period may do this both during and - * after preparation. * - * @param positionUs The current playback position. - * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return - * a different value than prior to the call. False otherwise. + *

This method may be called both during and after the period has been prepared. + * + *

A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the + * {@link Callback} passed to {@link #prepare(Callback, long)} to request that this method be + * called when the period is permitted to continue loading data. A period may do this both during + * and after preparation. + * + * @param positionUs The current playback position in microseconds. If playback of this period has + * not yet started, the value will be the starting position in this period minus the duration + * of any media in previous periods still to be played. + * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return a + * different value than prior to the call. False otherwise. */ @Override boolean continueLoading(long positionUs); + /** Returns whether the media period is currently loading. */ + boolean isLoading(); + + /** + * Re-evaluates the buffer given the playback position. + * + *

This method is only called after the period has been prepared. + * + *

A period may choose to discard buffered media so that it can be re-buffered in a different + * quality. + * + * @param positionUs The current playback position in microseconds. If playback of this period has + * not yet started, the value will be the starting position in this period minus the duration + * of any media in previous periods still to be played. + */ + @Override + void reevaluateBuffer(long positionUs); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSource.java index 61ee0a800c3b..7e757d5ade74 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSource.java @@ -15,73 +15,311 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source; -import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer; +import android.os.Handler; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; import java.io.IOException; /** - * A source of media consisting of one or more {@link MediaPeriod}s. + * Defines and provides media to be played by an {@link org.mozilla.thirdparty.com.google.android.exoplayer2ExoPlayer}. A + * MediaSource has two main responsibilities: + * + *

    + *
  • To provide the player with a {@link Timeline} defining the structure of its media, and to + * provide a new timeline whenever the structure of the media changes. The MediaSource + * provides these timelines by calling {@link MediaSourceCaller#onSourceInfoRefreshed} on the + * {@link MediaSourceCaller}s passed to {@link #prepareSource(MediaSourceCaller, + * TransferListener)}. + *
  • To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are + * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator, long)}, and provide a + * way for the player to load and read the media. + *
+ * + * All methods are called on the player's internal playback thread, as described in the {@link + * com.google.android.exoplayer2.ExoPlayer} Javadoc. They should not be called directly from + * application code. Instances can be re-used, but only for one {@link + * com.google.android.exoplayer2.ExoPlayer} instance simultaneously. */ public interface MediaSource { - /** - * Listener for source events. - */ - interface Listener { + /** A caller of media sources, which will be notified of source events. */ + interface MediaSourceCaller { /** - * Called when manifest and/or timeline has been refreshed. + * Called when the {@link Timeline} has been refreshed. * + *

Called on the playback thread. + * + * @param source The {@link MediaSource} whose info has been refreshed. * @param timeline The source's timeline. - * @param manifest The loaded manifest. */ - void onSourceInfoRefreshed(Timeline timeline, Object manifest); + void onSourceInfoRefreshed(MediaSource source, Timeline timeline); + } + /** Identifier for a {@link MediaPeriod}. */ + final class MediaPeriodId { + + /** The unique id of the timeline period. */ + public final Object periodUid; + + /** + * If the media period is in an ad group, the index of the ad group in the period. + * {@link C#INDEX_UNSET} otherwise. + */ + public final int adGroupIndex; + + /** + * If the media period is in an ad group, the index of the ad in its ad group in the period. + * {@link C#INDEX_UNSET} otherwise. + */ + public final int adIndexInAdGroup; + + /** + * The sequence number of the window in the buffered sequence of windows this media period is + * part of. {@link C#INDEX_UNSET} if the media period id is not part of a buffered sequence of + * windows. + */ + public final long windowSequenceNumber; + + /** + * The index of the next ad group to which the media period's content is clipped, or {@link + * C#INDEX_UNSET} if there is no following ad group or if this media period is an ad. + */ + public final int nextAdGroupIndex; + + /** + * Creates a media period identifier for a dummy period which is not part of a buffered sequence + * of windows. + * + * @param periodUid The unique id of the timeline period. + */ + public MediaPeriodId(Object periodUid) { + this(periodUid, /* windowSequenceNumber= */ C.INDEX_UNSET); + } + + /** + * Creates a media period identifier for the specified period in the timeline. + * + * @param periodUid The unique id of the timeline period. + * @param windowSequenceNumber The sequence number of the window in the buffered sequence of + * windows this media period is part of. + */ + public MediaPeriodId(Object periodUid, long windowSequenceNumber) { + this( + periodUid, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET, + windowSequenceNumber, + /* nextAdGroupIndex= */ C.INDEX_UNSET); + } + + /** + * Creates a media period identifier for the specified clipped period in the timeline. + * + * @param periodUid The unique id of the timeline period. + * @param windowSequenceNumber The sequence number of the window in the buffered sequence of + * windows this media period is part of. + * @param nextAdGroupIndex The index of the next ad group to which the media period's content is + * clipped. + */ + public MediaPeriodId(Object periodUid, long windowSequenceNumber, int nextAdGroupIndex) { + this( + periodUid, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET, + windowSequenceNumber, + nextAdGroupIndex); + } + + /** + * Creates a media period identifier that identifies an ad within an ad group at the specified + * timeline period. + * + * @param periodUid The unique id of the timeline period that contains the ad group. + * @param adGroupIndex The index of the ad group. + * @param adIndexInAdGroup The index of the ad in the ad group. + * @param windowSequenceNumber The sequence number of the window in the buffered sequence of + * windows this media period is part of. + */ + public MediaPeriodId( + Object periodUid, int adGroupIndex, int adIndexInAdGroup, long windowSequenceNumber) { + this( + periodUid, + adGroupIndex, + adIndexInAdGroup, + windowSequenceNumber, + /* nextAdGroupIndex= */ C.INDEX_UNSET); + } + + private MediaPeriodId( + Object periodUid, + int adGroupIndex, + int adIndexInAdGroup, + long windowSequenceNumber, + int nextAdGroupIndex) { + this.periodUid = periodUid; + this.adGroupIndex = adGroupIndex; + this.adIndexInAdGroup = adIndexInAdGroup; + this.windowSequenceNumber = windowSequenceNumber; + this.nextAdGroupIndex = nextAdGroupIndex; + } + + /** Returns a copy of this period identifier but with {@code newPeriodUid} as its period uid. */ + public MediaPeriodId copyWithPeriodUid(Object newPeriodUid) { + return periodUid.equals(newPeriodUid) + ? this + : new MediaPeriodId( + newPeriodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, nextAdGroupIndex); + } + + /** + * Returns whether this period identifier identifies an ad in an ad group in a period. + */ + public boolean isAd() { + return adGroupIndex != C.INDEX_UNSET; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + MediaPeriodId periodId = (MediaPeriodId) obj; + return periodUid.equals(periodId.periodUid) + && adGroupIndex == periodId.adGroupIndex + && adIndexInAdGroup == periodId.adIndexInAdGroup + && windowSequenceNumber == periodId.windowSequenceNumber + && nextAdGroupIndex == periodId.nextAdGroupIndex; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + periodUid.hashCode(); + result = 31 * result + adGroupIndex; + result = 31 * result + adIndexInAdGroup; + result = 31 * result + (int) windowSequenceNumber; + result = 31 * result + nextAdGroupIndex; + return result; + } } /** - * Starts preparation of the source. + * Adds a {@link MediaSourceEventListener} to the list of listeners which are notified of media + * source events. * - * @param player The player for which this source is being prepared. - * @param isTopLevelSource Whether this source has been passed directly to - * {@link ExoPlayer#prepare(MediaSource)} or - * {@link ExoPlayer#prepare(MediaSource, boolean, boolean)}. If {@code false}, this source is - * being prepared by another source (e.g. {@link ConcatenatingMediaSource}) for composition. - * @param listener The listener for source events. + * @param handler A handler on the which listener events will be posted. + * @param eventListener The listener to be added. */ - void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener); + void addEventListener(Handler handler, MediaSourceEventListener eventListener); + + /** + * Removes a {@link MediaSourceEventListener} from the list of listeners which are notified of + * media source events. + * + * @param eventListener The listener to be removed. + */ + void removeEventListener(MediaSourceEventListener eventListener); + + /** Returns the tag set on the media source, or null if none was set. */ + @Nullable + default Object getTag() { + return null; + } + + /** + * Registers a {@link MediaSourceCaller}. Starts source preparation if needed and enables the + * source for the creation of {@link MediaPeriod MediaPerods}. + * + *

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

{@link MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} will be called once + * the source has a {@link Timeline}. + * + *

For each call to this method, a call to {@link #releaseSource(MediaSourceCaller)} is needed + * to remove the caller and to release the source if no longer required. + * + * @param caller The {@link MediaSourceCaller} to be registered. + * @param mediaTransferListener The transfer listener which should be informed of any media data + * transfers. May be null if no listener is available. Note that this listener should be only + * informed of transfers related to the media loads and not of auxiliary loads for manifests + * and other data. + */ + void prepareSource(MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener); /** * Throws any pending error encountered while loading or refreshing source information. + * + *

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

Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}. */ void maybeThrowSourceInfoRefreshError() throws IOException; /** - * Returns a new {@link MediaPeriod} corresponding to the period at the specified {@code index}. - * This method may be called multiple times with the same index without an intervening call to - * {@link #releasePeriod(MediaPeriod)}. + * Enables the source for the creation of {@link MediaPeriod MediaPeriods}. * - * @param index The index of the period. + *

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

Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}. + * + * @param caller The {@link MediaSourceCaller} enabling the source. + */ + void enable(MediaSourceCaller caller); + + /** + * Returns a new {@link MediaPeriod} identified by {@code periodId}. + * + *

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

Must only be called if the source is enabled. + * + * @param id The identifier of the period. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. - * @param positionUs The player's current playback position. + * @param startPositionUs The expected start position, in microseconds. * @return A new {@link MediaPeriod}. */ - MediaPeriod createPeriod(int index, Allocator allocator, long positionUs); + MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs); /** * Releases the period. * + *

Should not be called directly from application code. + * * @param mediaPeriod The period to release. */ void releasePeriod(MediaPeriod mediaPeriod); /** - * Releases the source. - *

- * This method should be called when the source is no longer required. It may be called in any - * state. + * Disables the source for the creation of {@link MediaPeriod MediaPeriods}. The implementation + * should not hold onto limited resources used for the creation of media periods. + * + *

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

Must only be called after all {@link MediaPeriod MediaPeriods} previously created by {@link + * #createPeriod(MediaPeriodId, Allocator, long)} have been released by {@link + * #releasePeriod(MediaPeriod)}. + * + * @param caller The {@link MediaSourceCaller} disabling the source. */ - void releaseSource(); + void disable(MediaSourceCaller caller); + /** + * Unregisters a caller, and disables and releases the source if no longer required. + * + *

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

Must only be called if all created {@link MediaPeriod MediaPeriods} have been released by + * {@link #releasePeriod(MediaPeriod)}. + * + * @param caller The {@link MediaSourceCaller} to be unregistered. + */ + void releaseSource(MediaSourceCaller caller); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceEventListener.java new file mode 100644 index 000000000000..53c50d8a264a --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -0,0 +1,740 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import androidx.annotation.CheckResult; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +/** Interface for callbacks to be notified of {@link MediaSource} events. */ +public interface MediaSourceEventListener { + + /** Media source load event information. */ + final class LoadEventInfo { + + /** Defines the requested data. */ + public final DataSpec dataSpec; + /** + * The {@link Uri} from which data is being read. The uri will be identical to the one in {@link + * #dataSpec}.uri unless redirection has occurred. If redirection has occurred, this is the uri + * after redirection. + */ + public final Uri uri; + /** The response headers associated with the load, or an empty map if unavailable. */ + public final Map> responseHeaders; + /** The value of {@link SystemClock#elapsedRealtime} at the time of the load event. */ + public final long elapsedRealtimeMs; + /** The duration of the load up to the event time. */ + public final long loadDurationMs; + /** The number of bytes that were loaded up to the event time. */ + public final long bytesLoaded; + + /** + * Creates load event info. + * + * @param dataSpec Defines the requested data. + * @param uri The {@link Uri} from which data is being read. The uri must be identical to the + * one in {@code dataSpec.uri} unless redirection has occurred. If redirection has occurred, + * this is the uri after redirection. + * @param responseHeaders The response headers associated with the load, or an empty map if + * unavailable. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} at the time of the + * load event. + * @param loadDurationMs The duration of the load up to the event time. + * @param bytesLoaded The number of bytes that were loaded up to the event time. For compressed + * network responses, this is the decompressed size. + */ + public LoadEventInfo( + DataSpec dataSpec, + Uri uri, + Map> responseHeaders, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + this.dataSpec = dataSpec; + this.uri = uri; + this.responseHeaders = responseHeaders; + this.elapsedRealtimeMs = elapsedRealtimeMs; + this.loadDurationMs = loadDurationMs; + this.bytesLoaded = bytesLoaded; + } + } + + /** Descriptor for data being loaded or selected by a media source. */ + final class MediaLoadData { + + /** One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data. */ + public final int dataType; + /** + * One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to media of a + * specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + */ + public final int trackType; + /** + * The format of the track to which the data belongs. Null if the data does not belong to a + * specific track. + */ + @Nullable public final Format trackFormat; + /** + * One of the {@link C} {@code SELECTION_REASON_*} constants if the data belongs to a track. + * {@link C#SELECTION_REASON_UNKNOWN} otherwise. + */ + public final int trackSelectionReason; + /** + * Optional data associated with the selection of the track to which the data belongs. Null if + * the data does not belong to a track. + */ + @Nullable public final Object trackSelectionData; + /** + * The start time of the media, or {@link C#TIME_UNSET} if the data does not belong to a + * specific media period. + */ + public final long mediaStartTimeMs; + /** + * The end time of the media, or {@link C#TIME_UNSET} if the data does not belong to a specific + * media period or the end time is unknown. + */ + public final long mediaEndTimeMs; + + /** + * Creates media load data. + * + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds + * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does + * not belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which + * the data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media, or {@link C#TIME_UNSET} if the data does + * not belong to a specific media period. + * @param mediaEndTimeMs The end time of the media, or {@link C#TIME_UNSET} if the data does not + * belong to a specific media period or the end time is unknown. + */ + public MediaLoadData( + int dataType, + int trackType, + @Nullable Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs) { + this.dataType = dataType; + this.trackType = trackType; + this.trackFormat = trackFormat; + this.trackSelectionReason = trackSelectionReason; + this.trackSelectionData = trackSelectionData; + this.mediaStartTimeMs = mediaStartTimeMs; + this.mediaEndTimeMs = mediaEndTimeMs; + } + } + + /** + * Called when a media period is created by the media source. + * + * @param windowIndex The window index in the timeline this media period belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} of the created media period. + */ + default void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {} + + /** + * Called when a media period is released by the media source. + * + * @param windowIndex The window index in the timeline this media period belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} of the released media period. + */ + default void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {} + + /** + * Called when a load begins. + * + * @param windowIndex The window index in the timeline of the media source this load belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not + * belong to a specific media period. + * @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The value of {@link + * LoadEventInfo#uri} won't reflect potential redirection yet and {@link + * LoadEventInfo#responseHeaders} will be empty. + * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. + */ + default void onLoadStarted( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData) {} + + /** + * Called when a load ends. + * + * @param windowIndex The window index in the timeline of the media source this load belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not + * belong to a specific media period. + * @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The values of {@link + * LoadEventInfo#elapsedRealtimeMs} and {@link LoadEventInfo#bytesLoaded} are relative to the + * corresponding {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)} + * event. + * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. + */ + default void onLoadCompleted( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData) {} + + /** + * Called when a load is canceled. + * + * @param windowIndex The window index in the timeline of the media source this load belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not + * belong to a specific media period. + * @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The values of {@link + * LoadEventInfo#elapsedRealtimeMs} and {@link LoadEventInfo#bytesLoaded} are relative to the + * corresponding {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)} + * event. + * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. + */ + default void onLoadCanceled( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData) {} + + /** + * Called when a load error occurs. + * + *

The error may or may not have resulted in the load being canceled, as indicated by the + * {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will + * not be called in addition to this method. + * + *

This method being called does not indicate that playback has failed, or that it will fail. + * The player may be able to recover from the error and continue. Hence applications should + * not implement this method to display a user visible error or initiate an application + * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement + * such behavior). This method is called to provide the application with an opportunity to log the + * error if it wishes to do so. + * + * @param windowIndex The window index in the timeline of the media source this load belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not + * belong to a specific media period. + * @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The values of {@link + * LoadEventInfo#elapsedRealtimeMs} and {@link LoadEventInfo#bytesLoaded} are relative to the + * corresponding {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)} + * event. + * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. + * @param error The load error. + * @param wasCanceled Whether the load was canceled as a result of the error. + */ + default void onLoadError( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) {} + + /** + * Called when a media period is first being read from. + * + * @param windowIndex The window index in the timeline this media period belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} of the media period being read from. + */ + default void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {} + + /** + * Called when data is removed from the back of a media buffer, typically so that it can be + * re-buffered in a different format. + * + * @param windowIndex The window index in the timeline of the media source this load belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} the media belongs to. + * @param mediaLoadData The {@link MediaLoadData} defining the media being discarded. + */ + default void onUpstreamDiscarded( + int windowIndex, MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {} + + /** + * Called when a downstream format change occurs (i.e. when the format of the media being read + * from one or more {@link SampleStream}s provided by the source changes). + * + * @param windowIndex The window index in the timeline of the media source this load belongs to. + * @param mediaPeriodId The {@link MediaPeriodId} the media belongs to. + * @param mediaLoadData The {@link MediaLoadData} defining the newly selected downstream data. + */ + default void onDownstreamFormatChanged( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {} + + /** Dispatches events to {@link MediaSourceEventListener}s. */ + final class EventDispatcher { + + /** The timeline window index reported with the events. */ + public final int windowIndex; + /** The {@link MediaPeriodId} reported with the events. */ + @Nullable public final MediaPeriodId mediaPeriodId; + + private final CopyOnWriteArrayList listenerAndHandlers; + private final long mediaTimeOffsetMs; + + /** Creates an event dispatcher. */ + public EventDispatcher() { + this( + /* listenerAndHandlers= */ new CopyOnWriteArrayList<>(), + /* windowIndex= */ 0, + /* mediaPeriodId= */ null, + /* mediaTimeOffsetMs= */ 0); + } + + private EventDispatcher( + CopyOnWriteArrayList listenerAndHandlers, + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + long mediaTimeOffsetMs) { + this.listenerAndHandlers = listenerAndHandlers; + this.windowIndex = windowIndex; + this.mediaPeriodId = mediaPeriodId; + this.mediaTimeOffsetMs = mediaTimeOffsetMs; + } + + /** + * Creates a view of the event dispatcher with pre-configured window index, media period id, and + * media time offset. + * + * @param windowIndex The timeline window index to be reported with the events. + * @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events. + * @param mediaTimeOffsetMs The offset to be added to all media times, in milliseconds. + * @return A view of the event dispatcher with the pre-configured parameters. + */ + @CheckResult + public EventDispatcher withParameters( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { + return new EventDispatcher( + listenerAndHandlers, windowIndex, mediaPeriodId, mediaTimeOffsetMs); + } + + /** + * Adds a listener to the event dispatcher. + * + * @param handler A handler on the which listener events will be posted. + * @param eventListener The listener to be added. + */ + public void addEventListener(Handler handler, MediaSourceEventListener eventListener) { + Assertions.checkArgument(handler != null && eventListener != null); + listenerAndHandlers.add(new ListenerAndHandler(handler, eventListener)); + } + + /** + * Removes a listener from the event dispatcher. + * + * @param eventListener The listener to be removed. + */ + public void removeEventListener(MediaSourceEventListener eventListener) { + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + if (listenerAndHandler.listener == eventListener) { + listenerAndHandlers.remove(listenerAndHandler); + } + } + } + + /** Dispatches {@link #onMediaPeriodCreated(int, MediaPeriodId)}. */ + public void mediaPeriodCreated() { + MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId); + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, + () -> listener.onMediaPeriodCreated(windowIndex, mediaPeriodId)); + } + } + + /** Dispatches {@link #onMediaPeriodReleased(int, MediaPeriodId)}. */ + public void mediaPeriodReleased() { + MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId); + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, + () -> listener.onMediaPeriodReleased(windowIndex, mediaPeriodId)); + } + } + + /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ + public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { + loadStarted( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs); + } + + /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ + public void loadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + @Nullable Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long mediaStartTimeUs, + long mediaEndTimeUs, + long elapsedRealtimeMs) { + loadStarted( + new LoadEventInfo( + dataSpec, + dataSpec.uri, + /* responseHeaders= */ Collections.emptyMap(), + elapsedRealtimeMs, + /* loadDurationMs= */ 0, + /* bytesLoaded= */ 0), + new MediaLoadData( + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs))); + } + + /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ + public void loadStarted(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, + () -> listener.onLoadStarted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData)); + } + } + + /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ + public void loadCompleted( + DataSpec dataSpec, + Uri uri, + Map> responseHeaders, + int dataType, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + loadCompleted( + dataSpec, + uri, + responseHeaders, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + + /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ + public void loadCompleted( + DataSpec dataSpec, + Uri uri, + Map> responseHeaders, + int dataType, + int trackType, + @Nullable Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long mediaStartTimeUs, + long mediaEndTimeUs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + loadCompleted( + new LoadEventInfo( + dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded), + new MediaLoadData( + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs))); + } + + /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ + public void loadCompleted(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, + () -> + listener.onLoadCompleted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData)); + } + } + + /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ + public void loadCanceled( + DataSpec dataSpec, + Uri uri, + Map> responseHeaders, + int dataType, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + loadCanceled( + dataSpec, + uri, + responseHeaders, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + + /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ + public void loadCanceled( + DataSpec dataSpec, + Uri uri, + Map> responseHeaders, + int dataType, + int trackType, + @Nullable Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long mediaStartTimeUs, + long mediaEndTimeUs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + loadCanceled( + new LoadEventInfo( + dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded), + new MediaLoadData( + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs))); + } + + /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ + public void loadCanceled(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, + () -> + listener.onLoadCanceled(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData)); + } + } + + /** + * Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException, + * boolean)}. + */ + public void loadError( + DataSpec dataSpec, + Uri uri, + Map> responseHeaders, + int dataType, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled) { + loadError( + dataSpec, + uri, + responseHeaders, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded, + error, + wasCanceled); + } + + /** + * Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException, + * boolean)}. + */ + public void loadError( + DataSpec dataSpec, + Uri uri, + Map> responseHeaders, + int dataType, + int trackType, + @Nullable Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long mediaStartTimeUs, + long mediaEndTimeUs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled) { + loadError( + new LoadEventInfo( + dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded), + new MediaLoadData( + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs)), + error, + wasCanceled); + } + + /** + * Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException, + * boolean)}. + */ + public void loadError( + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, + () -> + listener.onLoadError( + windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData, error, wasCanceled)); + } + } + + /** Dispatches {@link #onReadingStarted(int, MediaPeriodId)}. */ + public void readingStarted() { + MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId); + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, + () -> listener.onReadingStarted(windowIndex, mediaPeriodId)); + } + } + + /** Dispatches {@link #onUpstreamDiscarded(int, MediaPeriodId, MediaLoadData)}. */ + public void upstreamDiscarded(int trackType, long mediaStartTimeUs, long mediaEndTimeUs) { + upstreamDiscarded( + new MediaLoadData( + C.DATA_TYPE_MEDIA, + trackType, + /* trackFormat= */ null, + C.SELECTION_REASON_ADAPTIVE, + /* trackSelectionData= */ null, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs))); + } + + /** Dispatches {@link #onUpstreamDiscarded(int, MediaPeriodId, MediaLoadData)}. */ + public void upstreamDiscarded(MediaLoadData mediaLoadData) { + MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId); + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, + () -> listener.onUpstreamDiscarded(windowIndex, mediaPeriodId, mediaLoadData)); + } + } + + /** Dispatches {@link #onDownstreamFormatChanged(int, MediaPeriodId, MediaLoadData)}. */ + public void downstreamFormatChanged( + int trackType, + @Nullable Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long mediaTimeUs) { + downstreamFormatChanged( + new MediaLoadData( + C.DATA_TYPE_MEDIA, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaTimeUs), + /* mediaEndTimeMs= */ C.TIME_UNSET)); + } + + /** Dispatches {@link #onDownstreamFormatChanged(int, MediaPeriodId, MediaLoadData)}. */ + public void downstreamFormatChanged(MediaLoadData mediaLoadData) { + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + final MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, + () -> listener.onDownstreamFormatChanged(windowIndex, mediaPeriodId, mediaLoadData)); + } + } + + private long adjustMediaTime(long mediaTimeUs) { + long mediaTimeMs = C.usToMs(mediaTimeUs); + return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; + } + + private void postOrRun(Handler handler, Runnable runnable) { + if (handler.getLooper() == Looper.myLooper()) { + runnable.run(); + } else { + handler.post(runnable); + } + } + + private static final class ListenerAndHandler { + + public final Handler handler; + public final MediaSourceEventListener listener; + + public ListenerAndHandler(Handler handler, MediaSourceEventListener listener) { + this.handler = handler; + this.listener = listener; + } + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceFactory.java new file mode 100644 index 000000000000..37c9dcee25db --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceFactory.java @@ -0,0 +1,62 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +import android.net.Uri; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey; +import java.util.List; + +/** Factory for creating {@link MediaSource}s from URIs. */ +public interface MediaSourceFactory { + + /** + * Sets a list of {@link StreamKey StreamKeys} by which the manifest is filtered. + * + * @param streamKeys A list of {@link StreamKey StreamKeys}. + * @return This factory, for convenience. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. + */ + default MediaSourceFactory setStreamKeys(List streamKeys) { + return this; + } + + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + MediaSourceFactory setDrmSessionManager(DrmSessionManager drmSessionManager); + + /** + * Creates a new {@link MediaSource} with the specified {@code uri}. + * + * @param uri The URI to play. + * @return The new {@link MediaSource media source}. + */ + MediaSource createMediaSource(Uri uri); + + /** + * Returns the {@link C.ContentType content types} supported by media sources created by this + * factory. + */ + @C.ContentType + int[] getSupportedTypes(); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaPeriod.java index 353746291508..f3315ec5cd9b 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -15,12 +15,16 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.IdentityHashMap; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Merges multiple {@link MediaPeriod}s. @@ -30,25 +34,31 @@ import java.util.IdentityHashMap; public final MediaPeriod[] periods; private final IdentityHashMap streamPeriodIndices; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final ArrayList childrenPendingPreparation; - private Callback callback; - private int pendingChildPrepareCount; - private TrackGroupArray trackGroups; - + @Nullable private Callback callback; + @Nullable private TrackGroupArray trackGroups; private MediaPeriod[] enabledPeriods; - private SequenceableLoader sequenceableLoader; + private SequenceableLoader compositeSequenceableLoader; - public MergingMediaPeriod(MediaPeriod... periods) { + public MergingMediaPeriod(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + MediaPeriod... periods) { + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.periods = periods; + childrenPendingPreparation = new ArrayList<>(); + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(); streamPeriodIndices = new IdentityHashMap<>(); + enabledPeriods = new MediaPeriod[0]; } @Override - public void prepare(Callback callback) { + public void prepare(Callback callback, long positionUs) { this.callback = callback; - pendingChildPrepareCount = periods.length; + Collections.addAll(childrenPendingPreparation, periods); for (MediaPeriod period : periods) { - period.prepare(this); + period.prepare(this, positionUs); } } @@ -61,12 +71,16 @@ import java.util.IdentityHashMap; @Override public TrackGroupArray getTrackGroups() { - return trackGroups; + return Assertions.checkNotNull(trackGroups); } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { // Map each selection and stream onto a child period index. int[] streamChildIndices = new int[selections.length]; int[] selectionChildIndices = new int[selections.length]; @@ -86,9 +100,9 @@ import java.util.IdentityHashMap; } streamPeriodIndices.clear(); // Select tracks for each child, copying the resulting streams back into a new streams array. - SampleStream[] newStreams = new SampleStream[selections.length]; - SampleStream[] childStreams = new SampleStream[selections.length]; - TrackSelection[] childSelections = new TrackSelection[selections.length]; + @NullableType SampleStream[] newStreams = new SampleStream[selections.length]; + @NullableType SampleStream[] childStreams = new SampleStream[selections.length]; + @NullableType TrackSelection[] childSelections = new TrackSelection[selections.length]; ArrayList enabledPeriodsList = new ArrayList<>(periods.length); for (int i = 0; i < periods.length; i++) { for (int j = 0; j < selections.length; j++) { @@ -100,16 +114,16 @@ import java.util.IdentityHashMap; if (i == 0) { positionUs = selectPositionUs; } else if (selectPositionUs != positionUs) { - throw new IllegalStateException("Children enabled at different positions"); + throw new IllegalStateException("Children enabled at different positions."); } boolean periodEnabled = false; for (int j = 0; j < selections.length; j++) { if (selectionChildIndices[j] == i) { // Assert that the child provided a stream for the selection. - Assertions.checkState(childStreams[j] != null); + SampleStream childStream = Assertions.checkNotNull(childStreams[j]); newStreams[j] = childStreams[j]; periodEnabled = true; - streamPeriodIndices.put(childStreams[j], i); + streamPeriodIndices.put(childStream, i); } else if (streamChildIndices[j] == i) { // Assert that the child cleared any previous stream. Assertions.checkState(childStreams[j] == null); @@ -124,25 +138,45 @@ import java.util.IdentityHashMap; // Update the local state. enabledPeriods = new MediaPeriod[enabledPeriodsList.size()]; enabledPeriodsList.toArray(enabledPeriods); - sequenceableLoader = new CompositeSequenceableLoader(enabledPeriods); + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(enabledPeriods); return positionUs; } @Override - public void discardBuffer(long positionUs) { + public void discardBuffer(long positionUs, boolean toKeyframe) { for (MediaPeriod period : enabledPeriods) { - period.discardBuffer(positionUs); + period.discardBuffer(positionUs, toKeyframe); } } + @Override + public void reevaluateBuffer(long positionUs) { + compositeSequenceableLoader.reevaluateBuffer(positionUs); + } + @Override public boolean continueLoading(long positionUs) { - return sequenceableLoader.continueLoading(positionUs); + if (!childrenPendingPreparation.isEmpty()) { + // Preparation is still going on. + int childrenPendingPreparationSize = childrenPendingPreparation.size(); + for (int i = 0; i < childrenPendingPreparationSize; i++) { + childrenPendingPreparation.get(i).continueLoading(positionUs); + } + return false; + } else { + return compositeSequenceableLoader.continueLoading(positionUs); + } + } + + @Override + public boolean isLoading() { + return compositeSequenceableLoader.isLoading(); } @Override public long getNextLoadPositionUs() { - return sequenceableLoader.getNextLoadPositionUs(); + return compositeSequenceableLoader.getNextLoadPositionUs(); } @Override @@ -151,7 +185,7 @@ import java.util.IdentityHashMap; // Periods other than the first one are not allowed to report discontinuities. for (int i = 1; i < periods.length; i++) { if (periods[i].readDiscontinuity() != C.TIME_UNSET) { - throw new IllegalStateException("Child reported discontinuity"); + throw new IllegalStateException("Child reported discontinuity."); } } // It must be possible to seek enabled periods to the new position, if there is one. @@ -159,7 +193,7 @@ import java.util.IdentityHashMap; for (MediaPeriod enabledPeriod : enabledPeriods) { if (enabledPeriod != periods[0] && enabledPeriod.seekToUs(positionUs) != positionUs) { - throw new IllegalStateException("Children seeked to different positions"); + throw new IllegalStateException("Unexpected child seekToUs result."); } } } @@ -168,14 +202,7 @@ import java.util.IdentityHashMap; @Override public long getBufferedPositionUs() { - long bufferedPositionUs = Long.MAX_VALUE; - for (MediaPeriod period : enabledPeriods) { - long rendererBufferedPositionUs = period.getBufferedPositionUs(); - if (rendererBufferedPositionUs != C.TIME_END_OF_SOURCE) { - bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs); - } - } - return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs; + return compositeSequenceableLoader.getBufferedPositionUs(); } @Override @@ -184,17 +211,24 @@ import java.util.IdentityHashMap; // Additional periods must seek to the same position. for (int i = 1; i < enabledPeriods.length; i++) { if (enabledPeriods[i].seekToUs(positionUs) != positionUs) { - throw new IllegalStateException("Children seeked to different positions"); + throw new IllegalStateException("Unexpected child seekToUs result."); } } return positionUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + MediaPeriod queryPeriod = enabledPeriods.length > 0 ? enabledPeriods[0] : periods[0]; + return queryPeriod.getAdjustedSeekPositionUs(positionUs, seekParameters); + } + // MediaPeriod.Callback implementation @Override - public void onPrepared(MediaPeriod ignored) { - if (--pendingChildPrepareCount > 0) { + public void onPrepared(MediaPeriod preparedPeriod) { + childrenPendingPreparation.remove(preparedPeriod); + if (!childrenPendingPreparation.isEmpty()) { return; } int totalTrackGroupCount = 0; @@ -211,16 +245,12 @@ import java.util.IdentityHashMap; } } trackGroups = new TrackGroupArray(trackGroupArray); - callback.onPrepared(this); + Assertions.checkNotNull(callback).onPrepared(this); } @Override public void onContinueLoadingRequested(MediaPeriod ignored) { - if (trackGroups == null) { - // Still preparing. - return; - } - callback.onContinueLoadingRequested(this); + Assertions.checkNotNull(callback).onContinueLoadingRequested(this); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaSource.java index f13239e22425..ac2ef3c7da10 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -15,53 +15,48 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source; -import android.support.annotation.IntDef; -import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; import java.io.IOException; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; /** * Merges multiple {@link MediaSource}s. - *

- * The {@link Timeline}s of the sources being merged must have the same number of periods, and must - * not have any dynamic windows. + * + *

The {@link Timeline}s of the sources being merged must have the same number of periods. */ -public final class MergingMediaSource implements MediaSource { +public final class MergingMediaSource extends CompositeMediaSource { /** * Thrown when a {@link MergingMediaSource} cannot merge its sources. */ public static final class IllegalMergeException extends IOException { - /** - * The reason the merge failed. - */ + /** The reason the merge failed. One of {@link #REASON_PERIOD_COUNT_MISMATCH}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({REASON_WINDOWS_ARE_DYNAMIC, REASON_PERIOD_COUNT_MISMATCH}) + @IntDef({REASON_PERIOD_COUNT_MISMATCH}) public @interface Reason {} /** - * The merge failed because one of the sources being merged has a dynamic window. + * The sources have different period counts. */ - public static final int REASON_WINDOWS_ARE_DYNAMIC = 0; - /** - * The merge failed because the sources have different period counts. - */ - public static final int REASON_PERIOD_COUNT_MISMATCH = 1; + public static final int REASON_PERIOD_COUNT_MISMATCH = 0; /** - * The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and - * {@link #REASON_PERIOD_COUNT_MISMATCH}. + * The reason the merge failed. */ @Reason public final int reason; /** - * @param reason The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and - * {@link #REASON_PERIOD_COUNT_MISMATCH}. + * @param reason The reason the merge failed. */ public IllegalMergeException(@Reason int reason) { this.reason = reason; @@ -72,36 +67,46 @@ public final class MergingMediaSource implements MediaSource { private static final int PERIOD_COUNT_UNSET = -1; private final MediaSource[] mediaSources; + private final Timeline[] timelines; private final ArrayList pendingTimelineSources; - private final Timeline.Window window; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private Listener listener; - private Timeline primaryTimeline; - private Object primaryManifest; private int periodCount; - private IllegalMergeException mergeError; + @Nullable private IllegalMergeException mergeError; /** * @param mediaSources The {@link MediaSource}s to merge. */ public MergingMediaSource(MediaSource... mediaSources) { + this(new DefaultCompositeSequenceableLoaderFactory(), mediaSources); + } + + /** + * @param compositeSequenceableLoaderFactory A factory to create composite + * {@link SequenceableLoader}s for when this media source loads data from multiple streams + * (video, audio etc...). + * @param mediaSources The {@link MediaSource}s to merge. + */ + public MergingMediaSource(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + MediaSource... mediaSources) { this.mediaSources = mediaSources; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources)); - window = new Timeline.Window(); periodCount = PERIOD_COUNT_UNSET; + timelines = new Timeline[mediaSources.length]; } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - this.listener = listener; + @Nullable + public Object getTag() { + return mediaSources.length > 0 ? mediaSources[0].getTag() : null; + } + + @Override + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); for (int i = 0; i < mediaSources.length; i++) { - final int sourceIndex = i; - mediaSources[sourceIndex].prepareSource(player, false, new Listener() { - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - handleSourceInfoRefreshed(sourceIndex, timeline, manifest); - } - }); + prepareChildSource(i, mediaSources[i]); } } @@ -110,18 +115,19 @@ public final class MergingMediaSource implements MediaSource { if (mergeError != null) { throw mergeError; } - for (MediaSource mediaSource : mediaSources) { - mediaSource.maybeThrowSourceInfoRefreshError(); - } + super.maybeThrowSourceInfoRefreshError(); } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { MediaPeriod[] periods = new MediaPeriod[mediaSources.length]; + int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid); for (int i = 0; i < periods.length; i++) { - periods[i] = mediaSources[i].createPeriod(index, allocator, positionUs); + MediaPeriodId childMediaPeriodId = + id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex)); + periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator, startPositionUs); } - return new MergingMediaPeriod(periods); + return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods); } @Override @@ -133,36 +139,40 @@ public final class MergingMediaSource implements MediaSource { } @Override - public void releaseSource() { - for (MediaSource mediaSource : mediaSources) { - mediaSource.releaseSource(); - } + protected void releaseSourceInternal() { + super.releaseSourceInternal(); + Arrays.fill(timelines, null); + periodCount = PERIOD_COUNT_UNSET; + mergeError = null; + pendingTimelineSources.clear(); + Collections.addAll(pendingTimelineSources, mediaSources); } - private void handleSourceInfoRefreshed(int sourceIndex, Timeline timeline, Object manifest) { + @Override + protected void onChildSourceInfoRefreshed( + Integer id, MediaSource mediaSource, Timeline timeline) { if (mergeError == null) { mergeError = checkTimelineMerges(timeline); } if (mergeError != null) { return; } - pendingTimelineSources.remove(mediaSources[sourceIndex]); - if (sourceIndex == 0) { - primaryTimeline = timeline; - primaryManifest = manifest; - } + pendingTimelineSources.remove(mediaSource); + timelines[id] = timeline; if (pendingTimelineSources.isEmpty()) { - listener.onSourceInfoRefreshed(primaryTimeline, primaryManifest); + refreshSourceInfo(timelines[0]); } } + @Override + @Nullable + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + Integer id, MediaPeriodId mediaPeriodId) { + return id == 0 ? mediaPeriodId : null; + } + + @Nullable private IllegalMergeException checkTimelineMerges(Timeline timeline) { - int windowCount = timeline.getWindowCount(); - for (int i = 0; i < windowCount; i++) { - if (timeline.getWindow(i, window, false).isDynamic) { - return new IllegalMergeException(IllegalMergeException.REASON_WINDOWS_ARE_DYNAMIC); - } - } if (periodCount == PERIOD_COUNT_UNSET) { periodCount = timeline.getPeriodCount(); } else if (timeline.getPeriodCount() != periodCount) { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java new file mode 100644 index 000000000000..4c62a73edbab --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -0,0 +1,1162 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +import android.net.Uri; +import android.os.Handler; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap.SeekPoints; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap.Unseekable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.icy.IcyHeaders; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.Loadable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.StatsDataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ConditionVariable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.EOFException; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** A {@link MediaPeriod} that extracts data using an {@link Extractor}. */ +/* package */ final class ProgressiveMediaPeriod + implements MediaPeriod, + ExtractorOutput, + Loader.Callback, + Loader.ReleaseCallback, + UpstreamFormatChangedListener { + + /** + * Listener for information about the period. + */ + interface Listener { + + /** + * Called when the duration, the ability to seek within the period, or the categorization as + * live stream changes. + * + * @param durationUs The duration of the period, or {@link C#TIME_UNSET}. + * @param isSeekable Whether the period is seekable. + * @param isLive Whether the period is live. + */ + void onSourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive); + } + + /** + * When the source's duration is unknown, it is calculated by adding this value to the largest + * sample timestamp seen when buffering completes. + */ + private static final long DEFAULT_LAST_SAMPLE_DURATION_US = 10000; + + private static final Map ICY_METADATA_HEADERS = createIcyMetadataHeaders(); + + private static final Format ICY_FORMAT = + Format.createSampleFormat("icy", MimeTypes.APPLICATION_ICY, Format.OFFSET_SAMPLE_RELATIVE); + + private final Uri uri; + private final DataSource dataSource; + private final DrmSessionManager drmSessionManager; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; + private final EventDispatcher eventDispatcher; + private final Listener listener; + private final Allocator allocator; + @Nullable private final String customCacheKey; + private final long continueLoadingCheckIntervalBytes; + private final Loader loader; + private final ExtractorHolder extractorHolder; + private final ConditionVariable loadCondition; + private final Runnable maybeFinishPrepareRunnable; + private final Runnable onContinueLoadingRequestedRunnable; + private final Handler handler; + + @Nullable private Callback callback; + @Nullable private SeekMap seekMap; + @Nullable private IcyHeaders icyHeaders; + private SampleQueue[] sampleQueues; + private TrackId[] sampleQueueTrackIds; + private boolean sampleQueuesBuilt; + private boolean prepared; + + @Nullable private PreparedState preparedState; + private boolean haveAudioVideoTracks; + private int dataType; + + private boolean seenFirstTrackSelection; + private boolean notifyDiscontinuity; + private boolean notifiedReadingStarted; + private int enabledTrackCount; + private long durationUs; + private long length; + private boolean isLive; + + private long lastSeekPositionUs; + private long pendingResetPositionUs; + private boolean pendingDeferredRetry; + + private int extractedSamplesCountAtStartOfLoad; + private boolean loadingFinished; + private boolean released; + + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSource The data source to read the media. + * @param extractors The extractors to use to read the data source. + * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. + * @param eventDispatcher A dispatcher to notify of events. + * @param listener A listener to notify when information about the period changes. + * @param allocator An {@link Allocator} from which to obtain media buffer allocations. + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache + * indexing. May be null. + * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each + * invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. + */ + // maybeFinishPrepare is not posted to the handler until initialization completes. + @SuppressWarnings({ + "nullness:argument.type.incompatible", + "nullness:methodref.receiver.bound.invalid" + }) + public ProgressiveMediaPeriod( + Uri uri, + DataSource dataSource, + Extractor[] extractors, + DrmSessionManager drmSessionManager, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, + EventDispatcher eventDispatcher, + Listener listener, + Allocator allocator, + @Nullable String customCacheKey, + int continueLoadingCheckIntervalBytes) { + this.uri = uri; + this.dataSource = dataSource; + this.drmSessionManager = drmSessionManager; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + this.eventDispatcher = eventDispatcher; + this.listener = listener; + this.allocator = allocator; + this.customCacheKey = customCacheKey; + this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; + loader = new Loader("Loader:ProgressiveMediaPeriod"); + extractorHolder = new ExtractorHolder(extractors); + loadCondition = new ConditionVariable(); + maybeFinishPrepareRunnable = this::maybeFinishPrepare; + onContinueLoadingRequestedRunnable = + () -> { + if (!released) { + Assertions.checkNotNull(callback) + .onContinueLoadingRequested(ProgressiveMediaPeriod.this); + } + }; + handler = new Handler(); + 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; + eventDispatcher.mediaPeriodCreated(); + } + + public void release() { + if (prepared) { + // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise + // sampleQueues may still be being modified by the loading thread. + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.preRelease(); + } + } + loader.release(/* callback= */ this); + handler.removeCallbacksAndMessages(null); + callback = null; + released = true; + eventDispatcher.mediaPeriodReleased(); + } + + @Override + public void onLoaderReleased() { + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.release(); + } + extractorHolder.release(); + } + + @Override + public void prepare(Callback callback, long positionUs) { + this.callback = callback; + loadCondition.open(); + startLoading(); + } + + @Override + public void maybeThrowPrepareError() throws IOException { + maybeThrowError(); + if (loadingFinished && !prepared) { + throw new ParserException("Loading finished before preparation is complete."); + } + } + + @Override + public TrackGroupArray getTrackGroups() { + return getPreparedState().tracks; + } + + @Override + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { + PreparedState preparedState = getPreparedState(); + TrackGroupArray tracks = preparedState.tracks; + boolean[] trackEnabledStates = preparedState.trackEnabledStates; + int oldEnabledTrackCount = enabledTrackCount; + // Deselect old tracks. + for (int i = 0; i < selections.length; i++) { + if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { + int track = ((SampleStreamImpl) streams[i]).track; + Assertions.checkState(trackEnabledStates[track]); + enabledTrackCount--; + trackEnabledStates[track] = false; + streams[i] = null; + } + } + // We'll always need to seek if this is a first selection to a non-zero position, or if we're + // making a selection having previously disabled all tracks. + boolean seekRequired = seenFirstTrackSelection ? oldEnabledTrackCount == 0 : positionUs != 0; + // Select new tracks. + for (int i = 0; i < selections.length; i++) { + if (streams[i] == null && selections[i] != null) { + TrackSelection selection = selections[i]; + Assertions.checkState(selection.length() == 1); + Assertions.checkState(selection.getIndexInTrackGroup(0) == 0); + int track = tracks.indexOf(selection.getTrackGroup()); + Assertions.checkState(!trackEnabledStates[track]); + enabledTrackCount++; + trackEnabledStates[track] = true; + streams[i] = new SampleStreamImpl(track); + streamResetFlags[i] = true; + // If there's still a chance of avoiding a seek, try and seek within the sample queue. + if (!seekRequired) { + SampleQueue sampleQueue = sampleQueues[track]; + // A seek can be avoided if we're able to seek to the current playback position in the + // sample queue, or if we haven't read anything from the queue since the previous seek + // (this case is common for sparse tracks such as metadata tracks). In all other cases a + // seek is required. + seekRequired = + !sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ true) + && sampleQueue.getReadIndex() != 0; + } + } + } + if (enabledTrackCount == 0) { + pendingDeferredRetry = false; + notifyDiscontinuity = false; + if (loader.isLoading()) { + // Discard as much as we can synchronously. + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.discardToEnd(); + } + loader.cancelLoading(); + } else { + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(); + } + } + } else if (seekRequired) { + positionUs = seekToUs(positionUs); + // We'll need to reset renderers consuming from all streams due to the seek. + for (int i = 0; i < streams.length; i++) { + if (streams[i] != null) { + streamResetFlags[i] = true; + } + } + } + seenFirstTrackSelection = true; + return positionUs; + } + + @Override + public void discardBuffer(long positionUs, boolean toKeyframe) { + if (isPendingReset()) { + return; + } + boolean[] trackEnabledStates = getPreparedState().trackEnabledStates; + int trackCount = sampleQueues.length; + for (int i = 0; i < trackCount; i++) { + sampleQueues[i].discardTo(positionUs, toKeyframe, trackEnabledStates[i]); + } + } + + @Override + public void reevaluateBuffer(long positionUs) { + // Do nothing. + } + + @Override + public boolean continueLoading(long playbackPositionUs) { + if (loadingFinished + || loader.hasFatalError() + || pendingDeferredRetry + || (prepared && enabledTrackCount == 0)) { + return false; + } + boolean continuedLoading = loadCondition.open(); + if (!loader.isLoading()) { + startLoading(); + continuedLoading = true; + } + return continuedLoading; + } + + @Override + public boolean isLoading() { + return loader.isLoading() && loadCondition.isOpen(); + } + + @Override + public long getNextLoadPositionUs() { + return enabledTrackCount == 0 ? C.TIME_END_OF_SOURCE : getBufferedPositionUs(); + } + + @Override + public long readDiscontinuity() { + if (!notifiedReadingStarted) { + eventDispatcher.readingStarted(); + notifiedReadingStarted = true; + } + if (notifyDiscontinuity + && (loadingFinished || getExtractedSamplesCount() > extractedSamplesCountAtStartOfLoad)) { + notifyDiscontinuity = false; + return lastSeekPositionUs; + } + return C.TIME_UNSET; + } + + @Override + public long getBufferedPositionUs() { + boolean[] trackIsAudioVideoFlags = getPreparedState().trackIsAudioVideoFlags; + if (loadingFinished) { + return C.TIME_END_OF_SOURCE; + } else if (isPendingReset()) { + return pendingResetPositionUs; + } + long largestQueuedTimestampUs = Long.MAX_VALUE; + if (haveAudioVideoTracks) { + // 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()) { + largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs, + sampleQueues[i].getLargestQueuedTimestampUs()); + } + } + } + if (largestQueuedTimestampUs == Long.MAX_VALUE) { + largestQueuedTimestampUs = getLargestQueuedTimestampUs(); + } + return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs + : largestQueuedTimestampUs; + } + + @Override + public long seekToUs(long positionUs) { + PreparedState preparedState = getPreparedState(); + SeekMap seekMap = preparedState.seekMap; + boolean[] trackIsAudioVideoFlags = preparedState.trackIsAudioVideoFlags; + // Treat all seeks into non-seekable media as being to t=0. + positionUs = seekMap.isSeekable() ? positionUs : 0; + + notifyDiscontinuity = false; + lastSeekPositionUs = positionUs; + if (isPendingReset()) { + // A reset is already pending. We only need to update its position. + pendingResetPositionUs = positionUs; + return positionUs; + } + + // If we're not playing a live stream, try and seek within the buffer. + if (dataType != C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE + && seekInsideBufferUs(trackIsAudioVideoFlags, positionUs)) { + return positionUs; + } + + // We can't seek inside the buffer, and so need to reset. + pendingDeferredRetry = false; + pendingResetPositionUs = positionUs; + loadingFinished = false; + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + loader.clearFatalError(); + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(); + } + } + return positionUs; + } + + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + SeekMap seekMap = getPreparedState().seekMap; + if (!seekMap.isSeekable()) { + // Treat all seeks into non-seekable media as being to t=0. + return 0; + } + SeekPoints seekPoints = seekMap.getSeekPoints(positionUs); + return Util.resolveSeekPositionUs( + positionUs, seekParameters, seekPoints.first.timeUs, seekPoints.second.timeUs); + } + + // SampleStream methods. + + /* package */ boolean isReady(int track) { + return !suppressRead() && sampleQueues[track].isReady(loadingFinished); + } + + /* package */ void maybeThrowError(int sampleQueueIndex) throws IOException { + sampleQueues[sampleQueueIndex].maybeThrowError(); + maybeThrowError(); + } + + /* package */ void maybeThrowError() throws IOException { + loader.maybeThrowError(loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType)); + } + + /* package */ int readData( + int sampleQueueIndex, + FormatHolder formatHolder, + DecoderInputBuffer buffer, + boolean formatRequired) { + if (suppressRead()) { + return C.RESULT_NOTHING_READ; + } + maybeNotifyDownstreamFormat(sampleQueueIndex); + int result = + sampleQueues[sampleQueueIndex].read( + formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); + if (result == C.RESULT_NOTHING_READ) { + maybeStartDeferredRetry(sampleQueueIndex); + } + return result; + } + + /* package */ int skipData(int track, long positionUs) { + if (suppressRead()) { + return 0; + } + maybeNotifyDownstreamFormat(track); + SampleQueue sampleQueue = sampleQueues[track]; + int skipCount; + if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { + skipCount = sampleQueue.advanceToEnd(); + } else { + skipCount = sampleQueue.advanceTo(positionUs); + } + if (skipCount == 0) { + maybeStartDeferredRetry(track); + } + return skipCount; + } + + private void maybeNotifyDownstreamFormat(int track) { + PreparedState preparedState = getPreparedState(); + boolean[] trackNotifiedDownstreamFormats = preparedState.trackNotifiedDownstreamFormats; + if (!trackNotifiedDownstreamFormats[track]) { + Format trackFormat = preparedState.tracks.get(track).getFormat(/* index= */ 0); + eventDispatcher.downstreamFormatChanged( + MimeTypes.getTrackType(trackFormat.sampleMimeType), + trackFormat, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + lastSeekPositionUs); + trackNotifiedDownstreamFormats[track] = true; + } + } + + private void maybeStartDeferredRetry(int track) { + boolean[] trackIsAudioVideoFlags = getPreparedState().trackIsAudioVideoFlags; + if (!pendingDeferredRetry + || !trackIsAudioVideoFlags[track] + || sampleQueues[track].isReady(/* loadingFinished= */ false)) { + return; + } + pendingResetPositionUs = 0; + pendingDeferredRetry = false; + notifyDiscontinuity = true; + lastSeekPositionUs = 0; + extractedSamplesCountAtStartOfLoad = 0; + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(); + } + Assertions.checkNotNull(callback).onContinueLoadingRequested(this); + } + + private boolean suppressRead() { + return notifyDiscontinuity || isPendingReset(); + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs) { + if (durationUs == C.TIME_UNSET && seekMap != null) { + boolean isSeekable = seekMap.isSeekable(); + long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); + durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0 + : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; + listener.onSourceInfoRefreshed(durationUs, isSeekable, isLive); + } + eventDispatcher.loadCompleted( + loadable.dataSpec, + loadable.dataSource.getLastOpenedUri(), + loadable.dataSource.getLastResponseHeaders(), + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ loadable.seekTimeUs, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.dataSource.getBytesRead()); + copyLengthFromLoader(loadable); + loadingFinished = true; + Assertions.checkNotNull(callback).onContinueLoadingRequested(this); + } + + @Override + public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs, boolean released) { + eventDispatcher.loadCanceled( + loadable.dataSpec, + loadable.dataSource.getLastOpenedUri(), + loadable.dataSource.getLastResponseHeaders(), + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ loadable.seekTimeUs, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.dataSource.getBytesRead()); + if (!released) { + copyLengthFromLoader(loadable); + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(); + } + if (enabledTrackCount > 0) { + Assertions.checkNotNull(callback).onContinueLoadingRequested(this); + } + } + } + + @Override + public LoadErrorAction onLoadError( + ExtractingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error, + int errorCount) { + copyLengthFromLoader(loadable); + LoadErrorAction loadErrorAction; + long retryDelayMs = + loadErrorHandlingPolicy.getRetryDelayMsFor(dataType, loadDurationMs, error, errorCount); + if (retryDelayMs == C.TIME_UNSET) { + loadErrorAction = Loader.DONT_RETRY_FATAL; + } else /* the load should be retried */ { + int extractedSamplesCount = getExtractedSamplesCount(); + boolean madeProgress = extractedSamplesCount > extractedSamplesCountAtStartOfLoad; + loadErrorAction = + configureRetry(loadable, extractedSamplesCount) + ? Loader.createRetryAction(/* resetErrorCount= */ madeProgress, retryDelayMs) + : Loader.DONT_RETRY; + } + + eventDispatcher.loadError( + loadable.dataSpec, + loadable.dataSource.getLastOpenedUri(), + loadable.dataSource.getLastResponseHeaders(), + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ loadable.seekTimeUs, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.dataSource.getBytesRead(), + error, + !loadErrorAction.isRetry()); + return loadErrorAction; + } + + // ExtractorOutput implementation. Called by the loading thread. + + @Override + public TrackOutput track(int id, int type) { + return prepareTrackOutput(new TrackId(id, /* isIcyTrack= */ false)); + } + + @Override + public void endTracks() { + sampleQueuesBuilt = true; + handler.post(maybeFinishPrepareRunnable); + } + + @Override + public void seekMap(SeekMap seekMap) { + this.seekMap = icyHeaders == null ? seekMap : new Unseekable(/* durationUs */ C.TIME_UNSET); + handler.post(maybeFinishPrepareRunnable); + } + + // Icy metadata. Called by the loading thread. + + /* package */ TrackOutput icyTrack() { + return prepareTrackOutput(new TrackId(0, /* isIcyTrack= */ true)); + } + + // UpstreamFormatChangedListener implementation. Called by the loading thread. + + @Override + public void onUpstreamFormatChanged(Format format) { + handler.post(maybeFinishPrepareRunnable); + } + + // Internal methods. + + private TrackOutput prepareTrackOutput(TrackId id) { + int trackCount = sampleQueues.length; + for (int i = 0; i < trackCount; i++) { + if (id.equals(sampleQueueTrackIds[i])) { + return sampleQueues[i]; + } + } + SampleQueue trackOutput = new SampleQueue(allocator, drmSessionManager); + trackOutput.setUpstreamFormatChangeListener(this); + @NullableType + TrackId[] sampleQueueTrackIds = Arrays.copyOf(this.sampleQueueTrackIds, trackCount + 1); + sampleQueueTrackIds[trackCount] = id; + this.sampleQueueTrackIds = Util.castNonNullTypeArray(sampleQueueTrackIds); + @NullableType SampleQueue[] sampleQueues = Arrays.copyOf(this.sampleQueues, trackCount + 1); + sampleQueues[trackCount] = trackOutput; + this.sampleQueues = Util.castNonNullTypeArray(sampleQueues); + return trackOutput; + } + + private void maybeFinishPrepare() { + SeekMap seekMap = this.seekMap; + if (released || prepared || !sampleQueuesBuilt || seekMap == null) { + return; + } + for (SampleQueue sampleQueue : sampleQueues) { + if (sampleQueue.getUpstreamFormat() == null) { + return; + } + } + loadCondition.close(); + int trackCount = sampleQueues.length; + TrackGroup[] trackArray = new TrackGroup[trackCount]; + boolean[] trackIsAudioVideoFlags = new boolean[trackCount]; + durationUs = seekMap.getDurationUs(); + for (int i = 0; i < trackCount; i++) { + Format trackFormat = sampleQueues[i].getUpstreamFormat(); + String mimeType = trackFormat.sampleMimeType; + boolean isAudio = MimeTypes.isAudio(mimeType); + boolean isAudioVideo = isAudio || MimeTypes.isVideo(mimeType); + trackIsAudioVideoFlags[i] = isAudioVideo; + haveAudioVideoTracks |= isAudioVideo; + IcyHeaders icyHeaders = this.icyHeaders; + if (icyHeaders != null) { + if (isAudio || sampleQueueTrackIds[i].isIcyTrack) { + Metadata metadata = trackFormat.metadata; + trackFormat = + trackFormat.copyWithMetadata( + metadata == null + ? new Metadata(icyHeaders) + : metadata.copyWithAppendedEntries(icyHeaders)); + } + if (isAudio + && trackFormat.bitrate == Format.NO_VALUE + && icyHeaders.bitrate != Format.NO_VALUE) { + trackFormat = trackFormat.copyWithBitrate(icyHeaders.bitrate); + } + } + trackArray[i] = new TrackGroup(trackFormat); + } + isLive = length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET; + dataType = isLive ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE : C.DATA_TYPE_MEDIA; + preparedState = + new PreparedState(seekMap, new TrackGroupArray(trackArray), trackIsAudioVideoFlags); + prepared = true; + listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable(), isLive); + Assertions.checkNotNull(callback).onPrepared(this); + } + + private PreparedState getPreparedState() { + return Assertions.checkNotNull(preparedState); + } + + private void copyLengthFromLoader(ExtractingLoadable loadable) { + if (length == C.LENGTH_UNSET) { + length = loadable.length; + } + } + + private void startLoading() { + ExtractingLoadable loadable = + new ExtractingLoadable( + uri, dataSource, extractorHolder, /* extractorOutput= */ this, loadCondition); + if (prepared) { + SeekMap seekMap = getPreparedState().seekMap; + Assertions.checkState(isPendingReset()); + if (durationUs != C.TIME_UNSET && pendingResetPositionUs > durationUs) { + loadingFinished = true; + pendingResetPositionUs = C.TIME_UNSET; + return; + } + loadable.setLoadPosition( + seekMap.getSeekPoints(pendingResetPositionUs).first.position, pendingResetPositionUs); + pendingResetPositionUs = C.TIME_UNSET; + } + extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); + long elapsedRealtimeMs = + loader.startLoading( + loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType)); + eventDispatcher.loadStarted( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ loadable.seekTimeUs, + durationUs, + elapsedRealtimeMs); + } + + /** + * Called to configure a retry when a load error occurs. + * + * @param loadable The current loadable for which the error was encountered. + * @param currentExtractedSampleCount The current number of samples that have been extracted into + * the sample queues. + * @return Whether the loader should retry with the current loadable. False indicates a deferred + * retry. + */ + private boolean configureRetry(ExtractingLoadable loadable, int currentExtractedSampleCount) { + if (length != C.LENGTH_UNSET + || (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; + return true; + } else if (prepared && !suppressRead()) { + // We're playing a stream of unknown length and duration. Assume it's live, and therefore that + // the data at the uri is a continuously shifting window of the latest available media. For + // this case there's no way to continue loading from where a previous load finished, so it's + // necessary to load from the start whenever commencing a new load. Deferring the retry until + // we run out of buffered data makes for a much better user experience. See: + // https://github.com/google/ExoPlayer/issues/1606. + // Note that the suppressRead() check means only a single deferred retry can occur without + // progress being made. Any subsequent failures without progress will go through the else + // block below. + pendingDeferredRetry = true; + return false; + } else { + // This is the same case as above, except in this case there's no value in deferring the retry + // because there's no buffered data to be read. This case also covers an on-demand stream with + // unknown length that has yet to be prepared. This case cannot be disambiguated from the live + // stream case, so we have no option but to load from the start. + notifyDiscontinuity = prepared; + lastSeekPositionUs = 0; + extractedSamplesCountAtStartOfLoad = 0; + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(); + } + loadable.setLoadPosition(0, 0); + return true; + } + } + + /** + * Attempts to seek to the specified position within the sample queues. + * + * @param trackIsAudioVideoFlags Whether each track is audio/video. + * @param positionUs The seek position in microseconds. + * @return Whether the in-buffer seek was successful. + */ + private boolean seekInsideBufferUs(boolean[] trackIsAudioVideoFlags, long positionUs) { + int trackCount = sampleQueues.length; + for (int i = 0; i < trackCount; i++) { + SampleQueue sampleQueue = sampleQueues[i]; + boolean seekInsideQueue = sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ false); + // If we have AV tracks then an in-buffer seek is successful if the seek into every AV queue + // is successful. We ignore whether seeks within non-AV queues are successful in this case, as + // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is + // successful only if the seek into every queue succeeds. + if (!seekInsideQueue && (trackIsAudioVideoFlags[i] || !haveAudioVideoTracks)) { + return false; + } + } + return true; + } + + private int getExtractedSamplesCount() { + int extractedSamplesCount = 0; + for (SampleQueue sampleQueue : sampleQueues) { + extractedSamplesCount += sampleQueue.getWriteIndex(); + } + return extractedSamplesCount; + } + + private long getLargestQueuedTimestampUs() { + long largestQueuedTimestampUs = Long.MIN_VALUE; + for (SampleQueue sampleQueue : sampleQueues) { + largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, + sampleQueue.getLargestQueuedTimestampUs()); + } + return largestQueuedTimestampUs; + } + + private boolean isPendingReset() { + return pendingResetPositionUs != C.TIME_UNSET; + } + + private final class SampleStreamImpl implements SampleStream { + + private final int track; + + public SampleStreamImpl(int track) { + this.track = track; + } + + @Override + public boolean isReady() { + return ProgressiveMediaPeriod.this.isReady(track); + } + + @Override + public void maybeThrowError() throws IOException { + ProgressiveMediaPeriod.this.maybeThrowError(track); + } + + @Override + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + return ProgressiveMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired); + } + + @Override + public int skipData(long positionUs) { + return ProgressiveMediaPeriod.this.skipData(track, positionUs); + } + + } + + /** Loads the media stream and extracts sample data from it. */ + /* package */ final class ExtractingLoadable implements Loadable, IcyDataSource.Listener { + + private final Uri uri; + private final StatsDataSource dataSource; + private final ExtractorHolder extractorHolder; + private final ExtractorOutput extractorOutput; + private final ConditionVariable loadCondition; + private final PositionHolder positionHolder; + + private volatile boolean loadCanceled; + + private boolean pendingExtractorSeek; + private long seekTimeUs; + private DataSpec dataSpec; + private long length; + @Nullable private TrackOutput icyTrackOutput; + private boolean seenIcyMetadata; + + @SuppressWarnings("method.invocation.invalid") + public ExtractingLoadable( + Uri uri, + DataSource dataSource, + ExtractorHolder extractorHolder, + ExtractorOutput extractorOutput, + ConditionVariable loadCondition) { + this.uri = uri; + this.dataSource = new StatsDataSource(dataSource); + this.extractorHolder = extractorHolder; + this.extractorOutput = extractorOutput; + this.loadCondition = loadCondition; + this.positionHolder = new PositionHolder(); + this.pendingExtractorSeek = true; + this.length = C.LENGTH_UNSET; + dataSpec = buildDataSpec(/* position= */ 0); + } + + // Loadable implementation. + + @Override + public void cancelLoad() { + loadCanceled = true; + } + + @Override + public void load() throws IOException, InterruptedException { + int result = Extractor.RESULT_CONTINUE; + while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { + ExtractorInput input = null; + try { + long position = positionHolder.position; + dataSpec = buildDataSpec(position); + length = dataSource.open(dataSpec); + if (length != C.LENGTH_UNSET) { + length += position; + } + Uri uri = Assertions.checkNotNull(dataSource.getUri()); + icyHeaders = IcyHeaders.parse(dataSource.getResponseHeaders()); + DataSource extractorDataSource = dataSource; + if (icyHeaders != null && icyHeaders.metadataInterval != C.LENGTH_UNSET) { + extractorDataSource = new IcyDataSource(dataSource, icyHeaders.metadataInterval, this); + icyTrackOutput = icyTrack(); + icyTrackOutput.format(ICY_FORMAT); + } + input = new DefaultExtractorInput(extractorDataSource, position, length); + Extractor extractor = extractorHolder.selectExtractor(input, extractorOutput, uri); + + // MP3 live streams commonly have seekable metadata, despite being unseekable. + if (icyHeaders != null && extractor instanceof Mp3Extractor) { + ((Mp3Extractor) extractor).disableSeeking(); + } + + if (pendingExtractorSeek) { + extractor.seek(position, seekTimeUs); + pendingExtractorSeek = false; + } + while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { + loadCondition.block(); + result = extractor.read(input, positionHolder); + if (input.getPosition() > position + continueLoadingCheckIntervalBytes) { + position = input.getPosition(); + loadCondition.close(); + handler.post(onContinueLoadingRequestedRunnable); + } + } + } finally { + if (result == Extractor.RESULT_SEEK) { + result = Extractor.RESULT_CONTINUE; + } else if (input != null) { + positionHolder.position = input.getPosition(); + } + Util.closeQuietly(dataSource); + } + } + } + + // IcyDataSource.Listener + + @Override + 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 : Math.max(getLargestQueuedTimestampUs(), seekTimeUs); + int length = metadata.bytesLeft(); + TrackOutput icyTrackOutput = Assertions.checkNotNull(this.icyTrackOutput); + icyTrackOutput.sampleData(metadata, length); + icyTrackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, length, /* offset= */ 0, /* encryptionData= */ null); + seenIcyMetadata = true; + } + + // Internal methods. + + private DataSpec buildDataSpec(long position) { + // Disable caching if the content length cannot be resolved, since this is indicative of a + // progressive live stream. + return new DataSpec( + uri, + position, + C.LENGTH_UNSET, + customCacheKey, + DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN | DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION, + ICY_METADATA_HEADERS); + } + + private void setLoadPosition(long position, long timeUs) { + positionHolder.position = position; + seekTimeUs = timeUs; + pendingExtractorSeek = true; + seenIcyMetadata = false; + } + } + + /** Stores a list of extractors and a selected extractor when the format has been detected. */ + private static final class ExtractorHolder { + + private final Extractor[] extractors; + + @Nullable private Extractor extractor; + + /** + * Creates a holder that will select an extractor and initialize it using the specified output. + * + * @param extractors One or more extractors to choose from. + */ + public ExtractorHolder(Extractor[] extractors) { + this.extractors = extractors; + } + + /** + * Returns an initialized extractor for reading {@code input}, and returns the same extractor on + * later calls. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @param output The {@link ExtractorOutput} that will be used to initialize the selected + * extractor. + * @param uri The {@link Uri} of the data. + * @return An initialized extractor for reading {@code input}. + * @throws UnrecognizedInputFormatException Thrown if the input format could not be detected. + * @throws IOException Thrown if the input could not be read. + * @throws InterruptedException Thrown if the thread was interrupted. + */ + public Extractor selectExtractor(ExtractorInput input, ExtractorOutput output, Uri uri) + throws IOException, InterruptedException { + if (extractor != null) { + return extractor; + } + if (extractors.length == 1) { + this.extractor = extractors[0]; + } else { + for (Extractor extractor : extractors) { + try { + if (extractor.sniff(input)) { + this.extractor = extractor; + break; + } + } catch (EOFException e) { + // Do nothing. + } finally { + input.resetPeekPosition(); + } + } + if (extractor == null) { + throw new UnrecognizedInputFormatException( + "None of the available extractors (" + + Util.getCommaDelimitedSimpleClassNames(extractors) + + ") could read the stream.", + uri); + } + } + extractor.init(output); + return extractor; + } + + public void release() { + if (extractor != null) { + extractor.release(); + extractor = null; + } + } + } + + /** Stores state that is initialized when preparation completes. */ + private static final class PreparedState { + + public final SeekMap seekMap; + public final TrackGroupArray tracks; + public final boolean[] trackIsAudioVideoFlags; + public final boolean[] trackEnabledStates; + public final boolean[] trackNotifiedDownstreamFormats; + + public PreparedState( + SeekMap seekMap, TrackGroupArray tracks, boolean[] trackIsAudioVideoFlags) { + this.seekMap = seekMap; + this.tracks = tracks; + this.trackIsAudioVideoFlags = trackIsAudioVideoFlags; + this.trackEnabledStates = new boolean[tracks.length]; + this.trackNotifiedDownstreamFormats = new boolean[tracks.length]; + } + } + + /** Identifies a track. */ + private static final class TrackId { + + public final int id; + public final boolean isIcyTrack; + + public TrackId(int id, boolean isIcyTrack) { + this.id = id; + this.isIcyTrack = isIcyTrack; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + TrackId other = (TrackId) obj; + return id == other.id && isIcyTrack == other.isIcyTrack; + } + + @Override + public int hashCode() { + return 31 * id + (isIcyTrack ? 1 : 0); + } + } + + private static Map createIcyMetadataHeaders() { + Map headers = new HashMap<>(); + headers.put( + IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME, + IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE); + return Collections.unmodifiableMap(headers); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaSource.java new file mode 100644 index 000000000000..bed34a354b71 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +import android.net.Uri; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; + +/** + * Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}. + * + *

If the possible input stream container formats are known, pass a factory that instantiates + * extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to use + * the default extractors. When reading a new stream, the first {@link Extractor} in the array of + * extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will be + * used to extract samples from the input stream. + * + *

Note that the built-in extractor for FLV streams does not support seeking. + */ +public final class ProgressiveMediaSource extends BaseMediaSource + implements ProgressiveMediaPeriod.Listener { + + /** Factory for {@link ProgressiveMediaSource}s. */ + public static final class Factory implements MediaSourceFactory { + + private final DataSource.Factory dataSourceFactory; + + private ExtractorsFactory extractorsFactory; + @Nullable private String customCacheKey; + @Nullable private Object tag; + private DrmSessionManager drmSessionManager; + private LoadErrorHandlingPolicy loadErrorHandlingPolicy; + private int continueLoadingCheckIntervalBytes; + private boolean isCreateCalled; + + /** + * Creates a new factory for {@link ProgressiveMediaSource}s, using the extractors provided by + * {@link DefaultExtractorsFactory}. + * + * @param dataSourceFactory A factory for {@link DataSource}s to read the media. + */ + public Factory(DataSource.Factory dataSourceFactory) { + this(dataSourceFactory, new DefaultExtractorsFactory()); + } + + /** + * Creates a new factory for {@link ProgressiveMediaSource}s. + * + * @param dataSourceFactory A factory for {@link DataSource}s to read the media. + * @param extractorsFactory A factory for extractors used to extract media from its container. + */ + public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) { + this.dataSourceFactory = dataSourceFactory; + this.extractorsFactory = extractorsFactory; + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); + continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; + } + + /** + * Sets the factory for {@link Extractor}s to process the media stream. The default value is an + * instance of {@link DefaultExtractorsFactory}. + * + * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the + * possible formats are known, pass a factory that instantiates extractors for those + * formats. + * @return This factory, for convenience. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. + * @deprecated Pass the {@link ExtractorsFactory} via {@link #Factory(DataSource.Factory, + * ExtractorsFactory)}. This is necessary so that proguard can treat the default extractors + * factory as unused. + */ + @Deprecated + public Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) { + Assertions.checkState(!isCreateCalled); + this.extractorsFactory = extractorsFactory; + return this; + } + + /** + * Sets the custom key that uniquely identifies the original stream. Used for cache indexing. + * The default value is {@code null}. + * + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for + * cache indexing. + * @return This factory, for convenience. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. + */ + public Factory setCustomCacheKey(@Nullable String customCacheKey) { + Assertions.checkState(!isCreateCalled); + this.customCacheKey = customCacheKey; + return this; + } + + /** + * Sets a tag for the media source which will be published in the {@link + * com.google.android.exoplayer2.Timeline} of the source as {@link + * com.google.android.exoplayer2.Timeline.Window#tag}. + * + * @param tag A tag for the media source. + * @return This factory, for convenience. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. + */ + public Factory setTag(Object tag) { + Assertions.checkState(!isCreateCalled); + this.tag = tag; + return this; + } + + /** + * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link + * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. + * + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @return This factory, for convenience. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. + */ + public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + Assertions.checkState(!isCreateCalled); + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + return this; + } + + /** + * Sets the number of bytes that should be loaded between each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. The default value is + * {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}. + * + * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between + * each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + * @return This factory, for convenience. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. + */ + public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { + Assertions.checkState(!isCreateCalled); + this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; + return this; + } + + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + @Override + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = drmSessionManager; + return this; + } + + /** + * Returns a new {@link ProgressiveMediaSource} using the current parameters. + * + * @param uri The {@link Uri}. + * @return The new {@link ProgressiveMediaSource}. + */ + @Override + public ProgressiveMediaSource createMediaSource(Uri uri) { + isCreateCalled = true; + return new ProgressiveMediaSource( + uri, + dataSourceFactory, + extractorsFactory, + drmSessionManager, + loadErrorHandlingPolicy, + customCacheKey, + continueLoadingCheckIntervalBytes, + tag); + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_OTHER}; + } + } + + /** + * The default number of bytes that should be loaded between each each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + */ + public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024; + + private final Uri uri; + private final DataSource.Factory dataSourceFactory; + private final ExtractorsFactory extractorsFactory; + private final DrmSessionManager drmSessionManager; + private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy; + @Nullable private final String customCacheKey; + private final int continueLoadingCheckIntervalBytes; + @Nullable private final Object tag; + + private long timelineDurationUs; + private boolean timelineIsSeekable; + private boolean timelineIsLive; + @Nullable private TransferListener transferListener; + + // TODO: Make private when ExtractorMediaSource is deleted. + /* package */ ProgressiveMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + DrmSessionManager drmSessionManager, + LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy, + @Nullable String customCacheKey, + int continueLoadingCheckIntervalBytes, + @Nullable Object tag) { + this.uri = uri; + this.dataSourceFactory = dataSourceFactory; + this.extractorsFactory = extractorsFactory; + this.drmSessionManager = drmSessionManager; + this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy; + this.customCacheKey = customCacheKey; + this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; + this.timelineDurationUs = C.TIME_UNSET; + this.tag = tag; + } + + @Override + @Nullable + public Object getTag() { + return tag; + } + + @Override + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + transferListener = mediaTransferListener; + drmSessionManager.prepare(); + notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable, timelineIsLive); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + // Do nothing. + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + DataSource dataSource = dataSourceFactory.createDataSource(); + if (transferListener != null) { + dataSource.addTransferListener(transferListener); + } + return new ProgressiveMediaPeriod( + uri, + dataSource, + extractorsFactory.createExtractors(), + drmSessionManager, + loadableLoadErrorHandlingPolicy, + createEventDispatcher(id), + this, + allocator, + customCacheKey, + continueLoadingCheckIntervalBytes); + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + ((ProgressiveMediaPeriod) mediaPeriod).release(); + } + + @Override + protected void releaseSourceInternal() { + drmSessionManager.release(); + } + + // ProgressiveMediaPeriod.Listener implementation. + + @Override + public void onSourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive) { + // If we already have the duration from a previous source info refresh, use it. + durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs; + if (timelineDurationUs == durationUs + && timelineIsSeekable == isSeekable + && timelineIsLive == isLive) { + // Suppress no-op source info changes. + return; + } + notifySourceInfoRefreshed(durationUs, isSeekable, isLive); + } + + // Internal methods. + + private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive) { + timelineDurationUs = durationUs; + timelineIsSeekable = isSeekable; + timelineIsLive = isLive; + // TODO: Split up isDynamic into multiple fields to indicate which values may change. Then + // indicate that the duration may change until it's known. See [internal: b/69703223]. + refreshSourceInfo( + new SinglePeriodTimeline( + timelineDurationUs, + timelineIsSeekable, + /* isDynamic= */ false, + /* isLive= */ timelineIsLive, + /* manifest= */ null, + tag)); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleDataQueue.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleDataQueue.java new file mode 100644 index 000000000000..81933a468d84 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleDataQueue.java @@ -0,0 +1,472 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.CryptoInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput.CryptoData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue.SampleExtrasHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocation; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** A queue of media sample data. */ +/* package */ class SampleDataQueue { + + private static final int INITIAL_SCRATCH_SIZE = 32; + + private final Allocator allocator; + private final int allocationLength; + private final ParsableByteArray scratch; + + // References into the linked list of allocations. + private AllocationNode firstAllocationNode; + private AllocationNode readAllocationNode; + private AllocationNode writeAllocationNode; + + // Accessed only by the loading thread (or the consuming thread when there is no loading thread). + private long totalBytesWritten; + + public SampleDataQueue(Allocator allocator) { + this.allocator = allocator; + allocationLength = allocator.getIndividualAllocationLength(); + scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); + firstAllocationNode = new AllocationNode(/* startPosition= */ 0, allocationLength); + readAllocationNode = firstAllocationNode; + writeAllocationNode = firstAllocationNode; + } + + // Called by the consuming thread, but only when there is no loading thread. + + /** Clears all sample data. */ + public void reset() { + clearAllocationNodes(firstAllocationNode); + firstAllocationNode = new AllocationNode(0, allocationLength); + readAllocationNode = firstAllocationNode; + writeAllocationNode = firstAllocationNode; + totalBytesWritten = 0; + allocator.trim(); + } + + /** + * Discards sample data bytes from the write side of the queue. + * + * @param totalBytesWritten The reduced total number of bytes written after the samples have been + * discarded, or 0 if the queue is now empty. + */ + public void discardUpstreamSampleBytes(long totalBytesWritten) { + this.totalBytesWritten = totalBytesWritten; + if (this.totalBytesWritten == 0 + || this.totalBytesWritten == firstAllocationNode.startPosition) { + clearAllocationNodes(firstAllocationNode); + firstAllocationNode = new AllocationNode(this.totalBytesWritten, allocationLength); + readAllocationNode = firstAllocationNode; + writeAllocationNode = firstAllocationNode; + } else { + // Find the last node containing at least 1 byte of data that we need to keep. + AllocationNode lastNodeToKeep = firstAllocationNode; + while (this.totalBytesWritten > lastNodeToKeep.endPosition) { + lastNodeToKeep = lastNodeToKeep.next; + } + // Discard all subsequent nodes. + AllocationNode firstNodeToDiscard = lastNodeToKeep.next; + clearAllocationNodes(firstNodeToDiscard); + // Reset the successor of the last node to be an uninitialized node. + lastNodeToKeep.next = new AllocationNode(lastNodeToKeep.endPosition, allocationLength); + // Update writeAllocationNode and readAllocationNode as necessary. + writeAllocationNode = + this.totalBytesWritten == lastNodeToKeep.endPosition + ? lastNodeToKeep.next + : lastNodeToKeep; + if (readAllocationNode == firstNodeToDiscard) { + readAllocationNode = lastNodeToKeep.next; + } + } + } + + // Called by the consuming thread. + + /** Rewinds the read position to the first sample in the queue. */ + public void rewind() { + readAllocationNode = firstAllocationNode; + } + + /** + * Reads data from the rolling buffer to populate a decoder input buffer. + * + * @param buffer The buffer to populate. + * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + */ + public void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { + // Read encryption data if the sample is encrypted. + if (buffer.isEncrypted()) { + readEncryptionData(buffer, extrasHolder); + } + // Read sample data, extracting supplemental data into a separate buffer if needed. + if (buffer.hasSupplementalData()) { + // If there is supplemental data, the sample data is prefixed by its size. + scratch.reset(4); + readData(extrasHolder.offset, scratch.data, 4); + int sampleSize = scratch.readUnsignedIntToInt(); + extrasHolder.offset += 4; + extrasHolder.size -= 4; + + // Write the sample data. + buffer.ensureSpaceForWrite(sampleSize); + readData(extrasHolder.offset, buffer.data, sampleSize); + extrasHolder.offset += sampleSize; + extrasHolder.size -= sampleSize; + + // Write the remaining data as supplemental data. + buffer.resetSupplementalData(extrasHolder.size); + readData(extrasHolder.offset, buffer.supplementalData, extrasHolder.size); + } else { + // Write the sample data. + buffer.ensureSpaceForWrite(extrasHolder.size); + readData(extrasHolder.offset, buffer.data, extrasHolder.size); + } + } + + /** + * Advances the read position to the specified absolute position. + * + * @param absolutePosition The new absolute read position. May be {@link C#POSITION_UNSET}, in + * which case calling this method is a no-op. + */ + public void discardDownstreamTo(long absolutePosition) { + if (absolutePosition == C.POSITION_UNSET) { + return; + } + while (absolutePosition >= firstAllocationNode.endPosition) { + // Advance firstAllocationNode to the specified absolute position. Also clear nodes that are + // advanced past, and return their underlying allocations to the allocator. + allocator.release(firstAllocationNode.allocation); + firstAllocationNode = firstAllocationNode.clear(); + } + if (readAllocationNode.startPosition < firstAllocationNode.startPosition) { + // We discarded the node referenced by readAllocationNode. We need to advance it to the first + // remaining node. + readAllocationNode = firstAllocationNode; + } + } + + // Called by the loading thread. + + public long getTotalBytesWritten() { + return totalBytesWritten; + } + + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + length = preAppend(length); + int bytesAppended = + input.read( + writeAllocationNode.allocation.data, + writeAllocationNode.translateOffset(totalBytesWritten), + length); + if (bytesAppended == C.RESULT_END_OF_INPUT) { + if (allowEndOfInput) { + return C.RESULT_END_OF_INPUT; + } + throw new EOFException(); + } + postAppend(bytesAppended); + return bytesAppended; + } + + public void sampleData(ParsableByteArray buffer, int length) { + while (length > 0) { + int bytesAppended = preAppend(length); + buffer.readBytes( + writeAllocationNode.allocation.data, + writeAllocationNode.translateOffset(totalBytesWritten), + bytesAppended); + length -= bytesAppended; + postAppend(bytesAppended); + } + } + + // Private methods. + + /** + * Reads encryption data for the current sample. + * + *

The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and {@link + * SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The same + * value is added to {@link SampleExtrasHolder#offset}. + * + * @param buffer The buffer into which the encryption data should be written. + * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + */ + private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { + long offset = extrasHolder.offset; + + // Read the signal byte. + scratch.reset(1); + readData(offset, scratch.data, 1); + offset++; + byte signalByte = scratch.data[0]; + boolean subsampleEncryption = (signalByte & 0x80) != 0; + int ivSize = signalByte & 0x7F; + + // Read the initialization vector. + CryptoInfo cryptoInfo = buffer.cryptoInfo; + if (cryptoInfo.iv == null) { + cryptoInfo.iv = new byte[16]; + } else { + // Zero out cryptoInfo.iv so that if ivSize < 16, the remaining bytes are correctly set to 0. + Arrays.fill(cryptoInfo.iv, (byte) 0); + } + readData(offset, cryptoInfo.iv, ivSize); + offset += ivSize; + + // Read the subsample count, if present. + int subsampleCount; + if (subsampleEncryption) { + scratch.reset(2); + readData(offset, scratch.data, 2); + offset += 2; + subsampleCount = scratch.readUnsignedShort(); + } else { + subsampleCount = 1; + } + + // Write the clear and encrypted subsample sizes. + @Nullable int[] clearDataSizes = cryptoInfo.numBytesOfClearData; + if (clearDataSizes == null || clearDataSizes.length < subsampleCount) { + clearDataSizes = new int[subsampleCount]; + } + @Nullable int[] encryptedDataSizes = cryptoInfo.numBytesOfEncryptedData; + if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) { + encryptedDataSizes = new int[subsampleCount]; + } + if (subsampleEncryption) { + int subsampleDataLength = 6 * subsampleCount; + scratch.reset(subsampleDataLength); + readData(offset, scratch.data, subsampleDataLength); + offset += subsampleDataLength; + scratch.setPosition(0); + for (int i = 0; i < subsampleCount; i++) { + clearDataSizes[i] = scratch.readUnsignedShort(); + encryptedDataSizes[i] = scratch.readUnsignedIntToInt(); + } + } else { + clearDataSizes[0] = 0; + encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset); + } + + // Populate the cryptoInfo. + CryptoData cryptoData = extrasHolder.cryptoData; + cryptoInfo.set( + subsampleCount, + clearDataSizes, + encryptedDataSizes, + cryptoData.encryptionKey, + cryptoInfo.iv, + cryptoData.cryptoMode, + cryptoData.encryptedBlocks, + cryptoData.clearBlocks); + + // Adjust the offset and size to take into account the bytes read. + int bytesRead = (int) (offset - extrasHolder.offset); + extrasHolder.offset += bytesRead; + extrasHolder.size -= bytesRead; + } + + /** + * Reads data from the front of the rolling buffer. + * + * @param absolutePosition The absolute position from which data should be read. + * @param target The buffer into which data should be written. + * @param length The number of bytes to read. + */ + private void readData(long absolutePosition, ByteBuffer target, int length) { + advanceReadTo(absolutePosition); + int remaining = length; + while (remaining > 0) { + int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); + Allocation allocation = readAllocationNode.allocation; + target.put(allocation.data, readAllocationNode.translateOffset(absolutePosition), toCopy); + remaining -= toCopy; + absolutePosition += toCopy; + if (absolutePosition == readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; + } + } + } + + /** + * Reads data from the front of the rolling buffer. + * + * @param absolutePosition The absolute position from which data should be read. + * @param target The array into which data should be written. + * @param length The number of bytes to read. + */ + private void readData(long absolutePosition, byte[] target, int length) { + advanceReadTo(absolutePosition); + int remaining = length; + while (remaining > 0) { + int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); + Allocation allocation = readAllocationNode.allocation; + System.arraycopy( + allocation.data, + readAllocationNode.translateOffset(absolutePosition), + target, + length - remaining, + toCopy); + remaining -= toCopy; + absolutePosition += toCopy; + if (absolutePosition == readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; + } + } + } + + /** + * Advances the read position to the specified absolute position. + * + * @param absolutePosition The position to which {@link #readAllocationNode} should be advanced. + */ + private void advanceReadTo(long absolutePosition) { + while (absolutePosition >= readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; + } + } + + /** + * Clears allocation nodes starting from {@code fromNode}. + * + * @param fromNode The node from which to clear. + */ + private void clearAllocationNodes(AllocationNode fromNode) { + if (!fromNode.wasInitialized) { + return; + } + // Bulk release allocations for performance (it's significantly faster when using + // DefaultAllocator because the allocator's lock only needs to be acquired and released once) + // [Internal: See b/29542039]. + int allocationCount = + (writeAllocationNode.wasInitialized ? 1 : 0) + + ((int) (writeAllocationNode.startPosition - fromNode.startPosition) + / allocationLength); + Allocation[] allocationsToRelease = new Allocation[allocationCount]; + AllocationNode currentNode = fromNode; + for (int i = 0; i < allocationsToRelease.length; i++) { + allocationsToRelease[i] = currentNode.allocation; + currentNode = currentNode.clear(); + } + allocator.release(allocationsToRelease); + } + + /** + * Called before writing sample data to {@link #writeAllocationNode}. May cause {@link + * #writeAllocationNode} to be initialized. + * + * @param length The number of bytes that the caller wishes to write. + * @return The number of bytes that the caller is permitted to write, which may be less than + * {@code length}. + */ + private int preAppend(int length) { + if (!writeAllocationNode.wasInitialized) { + writeAllocationNode.initialize( + allocator.allocate(), + new AllocationNode(writeAllocationNode.endPosition, allocationLength)); + } + return Math.min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten)); + } + + /** + * Called after writing sample data. May cause {@link #writeAllocationNode} to be advanced. + * + * @param length The number of bytes that were written. + */ + private void postAppend(int length) { + totalBytesWritten += length; + if (totalBytesWritten == writeAllocationNode.endPosition) { + writeAllocationNode = writeAllocationNode.next; + } + } + + /** A node in a linked list of {@link Allocation}s held by the output. */ + private static final class AllocationNode { + + /** The absolute position of the start of the data (inclusive). */ + public final long startPosition; + /** The absolute position of the end of the data (exclusive). */ + public final long endPosition; + /** Whether the node has been initialized. Remains true after {@link #clear()}. */ + public boolean wasInitialized; + /** The {@link Allocation}, or {@code null} if the node is not initialized. */ + @Nullable public Allocation allocation; + /** + * The next {@link AllocationNode} in the list, or {@code null} if the node has not been + * initialized. Remains set after {@link #clear()}. + */ + @Nullable public AllocationNode next; + + /** + * @param startPosition See {@link #startPosition}. + * @param allocationLength The length of the {@link Allocation} with which this node will be + * initialized. + */ + public AllocationNode(long startPosition, int allocationLength) { + this.startPosition = startPosition; + this.endPosition = startPosition + allocationLength; + } + + /** + * Initializes the node. + * + * @param allocation The node's {@link Allocation}. + * @param next The next {@link AllocationNode}. + */ + public void initialize(Allocation allocation, AllocationNode next) { + this.allocation = allocation; + this.next = next; + wasInitialized = true; + } + + /** + * Gets the offset into the {@link #allocation}'s {@link Allocation#data} that corresponds to + * the specified absolute position. + * + * @param absolutePosition The absolute position. + * @return The corresponding offset into the allocation's data. + */ + public int translateOffset(long absolutePosition) { + return (int) (absolutePosition - startPosition) + allocation.offset; + } + + /** + * Clears {@link #allocation} and {@link #next}. + * + * @return The cleared next {@link AllocationNode}. + */ + public AllocationNode clear() { + allocation = null; + AllocationNode temp = next; + next = null; + return temp; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleQueue.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleQueue.java new file mode 100644 index 000000000000..639cccee009f --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleQueue.java @@ -0,0 +1,926 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +import android.os.Looper; +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** A queue of media samples. */ +public class SampleQueue implements TrackOutput { + + /** A listener for changes to the upstream format. */ + public interface UpstreamFormatChangedListener { + + /** + * Called on the loading thread when an upstream format change occurs. + * + * @param format The new upstream format. + */ + void onUpstreamFormatChanged(Format format); + } + + @VisibleForTesting /* package */ static final int SAMPLE_CAPACITY_INCREMENT = 1000; + + private final SampleDataQueue sampleDataQueue; + private final SampleExtrasHolder extrasHolder; + private final DrmSessionManager drmSessionManager; + private UpstreamFormatChangedListener upstreamFormatChangeListener; + + @Nullable private Format downstreamFormat; + @Nullable private DrmSession currentDrmSession; + + private int capacity; + private int[] sourceIds; + private long[] offsets; + private int[] sizes; + private int[] flags; + private long[] timesUs; + private CryptoData[] cryptoDatas; + private Format[] formats; + + private int length; + private int absoluteFirstIndex; + private int relativeFirstIndex; + private int readPosition; + + private long largestDiscardedTimestampUs; + private long largestQueuedTimestampUs; + private boolean isLastSampleQueued; + private boolean upstreamKeyframeRequired; + private boolean upstreamFormatRequired; + private Format upstreamFormat; + private Format upstreamCommittedFormat; + private int upstreamSourceId; + + private boolean pendingUpstreamFormatAdjustment; + private Format unadjustedUpstreamFormat; + private long sampleOffsetUs; + private boolean pendingSplice; + + /** + * Creates a sample queue. + * + * @param allocator An {@link Allocator} from which allocations for sample data can be obtained. + * @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions} + * from. The created instance does not take ownership of this {@link DrmSessionManager}. + */ + public SampleQueue(Allocator allocator, DrmSessionManager drmSessionManager) { + sampleDataQueue = new SampleDataQueue(allocator); + this.drmSessionManager = drmSessionManager; + extrasHolder = new SampleExtrasHolder(); + capacity = SAMPLE_CAPACITY_INCREMENT; + sourceIds = new int[capacity]; + offsets = new long[capacity]; + timesUs = new long[capacity]; + flags = new int[capacity]; + sizes = new int[capacity]; + cryptoDatas = new CryptoData[capacity]; + formats = new Format[capacity]; + largestDiscardedTimestampUs = Long.MIN_VALUE; + largestQueuedTimestampUs = Long.MIN_VALUE; + upstreamFormatRequired = true; + upstreamKeyframeRequired = true; + } + + // Called by the consuming thread when there is no loading thread. + + /** Calls {@link #reset(boolean) reset(true)} and releases any resources owned by the queue. */ + @CallSuper + public void release() { + reset(/* resetUpstreamFormat= */ true); + releaseDrmSessionReferences(); + } + + /** Convenience method for {@code reset(false)}. */ + public final void reset() { + reset(/* resetUpstreamFormat= */ false); + } + + /** + * Clears all samples from the queue. + * + * @param resetUpstreamFormat Whether the upstream format should be cleared. If set to false, + * samples queued after the reset (and before a subsequent call to {@link #format(Format)}) + * are assumed to have the current upstream format. If set to true, {@link #format(Format)} + * must be called after the reset before any more samples can be queued. + */ + @CallSuper + public void reset(boolean resetUpstreamFormat) { + sampleDataQueue.reset(); + length = 0; + absoluteFirstIndex = 0; + relativeFirstIndex = 0; + readPosition = 0; + upstreamKeyframeRequired = true; + largestDiscardedTimestampUs = Long.MIN_VALUE; + largestQueuedTimestampUs = Long.MIN_VALUE; + isLastSampleQueued = false; + upstreamCommittedFormat = null; + if (resetUpstreamFormat) { + unadjustedUpstreamFormat = null; + upstreamFormat = null; + upstreamFormatRequired = true; + } + } + + /** + * Sets a source identifier for subsequent samples. + * + * @param sourceId The source identifier. + */ + public final void sourceId(int sourceId) { + upstreamSourceId = sourceId; + } + + /** Indicates samples that are subsequently queued should be spliced into those already queued. */ + public final void splice() { + pendingSplice = true; + } + + /** Returns the current absolute write index. */ + public final int getWriteIndex() { + return absoluteFirstIndex + length; + } + + /** + * Discards samples from the write side of the queue. + * + * @param discardFromIndex The absolute index of the first sample to be discarded. Must be in the + * range [{@link #getReadIndex()}, {@link #getWriteIndex()}]. + */ + public final void discardUpstreamSamples(int discardFromIndex) { + sampleDataQueue.discardUpstreamSampleBytes(discardUpstreamSampleMetadata(discardFromIndex)); + } + + // Called by the consuming thread. + + /** Calls {@link #discardToEnd()} and releases any resources owned by the queue. */ + @CallSuper + public void preRelease() { + discardToEnd(); + releaseDrmSessionReferences(); + } + + /** + * Throws an error that's preventing data from being read. Does nothing if no such error exists. + * + * @throws IOException The underlying error. + */ + @CallSuper + public void maybeThrowError() throws IOException { + // TODO: Avoid throwing if the DRM error is not preventing a read operation. + if (currentDrmSession != null && currentDrmSession.getState() == DrmSession.STATE_ERROR) { + throw Assertions.checkNotNull(currentDrmSession.getError()); + } + } + + /** Returns the current absolute start index. */ + public final int getFirstIndex() { + return absoluteFirstIndex; + } + + /** Returns the current absolute read index. */ + public final int getReadIndex() { + return absoluteFirstIndex + readPosition; + } + + /** + * Peeks the source id of the next sample to be read, or the current upstream source id if the + * queue is empty or if the read position is at the end of the queue. + * + * @return The source id. + */ + public final synchronized int peekSourceId() { + int relativeReadIndex = getRelativeIndex(readPosition); + return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId; + } + + /** Returns the upstream {@link Format} in which samples are being queued. */ + public final synchronized Format getUpstreamFormat() { + return upstreamFormatRequired ? null : upstreamFormat; + } + + /** + * Returns the largest sample timestamp that has been queued since the last {@link #reset}. + * + *

Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not + * considered as having been queued. Samples that were dequeued from the front of the queue are + * considered as having been queued. + * + * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no + * samples have been queued. + */ + public final synchronized long getLargestQueuedTimestampUs() { + return largestQueuedTimestampUs; + } + + /** + * Returns whether the last sample of the stream has knowingly been queued. A return value of + * {@code false} means that the last sample had not been queued or that it's unknown whether the + * last sample has been queued. + * + *

Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not + * considered as having been queued. Samples that were dequeued from the front of the queue are + * considered as having been queued. + */ + public final synchronized boolean isLastSampleQueued() { + return isLastSampleQueued; + } + + /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ + public final synchronized long getFirstTimestampUs() { + return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex]; + } + + /** + * Returns whether there is data available for reading. + * + *

Note: If the stream has ended then a buffer with the end of stream flag can always be read + * from {@link #read}. Hence an ended stream is always ready. + * + * @param loadingFinished Whether no more samples will be written to the sample queue. When true, + * this method returns true if the sample queue is empty, because an empty sample queue means + * the end of stream has been reached. When false, this method returns false if the sample + * queue is empty. + */ + @SuppressWarnings("ReferenceEquality") // See comments in setUpstreamFormat + @CallSuper + public synchronized boolean isReady(boolean loadingFinished) { + if (!hasNextSample()) { + return loadingFinished + || isLastSampleQueued + || (upstreamFormat != null && upstreamFormat != downstreamFormat); + } + int relativeReadIndex = getRelativeIndex(readPosition); + if (formats[relativeReadIndex] != downstreamFormat) { + // A format can be read. + return true; + } + return mayReadSample(relativeReadIndex); + } + + /** + * Attempts to read from the queue. + * + *

{@link Format Formats} read from this method may be associated to a {@link DrmSession} + * through {@link FormatHolder#drmSession}, which is populated in two scenarios: + * + *

    + *
  • The {@link Format} has a non-null {@link Format#drmInitData}. + *
  • The {@link DrmSessionManager} provides placeholder sessions for this queue's track type. + * See {@link DrmSessionManager#acquirePlaceholderSession(Looper, int)}. + *
+ * + * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. + * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the + * end of the stream. If the end of the stream has been reached, the {@link + * C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. If a {@link + * DecoderInputBuffer#isFlagsOnly() flags-only} buffer is passed, only the buffer flags may be + * populated by this method and the read position of the queue will not change. + * @param formatRequired Whether the caller requires that the format of the stream be read even if + * it's not changing. A sample will never be read if set to true, however it is still possible + * for the end of stream or nothing to be read. + * @param loadingFinished True if an empty queue should be considered the end of the stream. + * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will + * be set if the buffer's timestamp is less than this value. + * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or + * {@link C#RESULT_BUFFER_READ}. + */ + @CallSuper + public int read( + FormatHolder formatHolder, + DecoderInputBuffer buffer, + boolean formatRequired, + boolean loadingFinished, + long decodeOnlyUntilUs) { + int result = + readSampleMetadata( + formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilUs, extrasHolder); + if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream() && !buffer.isFlagsOnly()) { + sampleDataQueue.readToBuffer(buffer, extrasHolder); + } + return result; + } + + /** + * Attempts to seek the read position to the specified sample index. + * + * @param sampleIndex The sample index. + * @return Whether the seek was successful. + */ + public final synchronized boolean seekTo(int sampleIndex) { + rewind(); + if (sampleIndex < absoluteFirstIndex || sampleIndex > absoluteFirstIndex + length) { + return false; + } + readPosition = sampleIndex - absoluteFirstIndex; + return true; + } + + /** + * Attempts to seek the read position to the keyframe before or at the specified time. + * + * @param timeUs The time to seek to. + * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the + * end of the queue, by seeking to the last sample (or keyframe). + * @return Whether the seek was successful. + */ + public final synchronized boolean seekTo(long timeUs, boolean allowTimeBeyondBuffer) { + rewind(); + int relativeReadIndex = getRelativeIndex(readPosition); + if (!hasNextSample() + || timeUs < timesUs[relativeReadIndex] + || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) { + return false; + } + int offset = + findSampleBefore(relativeReadIndex, length - readPosition, timeUs, /* keyframe= */ true); + if (offset == -1) { + return false; + } + readPosition += offset; + return true; + } + + /** + * Advances the read position to the keyframe before or at the specified time. + * + * @param timeUs The time to advance to. + * @return The number of samples that were skipped, which may be equal to 0. + */ + public final synchronized int advanceTo(long timeUs) { + int relativeReadIndex = getRelativeIndex(readPosition); + if (!hasNextSample() || timeUs < timesUs[relativeReadIndex]) { + return 0; + } + int offset = + findSampleBefore(relativeReadIndex, length - readPosition, timeUs, /* keyframe= */ true); + if (offset == -1) { + return 0; + } + readPosition += offset; + return offset; + } + + /** + * Advances the read position to the end of the queue. + * + * @return The number of samples that were skipped. + */ + public final synchronized int advanceToEnd() { + int skipCount = length - readPosition; + readPosition = length; + return skipCount; + } + + /** + * Discards up to but not including the sample immediately before or at the specified time. + * + * @param timeUs The time to discard up to. + * @param toKeyframe If true then discards samples up to the keyframe before or at the specified + * time, rather than any sample before or at that time. + * @param stopAtReadPosition If true then samples are only discarded if they're before the read + * position. If false then samples at and beyond the read position may be discarded, in which + * case the read position is advanced to the first remaining sample. + */ + public final void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { + sampleDataQueue.discardDownstreamTo( + discardSampleMetadataTo(timeUs, toKeyframe, stopAtReadPosition)); + } + + /** Discards up to but not including the read position. */ + public final void discardToRead() { + sampleDataQueue.discardDownstreamTo(discardSampleMetadataToRead()); + } + + /** Discards all samples in the queue and advances the read position. */ + public final void discardToEnd() { + sampleDataQueue.discardDownstreamTo(discardSampleMetadataToEnd()); + } + + // Called by the loading thread. + + /** + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples that + * are subsequently queued. + * + * @param sampleOffsetUs The timestamp offset in microseconds. + */ + public final void setSampleOffsetUs(long sampleOffsetUs) { + if (this.sampleOffsetUs != sampleOffsetUs) { + this.sampleOffsetUs = sampleOffsetUs; + invalidateUpstreamFormatAdjustment(); + } + } + + /** + * Sets a listener to be notified of changes to the upstream format. + * + * @param listener The listener. + */ + public final void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) { + upstreamFormatChangeListener = listener; + } + + // TrackOutput implementation. Called by the loading thread. + + @Override + public final void format(Format unadjustedUpstreamFormat) { + Format adjustedUpstreamFormat = getAdjustedUpstreamFormat(unadjustedUpstreamFormat); + pendingUpstreamFormatAdjustment = false; + this.unadjustedUpstreamFormat = unadjustedUpstreamFormat; + boolean upstreamFormatChanged = setUpstreamFormat(adjustedUpstreamFormat); + if (upstreamFormatChangeListener != null && upstreamFormatChanged) { + upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedUpstreamFormat); + } + } + + @Override + public final int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + return sampleDataQueue.sampleData(input, length, allowEndOfInput); + } + + @Override + public final void sampleData(ParsableByteArray buffer, int length) { + sampleDataQueue.sampleData(buffer, length); + } + + @Override + public final void sampleMetadata( + long timeUs, + @C.BufferFlags int flags, + int size, + int offset, + @Nullable CryptoData cryptoData) { + if (pendingUpstreamFormatAdjustment) { + format(unadjustedUpstreamFormat); + } + timeUs += sampleOffsetUs; + if (pendingSplice) { + if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !attemptSplice(timeUs)) { + return; + } + pendingSplice = false; + } + long absoluteOffset = sampleDataQueue.getTotalBytesWritten() - size - offset; + commitSample(timeUs, flags, absoluteOffset, size, cryptoData); + } + + /** + * Invalidates the last upstream format adjustment. {@link #getAdjustedUpstreamFormat(Format)} + * will be called to adjust the upstream {@link Format} again before the next sample is queued. + */ + protected final void invalidateUpstreamFormatAdjustment() { + pendingUpstreamFormatAdjustment = true; + } + + /** + * Adjusts the upstream {@link Format} (i.e., the {@link Format} that was most recently passed to + * {@link #format(Format)}). + * + *

The default implementation incorporates the sample offset passed to {@link + * #setSampleOffsetUs(long)} into {@link Format#subsampleOffsetUs}. + * + * @param format The {@link Format} to adjust. + * @return The adjusted {@link Format}. + */ + @CallSuper + protected Format getAdjustedUpstreamFormat(Format format) { + if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) { + format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs); + } + return format; + } + + // Internal methods. + + /** Rewinds the read position to the first sample in the queue. */ + private synchronized void rewind() { + readPosition = 0; + sampleDataQueue.rewind(); + } + + @SuppressWarnings("ReferenceEquality") // See comments in setUpstreamFormat + private synchronized int readSampleMetadata( + FormatHolder formatHolder, + DecoderInputBuffer buffer, + boolean formatRequired, + boolean loadingFinished, + long decodeOnlyUntilUs, + SampleExtrasHolder extrasHolder) { + buffer.waitingForKeys = false; + // This is a temporary fix for https://github.com/google/ExoPlayer/issues/6155. + // TODO: Remove it and replace it with a fix that discards samples when writing to the queue. + boolean hasNextSample; + int relativeReadIndex = C.INDEX_UNSET; + while ((hasNextSample = hasNextSample())) { + relativeReadIndex = getRelativeIndex(readPosition); + long timeUs = timesUs[relativeReadIndex]; + if (timeUs < decodeOnlyUntilUs + && MimeTypes.allSamplesAreSyncSamples(formats[relativeReadIndex].sampleMimeType)) { + readPosition++; + } else { + break; + } + } + + if (!hasNextSample) { + if (loadingFinished || isLastSampleQueued) { + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } else if (upstreamFormat != null && (formatRequired || upstreamFormat != downstreamFormat)) { + onFormatResult(Assertions.checkNotNull(upstreamFormat), formatHolder); + return C.RESULT_FORMAT_READ; + } else { + return C.RESULT_NOTHING_READ; + } + } + + if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { + onFormatResult(formats[relativeReadIndex], formatHolder); + return C.RESULT_FORMAT_READ; + } + + if (!mayReadSample(relativeReadIndex)) { + buffer.waitingForKeys = true; + return C.RESULT_NOTHING_READ; + } + + buffer.setFlags(flags[relativeReadIndex]); + buffer.timeUs = timesUs[relativeReadIndex]; + if (buffer.timeUs < decodeOnlyUntilUs) { + buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); + } + if (buffer.isFlagsOnly()) { + return C.RESULT_BUFFER_READ; + } + extrasHolder.size = sizes[relativeReadIndex]; + extrasHolder.offset = offsets[relativeReadIndex]; + extrasHolder.cryptoData = cryptoDatas[relativeReadIndex]; + + readPosition++; + return C.RESULT_BUFFER_READ; + } + + private synchronized boolean setUpstreamFormat(Format format) { + if (format == null) { + upstreamFormatRequired = true; + return false; + } + upstreamFormatRequired = false; + if (Util.areEqual(format, upstreamFormat)) { + // The format is unchanged. If format and upstreamFormat are different objects, we keep the + // current upstreamFormat so we can detect format changes on the read side using cheap + // referential quality. + return false; + } else if (Util.areEqual(format, upstreamCommittedFormat)) { + // The format has changed back to the format of the last committed sample. If they are + // different objects, we revert back to using upstreamCommittedFormat as the upstreamFormat + // so we can detect format changes on the read side using cheap referential equality. + upstreamFormat = upstreamCommittedFormat; + return true; + } else { + upstreamFormat = format; + return true; + } + } + + private synchronized long discardSampleMetadataTo( + long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { + if (length == 0 || timeUs < timesUs[relativeFirstIndex]) { + return C.POSITION_UNSET; + } + int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length; + int discardCount = findSampleBefore(relativeFirstIndex, searchLength, timeUs, toKeyframe); + if (discardCount == -1) { + return C.POSITION_UNSET; + } + return discardSamples(discardCount); + } + + public synchronized long discardSampleMetadataToRead() { + if (readPosition == 0) { + return C.POSITION_UNSET; + } + return discardSamples(readPosition); + } + + private synchronized long discardSampleMetadataToEnd() { + if (length == 0) { + return C.POSITION_UNSET; + } + return discardSamples(length); + } + + private void releaseDrmSessionReferences() { + if (currentDrmSession != null) { + currentDrmSession.release(); + currentDrmSession = null; + // Clear downstream format to avoid violating the assumption that downstreamFormat.drmInitData + // != null implies currentSession != null + downstreamFormat = null; + } + } + + private synchronized void commitSample( + long timeUs, @C.BufferFlags int sampleFlags, long offset, int size, CryptoData cryptoData) { + if (upstreamKeyframeRequired) { + if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) { + return; + } + upstreamKeyframeRequired = false; + } + Assertions.checkState(!upstreamFormatRequired); + + isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0; + largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); + + int relativeEndIndex = getRelativeIndex(length); + timesUs[relativeEndIndex] = timeUs; + offsets[relativeEndIndex] = offset; + sizes[relativeEndIndex] = size; + flags[relativeEndIndex] = sampleFlags; + cryptoDatas[relativeEndIndex] = cryptoData; + formats[relativeEndIndex] = upstreamFormat; + sourceIds[relativeEndIndex] = upstreamSourceId; + upstreamCommittedFormat = upstreamFormat; + + length++; + if (length == capacity) { + // Increase the capacity. + int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT; + int[] newSourceIds = new int[newCapacity]; + long[] newOffsets = new long[newCapacity]; + long[] newTimesUs = new long[newCapacity]; + int[] newFlags = new int[newCapacity]; + int[] newSizes = new int[newCapacity]; + CryptoData[] newCryptoDatas = new CryptoData[newCapacity]; + Format[] newFormats = new Format[newCapacity]; + int beforeWrap = capacity - relativeFirstIndex; + System.arraycopy(offsets, relativeFirstIndex, newOffsets, 0, beforeWrap); + System.arraycopy(timesUs, relativeFirstIndex, newTimesUs, 0, beforeWrap); + System.arraycopy(flags, relativeFirstIndex, newFlags, 0, beforeWrap); + System.arraycopy(sizes, relativeFirstIndex, newSizes, 0, beforeWrap); + System.arraycopy(cryptoDatas, relativeFirstIndex, newCryptoDatas, 0, beforeWrap); + System.arraycopy(formats, relativeFirstIndex, newFormats, 0, beforeWrap); + System.arraycopy(sourceIds, relativeFirstIndex, newSourceIds, 0, beforeWrap); + int afterWrap = relativeFirstIndex; + System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap); + System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap); + System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); + System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap); + System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap); + System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap); + System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap); + offsets = newOffsets; + timesUs = newTimesUs; + flags = newFlags; + sizes = newSizes; + cryptoDatas = newCryptoDatas; + formats = newFormats; + sourceIds = newSourceIds; + relativeFirstIndex = 0; + capacity = newCapacity; + } + } + + /** + * Attempts to discard samples from the end of the queue to allow samples starting from the + * specified timestamp to be spliced in. Samples will not be discarded prior to the read position. + * + * @param timeUs The timestamp at which the splice occurs. + * @return Whether the splice was successful. + */ + private synchronized boolean attemptSplice(long timeUs) { + if (length == 0) { + return timeUs > largestDiscardedTimestampUs; + } + long largestReadTimestampUs = + Math.max(largestDiscardedTimestampUs, getLargestTimestamp(readPosition)); + if (largestReadTimestampUs >= timeUs) { + return false; + } + int retainCount = length; + int relativeSampleIndex = getRelativeIndex(length - 1); + while (retainCount > readPosition && timesUs[relativeSampleIndex] >= timeUs) { + retainCount--; + relativeSampleIndex--; + if (relativeSampleIndex == -1) { + relativeSampleIndex = capacity - 1; + } + } + discardUpstreamSampleMetadata(absoluteFirstIndex + retainCount); + return true; + } + + private long discardUpstreamSampleMetadata(int discardFromIndex) { + int discardCount = getWriteIndex() - discardFromIndex; + Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition)); + length -= discardCount; + largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length)); + isLastSampleQueued = discardCount == 0 && isLastSampleQueued; + if (length != 0) { + int relativeLastWriteIndex = getRelativeIndex(length - 1); + return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex]; + } + return 0; + } + + private boolean hasNextSample() { + return readPosition != length; + } + + /** + * Sets the downstream format, performs DRM resource management, and populates the {@code + * outputFormatHolder}. + * + * @param newFormat The new downstream format. + * @param outputFormatHolder The output {@link FormatHolder}. + */ + private void onFormatResult(Format newFormat, FormatHolder outputFormatHolder) { + outputFormatHolder.format = newFormat; + boolean isFirstFormat = downstreamFormat == null; + DrmInitData oldDrmInitData = isFirstFormat ? null : downstreamFormat.drmInitData; + downstreamFormat = newFormat; + if (drmSessionManager == DrmSessionManager.DUMMY) { + // Avoid attempting to acquire a session using the dummy DRM session manager. It's likely that + // the media source creation has not yet been migrated and the renderer can acquire the + // session for the read DRM init data. + // TODO: Remove once renderers are migrated [Internal ref: b/122519809]. + return; + } + DrmInitData newDrmInitData = newFormat.drmInitData; + outputFormatHolder.includesDrmSession = true; + outputFormatHolder.drmSession = currentDrmSession; + if (!isFirstFormat && Util.areEqual(oldDrmInitData, newDrmInitData)) { + // Nothing to do. + return; + } + // Ensure we acquire the new session before releasing the previous one in case the same session + // is being used for both DrmInitData. + DrmSession previousSession = currentDrmSession; + Looper playbackLooper = Assertions.checkNotNull(Looper.myLooper()); + currentDrmSession = + newDrmInitData != null + ? drmSessionManager.acquireSession(playbackLooper, newDrmInitData) + : drmSessionManager.acquirePlaceholderSession( + playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType)); + outputFormatHolder.drmSession = currentDrmSession; + + if (previousSession != null) { + previousSession.release(); + } + } + + /** + * Returns whether it's possible to read the next sample. + * + * @param relativeReadIndex The relative read index of the next sample. + * @return Whether it's possible to read the next sample. + */ + private boolean mayReadSample(int relativeReadIndex) { + if (drmSessionManager == DrmSessionManager.DUMMY) { + // TODO: Remove once renderers are migrated [Internal ref: b/122519809]. + // For protected content it's likely that the DrmSessionManager is still being injected into + // the renderers. We assume that the renderers will be able to acquire a DrmSession if needed. + return true; + } + return currentDrmSession == null + || currentDrmSession.getState() == DrmSession.STATE_OPENED_WITH_KEYS + || ((flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) == 0 + && currentDrmSession.playClearSamplesWithoutKeys()); + } + + /** + * Finds the sample in the specified range that's before or at the specified time. If {@code + * keyframe} is {@code true} then the sample is additionally required to be a keyframe. + * + * @param relativeStartIndex The relative index from which to start searching. + * @param length The length of the range being searched. + * @param timeUs The specified time. + * @param keyframe Whether only keyframes should be considered. + * @return The offset from {@code relativeFirstIndex} to the found sample, or -1 if no matching + * sample was found. + */ + private int findSampleBefore(int relativeStartIndex, int length, long timeUs, boolean keyframe) { + // This could be optimized to use a binary search, however in practice callers to this method + // normally pass times near to the start of the search region. Hence it's unclear whether + // switching to a binary search would yield any real benefit. + int sampleCountToTarget = -1; + int searchIndex = relativeStartIndex; + for (int i = 0; i < length && timesUs[searchIndex] <= timeUs; i++) { + if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + // We've found a suitable sample. + sampleCountToTarget = i; + } + searchIndex++; + if (searchIndex == capacity) { + searchIndex = 0; + } + } + return sampleCountToTarget; + } + + /** + * Discards the specified number of samples. + * + * @param discardCount The number of samples to discard. + * @return The corresponding offset up to which data should be discarded. + */ + private long discardSamples(int discardCount) { + largestDiscardedTimestampUs = + Math.max(largestDiscardedTimestampUs, getLargestTimestamp(discardCount)); + length -= discardCount; + absoluteFirstIndex += discardCount; + relativeFirstIndex += discardCount; + if (relativeFirstIndex >= capacity) { + relativeFirstIndex -= capacity; + } + readPosition -= discardCount; + if (readPosition < 0) { + readPosition = 0; + } + if (length == 0) { + int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1; + return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex]; + } else { + return offsets[relativeFirstIndex]; + } + } + + /** + * Finds the largest timestamp of any sample from the start of the queue up to the specified + * length, assuming that the timestamps prior to a keyframe are always less than the timestamp of + * the keyframe itself, and of subsequent frames. + * + * @param length The length of the range being searched. + * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length == 0}. + */ + private long getLargestTimestamp(int length) { + if (length == 0) { + return Long.MIN_VALUE; + } + long largestTimestampUs = Long.MIN_VALUE; + int relativeSampleIndex = getRelativeIndex(length - 1); + for (int i = 0; i < length; i++) { + largestTimestampUs = Math.max(largestTimestampUs, timesUs[relativeSampleIndex]); + if ((flags[relativeSampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + break; + } + relativeSampleIndex--; + if (relativeSampleIndex == -1) { + relativeSampleIndex = capacity - 1; + } + } + return largestTimestampUs; + } + + /** + * Returns the relative index for a given offset from the start of the queue. + * + * @param offset The offset, which must be in the range [0, length]. + */ + private int getRelativeIndex(int offset) { + int relativeIndex = relativeFirstIndex + offset; + return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity; + } + + /** A holder for sample metadata not held by {@link DecoderInputBuffer}. */ + /* package */ static final class SampleExtrasHolder { + + public int size; + public long offset; + public CryptoData cryptoData; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleStream.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleStream.java index 19fce8581bd0..54a7d0f895a3 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleStream.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleStream.java @@ -45,18 +45,20 @@ public interface SampleStream { /** * Attempts to read from the stream. - *

- * If the stream has ended then {@link C#BUFFER_FLAG_END_OF_STREAM} flag is set on {@code buffer} - * and {@link C#RESULT_BUFFER_READ} is returned. Else if no data is available then - * {@link C#RESULT_NOTHING_READ} is returned. Else if the format of the media is changing or if - * {@code formatRequired} is set then {@code formatHolder} is populated and - * {@link C#RESULT_FORMAT_READ} is returned. Else {@code buffer} is populated and - * {@link C#RESULT_BUFFER_READ} is returned. + * + *

If the stream has ended then {@link C#BUFFER_FLAG_END_OF_STREAM} flag is set on {@code + * buffer} and {@link C#RESULT_BUFFER_READ} is returned. Else if no data is available then {@link + * C#RESULT_NOTHING_READ} is returned. Else if the format of the media is changing or if {@code + * formatRequired} is set then {@code formatHolder} is populated and {@link C#RESULT_FORMAT_READ} + * is returned. Else {@code buffer} is populated and {@link C#RESULT_BUFFER_READ} is returned. * * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the - * end of the stream. If the end of the stream has been reached, the - * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. + * end of the stream. If the end of the stream has been reached, the {@link + * C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. If a {@link + * DecoderInputBuffer#isFlagsOnly() flags-only} buffer is passed, then no {@link + * DecoderInputBuffer#data} will be read and the read position of the stream will not change, + * but the flags of the buffer will be populated. * @param formatRequired Whether the caller requires that the format of the stream be read even if * it's not changing. A sample will never be read if set to true, however it is still possible * for the end of stream or nothing to be read. @@ -70,7 +72,8 @@ public interface SampleStream { * {@code positionUs} is beyond it. * * @param positionUs The specified time. + * @return The number of samples that were skipped. */ - void skipData(long positionUs); + int skipData(long positionUs); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SequenceableLoader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SequenceableLoader.java index d2ba17703e89..09cb8b663b5e 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SequenceableLoader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SequenceableLoader.java @@ -36,6 +36,14 @@ public interface SequenceableLoader { } + /** + * Returns an estimate of the position up to which data is buffered. + * + * @return An estimate of the absolute position in microseconds up to which data is buffered, or + * {@link C#TIME_END_OF_SOURCE} if the data is fully buffered. + */ + long getBufferedPositionUs(); + /** * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished. */ @@ -44,10 +52,26 @@ public interface SequenceableLoader { /** * Attempts to continue loading. * - * @param positionUs The current playback position. + * @param positionUs The current playback position in microseconds. If playback of the period to + * which this loader belongs has not yet started, the value will be the starting position + * in the period minus the duration of any media in previous periods still to be played. * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return * a different value than prior to the call. False otherwise. */ boolean continueLoading(long positionUs); + /** Returns whether the loader is currently loading. */ + boolean isLoading(); + + /** + * Re-evaluates the buffer given the playback position. + * + *

Re-evaluation may discard buffered media so that it can be re-buffered in a different + * quality. + * + * @param positionUs The current playback position in microseconds. If playback of this period has + * not yet started, the value will be the starting position in this period minus the duration + * of any media in previous periods still to be played. + */ + void reevaluateBuffer(long positionUs); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ShuffleOrder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ShuffleOrder.java new file mode 100644 index 000000000000..f137054145a6 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ShuffleOrder.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import java.util.Arrays; +import java.util.Random; + +/** + * Shuffled order of indices. + * + *

The shuffle order must be immutable to ensure thread safety. + */ +public interface ShuffleOrder { + + /** + * The default {@link ShuffleOrder} implementation for random shuffle order. + */ + class DefaultShuffleOrder implements ShuffleOrder { + + private final Random random; + private final int[] shuffled; + private final int[] indexInShuffled; + + /** + * Creates an instance with a specified length. + * + * @param length The length of the shuffle order. + */ + public DefaultShuffleOrder(int length) { + this(length, new Random()); + } + + /** + * Creates an instance with a specified length and the specified random seed. Shuffle orders of + * the same length initialized with the same random seed are guaranteed to be equal. + * + * @param length The length of the shuffle order. + * @param randomSeed A random seed. + */ + public DefaultShuffleOrder(int length, long randomSeed) { + this(length, new Random(randomSeed)); + } + + /** + * Creates an instance with a specified shuffle order and the specified random seed. The random + * seed is used for {@link #cloneAndInsert(int, int)} invocations. + * + * @param shuffledIndices The shuffled indices to use as order. + * @param randomSeed A random seed. + */ + public DefaultShuffleOrder(int[] shuffledIndices, long randomSeed) { + this(Arrays.copyOf(shuffledIndices, shuffledIndices.length), new Random(randomSeed)); + } + + private DefaultShuffleOrder(int length, Random random) { + this(createShuffledList(length, random), random); + } + + private DefaultShuffleOrder(int[] shuffled, Random random) { + this.shuffled = shuffled; + this.random = random; + this.indexInShuffled = new int[shuffled.length]; + for (int i = 0; i < shuffled.length; i++) { + indexInShuffled[shuffled[i]] = i; + } + } + + @Override + public int getLength() { + return shuffled.length; + } + + @Override + public int getNextIndex(int index) { + int shuffledIndex = indexInShuffled[index]; + return ++shuffledIndex < shuffled.length ? shuffled[shuffledIndex] : C.INDEX_UNSET; + } + + @Override + public int getPreviousIndex(int index) { + int shuffledIndex = indexInShuffled[index]; + return --shuffledIndex >= 0 ? shuffled[shuffledIndex] : C.INDEX_UNSET; + } + + @Override + public int getLastIndex() { + return shuffled.length > 0 ? shuffled[shuffled.length - 1] : C.INDEX_UNSET; + } + + @Override + public int getFirstIndex() { + return shuffled.length > 0 ? shuffled[0] : C.INDEX_UNSET; + } + + @Override + public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) { + int[] insertionPoints = new int[insertionCount]; + int[] insertionValues = new int[insertionCount]; + for (int i = 0; i < insertionCount; i++) { + insertionPoints[i] = random.nextInt(shuffled.length + 1); + int swapIndex = random.nextInt(i + 1); + insertionValues[i] = insertionValues[swapIndex]; + insertionValues[swapIndex] = i + insertionIndex; + } + Arrays.sort(insertionPoints); + int[] newShuffled = new int[shuffled.length + insertionCount]; + int indexInOldShuffled = 0; + int indexInInsertionList = 0; + for (int i = 0; i < shuffled.length + insertionCount; i++) { + if (indexInInsertionList < insertionCount + && indexInOldShuffled == insertionPoints[indexInInsertionList]) { + newShuffled[i] = insertionValues[indexInInsertionList++]; + } else { + newShuffled[i] = shuffled[indexInOldShuffled++]; + if (newShuffled[i] >= insertionIndex) { + newShuffled[i] += insertionCount; + } + } + } + return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong())); + } + + @Override + public ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive) { + int numberOfElementsToRemove = indexToExclusive - indexFrom; + int[] newShuffled = new int[shuffled.length - numberOfElementsToRemove]; + int foundElementsCount = 0; + for (int i = 0; i < shuffled.length; i++) { + if (shuffled[i] >= indexFrom && shuffled[i] < indexToExclusive) { + foundElementsCount++; + } else { + newShuffled[i - foundElementsCount] = + shuffled[i] >= indexFrom ? shuffled[i] - numberOfElementsToRemove : shuffled[i]; + } + } + return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong())); + } + + @Override + public ShuffleOrder cloneAndClear() { + return new DefaultShuffleOrder(/* length= */ 0, new Random(random.nextLong())); + } + + private static int[] createShuffledList(int length, Random random) { + int[] shuffled = new int[length]; + for (int i = 0; i < length; i++) { + int swapIndex = random.nextInt(i + 1); + shuffled[i] = shuffled[swapIndex]; + shuffled[swapIndex] = i; + } + return shuffled; + } + + } + + /** + * A {@link ShuffleOrder} implementation which does not shuffle. + */ + final class UnshuffledShuffleOrder implements ShuffleOrder { + + private final int length; + + /** + * Creates an instance with a specified length. + * + * @param length The length of the shuffle order. + */ + public UnshuffledShuffleOrder(int length) { + this.length = length; + } + + @Override + public int getLength() { + return length; + } + + @Override + public int getNextIndex(int index) { + return ++index < length ? index : C.INDEX_UNSET; + } + + @Override + public int getPreviousIndex(int index) { + return --index >= 0 ? index : C.INDEX_UNSET; + } + + @Override + public int getLastIndex() { + return length > 0 ? length - 1 : C.INDEX_UNSET; + } + + @Override + public int getFirstIndex() { + return length > 0 ? 0 : C.INDEX_UNSET; + } + + @Override + public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) { + return new UnshuffledShuffleOrder(length + insertionCount); + } + + @Override + public ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive) { + return new UnshuffledShuffleOrder(length - indexToExclusive + indexFrom); + } + + @Override + public ShuffleOrder cloneAndClear() { + return new UnshuffledShuffleOrder(/* length= */ 0); + } + } + + /** + * Returns length of shuffle order. + */ + int getLength(); + + /** + * Returns the next index in the shuffle order. + * + * @param index An index. + * @return The index after {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the last + * element. + */ + int getNextIndex(int index); + + /** + * Returns the previous index in the shuffle order. + * + * @param index An index. + * @return The index before {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the first + * element. + */ + int getPreviousIndex(int index); + + /** + * Returns the last index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is + * empty. + */ + int getLastIndex(); + + /** + * Returns the first index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is + * empty. + */ + int getFirstIndex(); + + /** + * Returns a copy of the shuffle order with newly inserted elements. + * + * @param insertionIndex The index in the unshuffled order at which elements are inserted. + * @param insertionCount The number of elements inserted at {@code insertionIndex}. + * @return A copy of this {@link ShuffleOrder} with newly inserted elements. + */ + ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount); + + /** + * Returns a copy of the shuffle order with a range of elements removed. + * + * @param indexFrom The starting index in the unshuffled order of the range to remove. + * @param indexToExclusive The smallest index (must be greater or equal to {@code indexFrom}) that + * will not be removed. + * @return A copy of this {@link ShuffleOrder} without the elements in the removed range. + */ + ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive); + + /** Returns a copy of the shuffle order with all elements removed. */ + ShuffleOrder cloneAndClear(); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SilenceMediaSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SilenceMediaSource.java new file mode 100644 index 000000000000..096cc66622c9 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SilenceMediaSource.java @@ -0,0 +1,253 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** Media source with a single period consisting of silent raw audio of a given duration. */ +public final class SilenceMediaSource extends BaseMediaSource { + + private static final int SAMPLE_RATE_HZ = 44100; + @C.PcmEncoding private static final int ENCODING = C.ENCODING_PCM_16BIT; + private static final int CHANNEL_COUNT = 2; + private static final Format FORMAT = + Format.createAudioSampleFormat( + /* id=*/ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + CHANNEL_COUNT, + SAMPLE_RATE_HZ, + ENCODING, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + private static final byte[] SILENCE_SAMPLE = + new byte[Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT) * 1024]; + + private final long durationUs; + + /** + * Creates a new media source providing silent audio of the given duration. + * + * @param durationUs The duration of silent audio to output, in microseconds. + */ + public SilenceMediaSource(long durationUs) { + Assertions.checkArgument(durationUs >= 0); + this.durationUs = durationUs; + } + + @Override + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + refreshSourceInfo( + new SinglePeriodTimeline( + durationUs, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false)); + } + + @Override + public void maybeThrowSourceInfoRefreshError() {} + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + return new SilenceMediaPeriod(durationUs); + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) {} + + @Override + protected void releaseSourceInternal() {} + + private static final class SilenceMediaPeriod implements MediaPeriod { + + private static final TrackGroupArray TRACKS = new TrackGroupArray(new TrackGroup(FORMAT)); + + private final long durationUs; + private final ArrayList sampleStreams; + + public SilenceMediaPeriod(long durationUs) { + this.durationUs = durationUs; + sampleStreams = new ArrayList<>(); + } + + @Override + public void prepare(Callback callback, long positionUs) { + callback.onPrepared(/* mediaPeriod= */ this); + } + + @Override + public void maybeThrowPrepareError() {} + + @Override + public TrackGroupArray getTrackGroups() { + return TRACKS; + } + + @Override + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { + positionUs = constrainSeekPosition(positionUs); + for (int i = 0; i < selections.length; i++) { + if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { + sampleStreams.remove(streams[i]); + streams[i] = null; + } + if (streams[i] == null && selections[i] != null) { + SilenceSampleStream stream = new SilenceSampleStream(durationUs); + stream.seekTo(positionUs); + sampleStreams.add(stream); + streams[i] = stream; + streamResetFlags[i] = true; + } + } + return positionUs; + } + + @Override + public void discardBuffer(long positionUs, boolean toKeyframe) {} + + @Override + public long readDiscontinuity() { + return C.TIME_UNSET; + } + + @Override + public long seekToUs(long positionUs) { + positionUs = constrainSeekPosition(positionUs); + for (int i = 0; i < sampleStreams.size(); i++) { + ((SilenceSampleStream) sampleStreams.get(i)).seekTo(positionUs); + } + return positionUs; + } + + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return constrainSeekPosition(positionUs); + } + + @Override + public long getBufferedPositionUs() { + return C.TIME_END_OF_SOURCE; + } + + @Override + public long getNextLoadPositionUs() { + return C.TIME_END_OF_SOURCE; + } + + @Override + public boolean continueLoading(long positionUs) { + return false; + } + + @Override + public boolean isLoading() { + return false; + } + + @Override + public void reevaluateBuffer(long positionUs) {} + + private long constrainSeekPosition(long positionUs) { + return Util.constrainValue(positionUs, 0, durationUs); + } + } + + private static final class SilenceSampleStream implements SampleStream { + + private final long durationBytes; + + private boolean sentFormat; + private long positionBytes; + + public SilenceSampleStream(long durationUs) { + durationBytes = getAudioByteCount(durationUs); + seekTo(0); + } + + public void seekTo(long positionUs) { + positionBytes = Util.constrainValue(getAudioByteCount(positionUs), 0, durationBytes); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void maybeThrowError() {} + + @Override + public int readData( + FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) { + if (!sentFormat || formatRequired) { + formatHolder.format = FORMAT; + sentFormat = true; + return C.RESULT_FORMAT_READ; + } + + long bytesRemaining = durationBytes - positionBytes; + if (bytesRemaining == 0) { + buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } + + int bytesToWrite = (int) Math.min(SILENCE_SAMPLE.length, bytesRemaining); + buffer.ensureSpaceForWrite(bytesToWrite); + buffer.data.put(SILENCE_SAMPLE, /* offset= */ 0, bytesToWrite); + buffer.timeUs = getAudioPositionUs(positionBytes); + buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); + positionBytes += bytesToWrite; + return C.RESULT_BUFFER_READ; + } + + @Override + public int skipData(long positionUs) { + long oldPositionBytes = positionBytes; + seekTo(positionUs); + return (int) ((positionBytes - oldPositionBytes) / SILENCE_SAMPLE.length); + } + } + + private static long getAudioByteCount(long durationUs) { + long audioSampleCount = durationUs * SAMPLE_RATE_HZ / C.MICROS_PER_SECOND; + return Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT) * audioSampleCount; + } + + private static long getAudioPositionUs(long bytes) { + long audioSampleCount = bytes / Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT); + return audioSampleCount * C.MICROS_PER_SECOND / SAMPLE_RATE_HZ; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index 2a22445a44a0..72d805dfa364 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -15,6 +15,7 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; @@ -24,29 +25,65 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; */ public final class SinglePeriodTimeline extends Timeline { - private static final Object ID = new Object(); + private static final Object UID = new Object(); + private final long presentationStartTimeMs; + private final long windowStartTimeMs; private final long periodDurationUs; private final long windowDurationUs; private final long windowPositionInPeriodUs; private final long windowDefaultStartPositionUs; private final boolean isSeekable; private final boolean isDynamic; + private final boolean isLive; + @Nullable private final Object tag; + @Nullable private final Object manifest; /** - * Creates a timeline of one period of known duration, and a static window starting at zero and - * extending to that duration. + * Creates a timeline containing a single period and a window that spans it. * * @param durationUs The duration of the period, in microseconds. * @param isSeekable Whether seeking is supported within the period. + * @param isDynamic Whether the window may change when the timeline is updated. + * @param isLive Whether the window is live. */ - public SinglePeriodTimeline(long durationUs, boolean isSeekable) { - this(durationUs, durationUs, 0, 0, isSeekable, false); + public SinglePeriodTimeline( + long durationUs, boolean isSeekable, boolean isDynamic, boolean isLive) { + this(durationUs, isSeekable, isDynamic, isLive, /* manifest= */ null, /* tag= */ null); } /** - * Creates a timeline with one period of known duration, and a window of known duration starting - * at a specified position in the period. + * Creates a timeline containing a single period and a window that spans it. + * + * @param durationUs The duration of the period, in microseconds. + * @param isSeekable Whether seeking is supported within the period. + * @param isDynamic Whether the window may change when the timeline is updated. + * @param isLive Whether the window is live. + * @param manifest The manifest. May be {@code null}. + * @param tag A tag used for {@link Window#tag}. + */ + public SinglePeriodTimeline( + long durationUs, + boolean isSeekable, + boolean isDynamic, + boolean isLive, + @Nullable Object manifest, + @Nullable Object tag) { + this( + durationUs, + durationUs, + /* windowPositionInPeriodUs= */ 0, + /* windowDefaultStartPositionUs= */ 0, + isSeekable, + isDynamic, + isLive, + manifest, + tag); + } + + /** + * Creates a timeline with one period, and a window of known duration starting at a specified + * position in the period. * * @param periodDurationUs The duration of the period in microseconds. * @param windowDurationUs The duration of the window in microseconds. @@ -56,16 +93,76 @@ public final class SinglePeriodTimeline extends Timeline { * which to begin playback, in microseconds. * @param isSeekable Whether seeking is supported within the window. * @param isDynamic Whether the window may change when the timeline is updated. + * @param isLive Whether the window is live. + * @param manifest The manifest. May be (@code null}. + * @param tag A tag used for {@link Timeline.Window#tag}. */ - public SinglePeriodTimeline(long periodDurationUs, long windowDurationUs, - long windowPositionInPeriodUs, long windowDefaultStartPositionUs, boolean isSeekable, - boolean isDynamic) { + public SinglePeriodTimeline( + long periodDurationUs, + long windowDurationUs, + long windowPositionInPeriodUs, + long windowDefaultStartPositionUs, + boolean isSeekable, + boolean isDynamic, + boolean isLive, + @Nullable Object manifest, + @Nullable Object tag) { + this( + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + periodDurationUs, + windowDurationUs, + windowPositionInPeriodUs, + windowDefaultStartPositionUs, + isSeekable, + isDynamic, + isLive, + manifest, + tag); + } + + /** + * Creates a timeline with one period, and a window of known duration starting at a specified + * position in the period. + * + * @param presentationStartTimeMs The start time of the presentation in milliseconds since the + * epoch. + * @param windowStartTimeMs The window's start time in milliseconds since the epoch. + * @param periodDurationUs The duration of the period in microseconds. + * @param windowDurationUs The duration of the window in microseconds. + * @param windowPositionInPeriodUs The position of the start of the window in the period, in + * microseconds. + * @param windowDefaultStartPositionUs The default position relative to the start of the window at + * which to begin playback, in microseconds. + * @param isSeekable Whether seeking is supported within the window. + * @param isDynamic Whether the window may change when the timeline is updated. + * @param isLive Whether the window is live. + * @param manifest The manifest. May be {@code null}. + * @param tag A tag used for {@link Timeline.Window#tag}. + */ + public SinglePeriodTimeline( + long presentationStartTimeMs, + long windowStartTimeMs, + long periodDurationUs, + long windowDurationUs, + long windowPositionInPeriodUs, + long windowDefaultStartPositionUs, + boolean isSeekable, + boolean isDynamic, + boolean isLive, + @Nullable Object manifest, + @Nullable Object tag) { + this.presentationStartTimeMs = presentationStartTimeMs; + this.windowStartTimeMs = windowStartTimeMs; this.periodDurationUs = periodDurationUs; this.windowDurationUs = windowDurationUs; this.windowPositionInPeriodUs = windowPositionInPeriodUs; this.windowDefaultStartPositionUs = windowDefaultStartPositionUs; this.isSeekable = isSeekable; this.isDynamic = isDynamic; + this.isLive = isLive; + this.manifest = manifest; + this.tag = tag; } @Override @@ -74,20 +171,35 @@ public final class SinglePeriodTimeline extends Timeline { } @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { Assertions.checkIndex(windowIndex, 0, 1); - Object id = setIds ? ID : null; long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; - if (isDynamic) { - windowDefaultStartPositionUs += defaultPositionProjectionUs; - if (windowDefaultStartPositionUs > windowDurationUs) { - // The projection takes us beyond the end of the live window. + if (isDynamic && defaultPositionProjectionUs != 0) { + if (windowDurationUs == C.TIME_UNSET) { + // Don't allow projection into a window that has an unknown duration. windowDefaultStartPositionUs = C.TIME_UNSET; + } else { + windowDefaultStartPositionUs += defaultPositionProjectionUs; + if (windowDefaultStartPositionUs > windowDurationUs) { + // The projection takes us beyond the end of the window. + windowDefaultStartPositionUs = C.TIME_UNSET; + } } } - return window.set(id, C.TIME_UNSET, C.TIME_UNSET, isSeekable, isDynamic, - windowDefaultStartPositionUs, windowDurationUs, 0, 0, windowPositionInPeriodUs); + return window.set( + Window.SINGLE_WINDOW_UID, + tag, + manifest, + presentationStartTimeMs, + windowStartTimeMs, + isSeekable, + isDynamic, + isLive, + windowDefaultStartPositionUs, + windowDurationUs, + 0, + 0, + windowPositionInPeriodUs); } @Override @@ -98,13 +210,18 @@ public final class SinglePeriodTimeline extends Timeline { @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { Assertions.checkIndex(periodIndex, 0, 1); - Object id = setIds ? ID : null; - return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs, false); + Object uid = setIds ? UID : null; + return period.set(/* id= */ null, uid, 0, periodDurationUs, -windowPositionInPeriodUs); } @Override public int getIndexOfPeriod(Object uid) { - return ID.equals(uid) ? 0 : C.INDEX_UNSET; + return UID.equals(uid) ? 0 : C.INDEX_UNSET; } + @Override + public Object getUidOfPeriod(int periodIndex) { + Assertions.checkIndex(periodIndex, 0, 1); + return UID; + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index d0364f8c41e4..6c7d92dac92b 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -15,23 +15,30 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source; -import android.net.Uri; -import android.os.Handler; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SingleSampleMediaSource.EventListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.Loadable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.StatsDataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link MediaPeriod} with a single sample. @@ -44,48 +51,61 @@ import java.util.Arrays; */ private static final int INITIAL_SAMPLE_SIZE = 1024; - private final Uri uri; + private final DataSpec dataSpec; private final DataSource.Factory dataSourceFactory; - private final int minLoadableRetryCount; - private final Handler eventHandler; - private final EventListener eventListener; - private final int eventSourceId; + @Nullable private final TransferListener transferListener; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; + private final EventDispatcher eventDispatcher; private final TrackGroupArray tracks; private final ArrayList sampleStreams; + private final long durationUs; + + // Package private to avoid thunk methods. /* package */ final Loader loader; /* package */ final Format format; + /* package */ final boolean treatLoadErrorsAsEndOfStream; + /* package */ boolean notifiedReadingStarted; /* package */ boolean loadingFinished; - /* package */ byte[] sampleData; + /* package */ byte @MonotonicNonNull [] sampleData; /* package */ int sampleSize; - public SingleSampleMediaPeriod(Uri uri, DataSource.Factory dataSourceFactory, Format format, - int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, - int eventSourceId) { - this.uri = uri; + public SingleSampleMediaPeriod( + DataSpec dataSpec, + DataSource.Factory dataSourceFactory, + @Nullable TransferListener transferListener, + Format format, + long durationUs, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, + EventDispatcher eventDispatcher, + boolean treatLoadErrorsAsEndOfStream) { + this.dataSpec = dataSpec; this.dataSourceFactory = dataSourceFactory; + this.transferListener = transferListener; this.format = format; - this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - this.eventSourceId = eventSourceId; + this.durationUs = durationUs; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + this.eventDispatcher = eventDispatcher; + this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; tracks = new TrackGroupArray(new TrackGroup(format)); sampleStreams = new ArrayList<>(); loader = new Loader("Loader:SingleSampleMediaPeriod"); + eventDispatcher.mediaPeriodCreated(); } public void release() { loader.release(); + eventDispatcher.mediaPeriodReleased(); } @Override - public void prepare(Callback callback) { + public void prepare(Callback callback, long positionUs) { callback.onPrepared(this); } @Override public void maybeThrowPrepareError() throws IOException { - loader.maybeThrowError(); + // Do nothing. } @Override @@ -94,8 +114,12 @@ import java.util.Arrays; } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { for (int i = 0; i < selections.length; i++) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { sampleStreams.remove(streams[i]); @@ -112,22 +136,53 @@ import java.util.Arrays; } @Override - public void discardBuffer(long positionUs) { + public void discardBuffer(long positionUs, boolean toKeyframe) { + // Do nothing. + } + + @Override + public void reevaluateBuffer(long positionUs) { // Do nothing. } @Override public boolean continueLoading(long positionUs) { - if (loadingFinished || loader.isLoading()) { + if (loadingFinished || loader.isLoading() || loader.hasFatalError()) { return false; } - loader.startLoading(new SourceLoadable(uri, dataSourceFactory.createDataSource()), this, - minLoadableRetryCount); + DataSource dataSource = dataSourceFactory.createDataSource(); + if (transferListener != null) { + dataSource.addTransferListener(transferListener); + } + long elapsedRealtimeMs = + loader.startLoading( + new SourceLoadable(dataSpec, dataSource), + /* callback= */ this, + loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MEDIA)); + eventDispatcher.loadStarted( + dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs); return true; } + @Override + public boolean isLoading() { + return loader.isLoading(); + } + @Override public long readDiscontinuity() { + if (!notifiedReadingStarted) { + eventDispatcher.readingStarted(); + notifiedReadingStarted = true; + } return C.TIME_UNSET; } @@ -144,45 +199,101 @@ import java.util.Arrays; @Override public long seekToUs(long positionUs) { for (int i = 0; i < sampleStreams.size(); i++) { - sampleStreams.get(i).seekToUs(positionUs); + sampleStreams.get(i).reset(); } return positionUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return positionUs; + } + // Loader.Callback implementation. @Override public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - sampleSize = loadable.sampleSize; - sampleData = loadable.sampleData; + sampleSize = (int) loadable.dataSource.getBytesRead(); + sampleData = Assertions.checkNotNull(loadable.sampleData); loadingFinished = true; + eventDispatcher.loadCompleted( + loadable.dataSpec, + loadable.dataSource.getLastOpenedUri(), + loadable.dataSource.getLastResponseHeaders(), + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + sampleSize); } @Override public void onLoadCanceled(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - // Do nothing. + eventDispatcher.loadCanceled( + loadable.dataSpec, + loadable.dataSource.getLastOpenedUri(), + loadable.dataSource.getLastResponseHeaders(), + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.dataSource.getBytesRead()); } @Override - public int onLoadError(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, - IOException error) { - notifyLoadError(error); - return Loader.RETRY; - } + public LoadErrorAction onLoadError( + SourceLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error, + int errorCount) { + long retryDelay = + loadErrorHandlingPolicy.getRetryDelayMsFor( + C.DATA_TYPE_MEDIA, loadDurationMs, error, errorCount); + boolean errorCanBePropagated = + retryDelay == C.TIME_UNSET + || errorCount + >= loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MEDIA); - // Internal methods. - - private void notifyLoadError(final IOException e) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onLoadError(eventSourceId, e); - } - }); + LoadErrorAction action; + if (treatLoadErrorsAsEndOfStream && errorCanBePropagated) { + loadingFinished = true; + action = Loader.DONT_RETRY; + } else { + action = + retryDelay != C.TIME_UNSET + ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelay) + : Loader.DONT_RETRY_FATAL; } + eventDispatcher.loadError( + loadable.dataSpec, + loadable.dataSource.getLastOpenedUri(), + loadable.dataSource.getLastResponseHeaders(), + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.dataSource.getBytesRead(), + error, + /* wasCanceled= */ !action.isRetry()); + return action; } private final class SampleStreamImpl implements SampleStream { @@ -192,8 +303,9 @@ import java.util.Arrays; private static final int STREAM_STATE_END_OF_STREAM = 2; private int streamState; + private boolean notifiedDownstreamFormat; - public void seekToUs(long positionUs) { + public void reset() { if (streamState == STREAM_STATE_END_OF_STREAM) { streamState = STREAM_STATE_SEND_SAMPLE; } @@ -206,12 +318,15 @@ import java.util.Arrays; @Override public void maybeThrowError() throws IOException { - loader.maybeThrowError(); + if (!treatLoadErrorsAsEndOfStream) { + loader.maybeThrowError(); + } } @Override public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { + maybeNotifyDownstreamFormat(); if (streamState == STREAM_STATE_END_OF_STREAM) { buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; @@ -219,41 +334,60 @@ import java.util.Arrays; formatHolder.format = format; streamState = STREAM_STATE_SEND_SAMPLE; return C.RESULT_FORMAT_READ; - } - - Assertions.checkState(streamState == STREAM_STATE_SEND_SAMPLE); - if (!loadingFinished) { - return C.RESULT_NOTHING_READ; - } else { - buffer.timeUs = 0; - buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); - buffer.ensureSpaceForWrite(sampleSize); - buffer.data.put(sampleData, 0, sampleSize); + } else if (loadingFinished) { + if (sampleData != null) { + buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); + buffer.timeUs = 0; + if (buffer.isFlagsOnly()) { + return C.RESULT_BUFFER_READ; + } + buffer.ensureSpaceForWrite(sampleSize); + buffer.data.put(sampleData, 0, sampleSize); + } else { + buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); + } streamState = STREAM_STATE_END_OF_STREAM; return C.RESULT_BUFFER_READ; } + return C.RESULT_NOTHING_READ; } @Override - public void skipData(long positionUs) { - if (positionUs > 0) { + public int skipData(long positionUs) { + maybeNotifyDownstreamFormat(); + if (positionUs > 0 && streamState != STREAM_STATE_END_OF_STREAM) { streamState = STREAM_STATE_END_OF_STREAM; + return 1; } + return 0; } + private void maybeNotifyDownstreamFormat() { + if (!notifiedDownstreamFormat) { + eventDispatcher.downstreamFormatChanged( + MimeTypes.getTrackType(format.sampleMimeType), + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaTimeUs= */ 0); + notifiedDownstreamFormat = true; + } + } } /* package */ static final class SourceLoadable implements Loadable { - private final Uri uri; - private final DataSource dataSource; + public final DataSpec dataSpec; - private int sampleSize; - private byte[] sampleData; + private final StatsDataSource dataSource; - public SourceLoadable(Uri uri, DataSource dataSource) { - this.uri = uri; - this.dataSource = dataSource; + @Nullable private byte[] sampleData; + + // the constructor does not initialize fields: sampleData + @SuppressWarnings("nullness:initialization.fields.uninitialized") + public SourceLoadable(DataSpec dataSpec, DataSource dataSource) { + this.dataSpec = dataSpec; + this.dataSource = new StatsDataSource(dataSource); } @Override @@ -261,22 +395,17 @@ import java.util.Arrays; // Never happens. } - @Override - public boolean isLoadCanceled() { - return false; - } - @Override public void load() throws IOException, InterruptedException { - // We always load from the beginning, so reset the sampleSize to 0. - sampleSize = 0; + // We always load from the beginning, so reset bytesRead to 0. + dataSource.resetBytesRead(); try { // Create and open the input. - dataSource.open(new DataSpec(uri)); + dataSource.open(dataSpec); // Load the sample data. int result = 0; while (result != C.RESULT_END_OF_INPUT) { - sampleSize += result; + int sampleSize = (int) dataSource.getBytesRead(); if (sampleData == null) { sampleData = new byte[INITIAL_SAMPLE_SIZE]; } else if (sampleSize == sampleData.length) { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 140ce28243a5..01f35ef77529 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -17,22 +17,29 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; -import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.IOException; /** * Loads data at a given {@link Uri} as a single sample belonging to a single {@link MediaPeriod}. */ -public final class SingleSampleMediaSource implements MediaSource { +public final class SingleSampleMediaSource extends BaseMediaSource { /** * Listener of {@link SingleSampleMediaSource} events. + * + * @deprecated Use {@link MediaSourceEventListener}. */ + @Deprecated public interface EventListener { /** @@ -45,48 +52,265 @@ public final class SingleSampleMediaSource implements MediaSource { } - /** - * The default minimum number of times to retry loading data prior to failing. - */ - public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; + /** Factory for {@link SingleSampleMediaSource}. */ + public static final class Factory { - private final Uri uri; + private final DataSource.Factory dataSourceFactory; + + private LoadErrorHandlingPolicy loadErrorHandlingPolicy; + private boolean treatLoadErrorsAsEndOfStream; + private boolean isCreateCalled; + @Nullable private Object tag; + + /** + * Creates a factory for {@link SingleSampleMediaSource}s. + * + * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will + * be obtained. + */ + public Factory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = Assertions.checkNotNull(dataSourceFactory); + loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); + } + + /** + * Sets a tag for the media source which will be published in the {@link Timeline} of the source + * as {@link Timeline.Window#tag}. + * + * @param tag A tag for the media source. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setTag(Object tag) { + Assertions.checkState(!isCreateCalled); + this.tag = tag; + return this; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. See {@link + * #setLoadErrorHandlingPolicy} for the default value. + * + *

Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with + * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int) + * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)} + * + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead. + */ + @Deprecated + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)); + } + + /** + * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link + * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. + * + *

Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}. + * + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + Assertions.checkState(!isCreateCalled); + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + return this; + } + + /** + * Sets whether load errors will be treated as end-of-stream signal (load errors will not be + * propagated). The default value is false. + * + * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample + * streams, treating them as ended instead. If false, load errors will be propagated + * normally by {@link SampleStream#maybeThrowError()}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) { + Assertions.checkState(!isCreateCalled); + this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; + return this; + } + + /** + * Returns a new {@link SingleSampleMediaSource} using the current parameters. + * + * @param uri The {@link Uri}. + * @param format The {@link Format} of the media stream. + * @param durationUs The duration of the media stream in microseconds. + * @return The new {@link SingleSampleMediaSource}. + */ + public SingleSampleMediaSource createMediaSource(Uri uri, Format format, long durationUs) { + isCreateCalled = true; + return new SingleSampleMediaSource( + uri, + dataSourceFactory, + format, + durationUs, + loadErrorHandlingPolicy, + treatLoadErrorsAsEndOfStream, + tag); + } + + /** + * @deprecated Use {@link #createMediaSource(Uri, Format, long)} and {@link + * #addEventListener(Handler, MediaSourceEventListener)} instead. + */ + @Deprecated + public SingleSampleMediaSource createMediaSource( + Uri uri, + Format format, + long durationUs, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + SingleSampleMediaSource mediaSource = createMediaSource(uri, format, durationUs); + if (eventHandler != null && eventListener != null) { + mediaSource.addEventListener(eventHandler, eventListener); + } + return mediaSource; + } + + } + + private final DataSpec dataSpec; private final DataSource.Factory dataSourceFactory; private final Format format; - private final int minLoadableRetryCount; - private final Handler eventHandler; - private final EventListener eventListener; - private final int eventSourceId; + private final long durationUs; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; + private final boolean treatLoadErrorsAsEndOfStream; private final Timeline timeline; + @Nullable private final Object tag; - public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, - long durationUs) { - this(uri, dataSourceFactory, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT); + @Nullable private TransferListener transferListener; + + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will + * be obtained. + * @param format The {@link Format} associated with the output track. + * @param durationUs The duration of the media stream in microseconds. + * @deprecated Use {@link Factory} instead. + */ + @Deprecated + @SuppressWarnings("deprecation") + public SingleSampleMediaSource( + Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { + this( + uri, + dataSourceFactory, + format, + durationUs, + DefaultLoadErrorHandlingPolicy.DEFAULT_MIN_LOADABLE_RETRY_COUNT); } - public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, - long durationUs, int minLoadableRetryCount) { - this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0); + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will + * be obtained. + * @param format The {@link Format} associated with the output track. + * @param durationUs The duration of the media stream in microseconds. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @deprecated Use {@link Factory} instead. + */ + @Deprecated + public SingleSampleMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount) { + this( + uri, + dataSourceFactory, + format, + durationUs, + new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), + /* treatLoadErrorsAsEndOfStream= */ false, + /* tag= */ null); } - public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, - long durationUs, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, - int eventSourceId) { - this.uri = uri; + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will + * be obtained. + * @param format The {@link Format} associated with the output track. + * @param durationUs The duration of the media stream in microseconds. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param eventSourceId An identifier that gets passed to {@code eventListener} methods. + * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample + * streams, treating them as ended instead. If false, load errors will be propagated normally + * by {@link SampleStream#maybeThrowError()}. + * @deprecated Use {@link Factory} instead. + */ + @Deprecated + @SuppressWarnings("deprecation") + public SingleSampleMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount, + Handler eventHandler, + EventListener eventListener, + int eventSourceId, + boolean treatLoadErrorsAsEndOfStream) { + this( + uri, + dataSourceFactory, + format, + durationUs, + new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), + treatLoadErrorsAsEndOfStream, + /* tag= */ null); + if (eventHandler != null && eventListener != null) { + addEventListener(eventHandler, new EventListenerWrapper(eventListener, eventSourceId)); + } + } + + private SingleSampleMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, + boolean treatLoadErrorsAsEndOfStream, + @Nullable Object tag) { this.dataSourceFactory = dataSourceFactory; this.format = format; - this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - this.eventSourceId = eventSourceId; - timeline = new SinglePeriodTimeline(durationUs, true); + this.durationUs = durationUs; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; + this.tag = tag; + dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP); + timeline = + new SinglePeriodTimeline( + durationUs, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* manifest= */ null, + tag); } // MediaSource implementation. @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - listener.onSourceInfoRefreshed(timeline, null); + @Nullable + public Object getTag() { + return tag; + } + + @Override + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + transferListener = mediaTransferListener; + refreshSourceInfo(timeline); } @Override @@ -95,10 +319,16 @@ public final class SingleSampleMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - Assertions.checkArgument(index == 0); - return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount, - eventHandler, eventListener, eventSourceId); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + return new SingleSampleMediaPeriod( + dataSpec, + dataSourceFactory, + transferListener, + format, + durationUs, + loadErrorHandlingPolicy, + createEventDispatcher(id), + treatLoadErrorsAsEndOfStream); } @Override @@ -107,8 +337,35 @@ public final class SingleSampleMediaSource implements MediaSource { } @Override - public void releaseSource() { + protected void releaseSourceInternal() { // Do nothing. } + /** + * Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in + * {@link MediaSourceEventListener}. + */ + @Deprecated + @SuppressWarnings("deprecation") + private static final class EventListenerWrapper implements MediaSourceEventListener { + + private final EventListener eventListener; + private final int eventSourceId; + + public EventListenerWrapper(EventListener eventListener, int eventSourceId) { + this.eventListener = Assertions.checkNotNull(eventListener); + this.eventSourceId = eventSourceId; + } + + @Override + public void onLoadError( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + eventListener.onLoadError(eventSourceId, error); + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroup.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroup.java index 51833e37989c..566238dbdbfb 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroup.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroup.java @@ -15,6 +15,9 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source; +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; @@ -24,12 +27,12 @@ import java.util.Arrays; // does not apply. /** * Defines a group of tracks exposed by a {@link MediaPeriod}. - *

- * A {@link MediaPeriod} is only able to provide one {@link SampleStream} corresponding to a group - * at any given time, however this {@link SampleStream} may adapt between multiple tracks within the - * group. + * + *

A {@link MediaPeriod} is only able to provide one {@link SampleStream} corresponding to a + * group at any given time, however this {@link SampleStream} may adapt between multiple tracks + * within the group. */ -public final class TrackGroup { +public final class TrackGroup implements Parcelable { /** * The number of tracks in the group. @@ -42,7 +45,7 @@ public final class TrackGroup { private int hashCode; /** - * @param formats The track formats. Must not be null or contain null elements. + * @param formats The track formats. Must not be null, contain null elements or be of length 0. */ public TrackGroup(Format... formats) { Assertions.checkState(formats.length > 0); @@ -50,6 +53,14 @@ public final class TrackGroup { this.length = formats.length; } + /* package */ TrackGroup(Parcel in) { + length = in.readInt(); + formats = new Format[length]; + for (int i = 0; i < length; i++) { + formats[i] = in.readParcelable(Format.class.getClassLoader()); + } + } + /** * Returns the format of the track at a given index. * @@ -61,11 +72,14 @@ public final class TrackGroup { } /** - * Returns the index of the track with the given format in the group. + * Returns the index of the track with the given format in the group. The format is located by + * identity so, for example, {@code group.indexOf(group.getFormat(index)) == index} even if + * multiple tracks have formats that contain the same values. * * @param format The format. * @return The index of the track, or {@link C#INDEX_UNSET} if no such track exists. */ + @SuppressWarnings("ReferenceEquality") public int indexOf(Format format) { for (int i = 0; i < formats.length; i++) { if (format == formats[i]) { @@ -86,7 +100,7 @@ public final class TrackGroup { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -97,4 +111,32 @@ public final class TrackGroup { return length == other.length && Arrays.equals(formats, other.formats); } + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(length); + for (int i = 0; i < length; i++) { + dest.writeParcelable(formats[i], 0); + } + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public TrackGroup createFromParcel(Parcel in) { + return new TrackGroup(in); + } + + @Override + public TrackGroup[] newArray(int size) { + return new TrackGroup[size]; + } + }; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroupArray.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroupArray.java index 2348ab57fe04..103a45080e02 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroupArray.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroupArray.java @@ -15,13 +15,14 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source; +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import java.util.Arrays; -/** - * An array of {@link TrackGroup}s exposed by a {@link MediaPeriod}. - */ -public final class TrackGroupArray { +/** An array of {@link TrackGroup}s exposed by a {@link MediaPeriod}. */ +public final class TrackGroupArray implements Parcelable { /** * The empty array. @@ -46,6 +47,14 @@ public final class TrackGroupArray { this.length = trackGroups.length; } + /* package */ TrackGroupArray(Parcel in) { + length = in.readInt(); + trackGroups = new TrackGroup[length]; + for (int i = 0; i < length; i++) { + trackGroups[i] = in.readParcelable(TrackGroup.class.getClassLoader()); + } + } + /** * Returns the group at a given index. * @@ -62,8 +71,11 @@ public final class TrackGroupArray { * @param group The group. * @return The index of the group, or {@link C#INDEX_UNSET} if no such group exists. */ + @SuppressWarnings("ReferenceEquality") public int indexOf(TrackGroup group) { for (int i = 0; i < length; i++) { + // Suppressed reference equality warning because this is looking for the index of a specific + // TrackGroup object, not the index of a potential equal TrackGroup. if (trackGroups[i] == group) { return i; } @@ -71,6 +83,13 @@ public final class TrackGroupArray { return C.INDEX_UNSET; } + /** + * Returns whether this track group array is empty. + */ + public boolean isEmpty() { + return length == 0; + } + @Override public int hashCode() { if (hashCode == 0) { @@ -80,7 +99,7 @@ public final class TrackGroupArray { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -91,4 +110,32 @@ public final class TrackGroupArray { return length == other.length && Arrays.equals(trackGroups, other.trackGroups); } + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(length); + for (int i = 0; i < length; i++) { + dest.writeParcelable(trackGroups[i], 0); + } + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public TrackGroupArray createFromParcel(Parcel in) { + return new TrackGroupArray(in); + } + + @Override + public TrackGroupArray[] newArray(int size) { + return new TrackGroupArray[size]; + } + }; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdPlaybackState.java new file mode 100644 index 000000000000..83b5b1bc40f5 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -0,0 +1,486 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.ads; + +import android.net.Uri; +import androidx.annotation.CheckResult; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** + * Represents ad group times relative to the start of the media and information on the state and + * URIs of ads within each ad group. + * + *

Instances are immutable. Call the {@code with*} methods to get new instances that have the + * required changes. + */ +public final class AdPlaybackState { + + /** + * Represents a group of ads, with information about their states. + * + *

Instances are immutable. Call the {@code with*} methods to get new instances that have the + * required changes. + */ + public static final class AdGroup { + + /** The number of ads in the ad group, or {@link C#LENGTH_UNSET} if unknown. */ + public final int count; + /** The URI of each ad in the ad group. */ + public final @NullableType Uri[] uris; + /** The state of each ad in the ad group. */ + @AdState public final int[] states; + /** The durations of each ad in the ad group, in microseconds. */ + public final long[] durationsUs; + + /** Creates a new ad group with an unspecified number of ads. */ + public AdGroup() { + this( + /* count= */ C.LENGTH_UNSET, + /* states= */ new int[0], + /* uris= */ new Uri[0], + /* durationsUs= */ new long[0]); + } + + private AdGroup( + int count, @AdState int[] states, @NullableType Uri[] uris, long[] durationsUs) { + Assertions.checkArgument(states.length == uris.length); + this.count = count; + this.states = states; + this.uris = uris; + this.durationsUs = durationsUs; + } + + /** + * Returns the index of the first ad in the ad group that should be played, or {@link #count} if + * no ads should be played. + */ + public int getFirstAdIndexToPlay() { + return getNextAdIndexToPlay(-1); + } + + /** + * Returns the index of the next ad in the ad group that should be played after playing {@code + * lastPlayedAdIndex}, or {@link #count} if no later ads should be played. + */ + public int getNextAdIndexToPlay(int lastPlayedAdIndex) { + int nextAdIndexToPlay = lastPlayedAdIndex + 1; + while (nextAdIndexToPlay < states.length) { + if (states[nextAdIndexToPlay] == AD_STATE_UNAVAILABLE + || states[nextAdIndexToPlay] == AD_STATE_AVAILABLE) { + break; + } + nextAdIndexToPlay++; + } + return nextAdIndexToPlay; + } + + /** Returns whether the ad group has at least one ad that still needs to be played. */ + public boolean hasUnplayedAds() { + return count == C.LENGTH_UNSET || getFirstAdIndexToPlay() < count; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AdGroup adGroup = (AdGroup) o; + return count == adGroup.count + && Arrays.equals(uris, adGroup.uris) + && Arrays.equals(states, adGroup.states) + && Arrays.equals(durationsUs, adGroup.durationsUs); + } + + @Override + public int hashCode() { + int result = count; + result = 31 * result + Arrays.hashCode(uris); + result = 31 * result + Arrays.hashCode(states); + result = 31 * result + Arrays.hashCode(durationsUs); + return result; + } + + /** + * Returns a new instance with the ad count set to {@code count}. This method may only be called + * if this instance's ad count has not yet been specified. + */ + @CheckResult + public AdGroup withAdCount(int count) { + Assertions.checkArgument(this.count == C.LENGTH_UNSET && states.length <= count); + @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, count); + long[] durationsUs = copyDurationsUsWithSpaceForAdCount(this.durationsUs, count); + @NullableType Uri[] uris = Arrays.copyOf(this.uris, count); + return new AdGroup(count, states, uris, durationsUs); + } + + /** + * Returns a new instance with the specified {@code uri} set for the specified ad, and the ad + * marked as {@link #AD_STATE_AVAILABLE}. The specified ad must currently be in {@link + * #AD_STATE_UNAVAILABLE}, which is the default state. + * + *

This instance's ad count may be unknown, in which case {@code index} must be less than the + * ad count specified later. Otherwise, {@code index} must be less than the current ad count. + */ + @CheckResult + public AdGroup withAdUri(Uri uri, int index) { + Assertions.checkArgument(count == C.LENGTH_UNSET || index < count); + @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1); + Assertions.checkArgument(states[index] == AD_STATE_UNAVAILABLE); + long[] durationsUs = + this.durationsUs.length == states.length + ? this.durationsUs + : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length); + @NullableType Uri[] uris = Arrays.copyOf(this.uris, states.length); + uris[index] = uri; + states[index] = AD_STATE_AVAILABLE; + return new AdGroup(count, states, uris, durationsUs); + } + + /** + * Returns a new instance with the specified ad set to the specified {@code state}. The ad + * specified must currently either be in {@link #AD_STATE_UNAVAILABLE} or {@link + * #AD_STATE_AVAILABLE}. + * + *

This instance's ad count may be unknown, in which case {@code index} must be less than the + * ad count specified later. Otherwise, {@code index} must be less than the current ad count. + */ + @CheckResult + public AdGroup withAdState(@AdState int state, int index) { + Assertions.checkArgument(count == C.LENGTH_UNSET || index < count); + @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1); + Assertions.checkArgument( + states[index] == AD_STATE_UNAVAILABLE + || states[index] == AD_STATE_AVAILABLE + || states[index] == state); + long[] durationsUs = + this.durationsUs.length == states.length + ? this.durationsUs + : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length); + @NullableType + Uri[] uris = + this.uris.length == states.length ? this.uris : Arrays.copyOf(this.uris, states.length); + states[index] = state; + return new AdGroup(count, states, uris, durationsUs); + } + + /** Returns a new instance with the specified ad durations, in microseconds. */ + @CheckResult + public AdGroup withAdDurationsUs(long[] durationsUs) { + Assertions.checkArgument(count == C.LENGTH_UNSET || durationsUs.length <= this.uris.length); + if (durationsUs.length < this.uris.length) { + durationsUs = copyDurationsUsWithSpaceForAdCount(durationsUs, uris.length); + } + return new AdGroup(count, states, uris, durationsUs); + } + + /** + * Returns an instance with all unavailable and available ads marked as skipped. If the ad count + * hasn't been set, it will be set to zero. + */ + @CheckResult + public AdGroup withAllAdsSkipped() { + if (count == C.LENGTH_UNSET) { + return new AdGroup( + /* count= */ 0, + /* states= */ new int[0], + /* uris= */ new Uri[0], + /* durationsUs= */ new long[0]); + } + int count = this.states.length; + @AdState int[] states = Arrays.copyOf(this.states, count); + for (int i = 0; i < count; i++) { + if (states[i] == AD_STATE_AVAILABLE || states[i] == AD_STATE_UNAVAILABLE) { + states[i] = AD_STATE_SKIPPED; + } + } + return new AdGroup(count, states, uris, durationsUs); + } + + @CheckResult + private static @AdState int[] copyStatesWithSpaceForAdCount(@AdState int[] states, int count) { + int oldStateCount = states.length; + int newStateCount = Math.max(count, oldStateCount); + states = Arrays.copyOf(states, newStateCount); + Arrays.fill(states, oldStateCount, newStateCount, AD_STATE_UNAVAILABLE); + return states; + } + + @CheckResult + private static long[] copyDurationsUsWithSpaceForAdCount(long[] durationsUs, int count) { + int oldDurationsUsCount = durationsUs.length; + int newDurationsUsCount = Math.max(count, oldDurationsUsCount); + durationsUs = Arrays.copyOf(durationsUs, newDurationsUsCount); + Arrays.fill(durationsUs, oldDurationsUsCount, newDurationsUsCount, C.TIME_UNSET); + return durationsUs; + } + } + + /** + * Represents the state of an ad in an ad group. One of {@link #AD_STATE_UNAVAILABLE}, {@link + * #AD_STATE_AVAILABLE}, {@link #AD_STATE_SKIPPED}, {@link #AD_STATE_PLAYED} or {@link + * #AD_STATE_ERROR}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + AD_STATE_UNAVAILABLE, + AD_STATE_AVAILABLE, + AD_STATE_SKIPPED, + AD_STATE_PLAYED, + AD_STATE_ERROR, + }) + public @interface AdState {} + /** State for an ad that does not yet have a URL. */ + public static final int AD_STATE_UNAVAILABLE = 0; + /** State for an ad that has a URL but has not yet been played. */ + public static final int AD_STATE_AVAILABLE = 1; + /** State for an ad that was skipped. */ + public static final int AD_STATE_SKIPPED = 2; + /** State for an ad that was played in full. */ + public static final int AD_STATE_PLAYED = 3; + /** State for an ad that could not be loaded. */ + public static final int AD_STATE_ERROR = 4; + + /** Ad playback state with no ads. */ + public static final AdPlaybackState NONE = new AdPlaybackState(); + + /** The number of ad groups. */ + public final int adGroupCount; + /** + * The times of ad groups, in microseconds. A final element with the value {@link + * C#TIME_END_OF_SOURCE} indicates a postroll ad. + */ + public final long[] adGroupTimesUs; + /** The ad groups. */ + public final AdGroup[] adGroups; + /** The position offset in the first unplayed ad at which to begin playback, in microseconds. */ + public final long adResumePositionUs; + /** The content duration in microseconds, if known. {@link C#TIME_UNSET} otherwise. */ + public final long contentDurationUs; + + /** + * Creates a new ad playback state with the specified ad group times. + * + * @param adGroupTimesUs The times of ad groups in microseconds. A final element with the value + * {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad. + */ + public AdPlaybackState(long... adGroupTimesUs) { + int count = adGroupTimesUs.length; + adGroupCount = count; + this.adGroupTimesUs = Arrays.copyOf(adGroupTimesUs, count); + this.adGroups = new AdGroup[count]; + for (int i = 0; i < count; i++) { + adGroups[i] = new AdGroup(); + } + adResumePositionUs = 0; + contentDurationUs = C.TIME_UNSET; + } + + private AdPlaybackState( + long[] adGroupTimesUs, AdGroup[] adGroups, long adResumePositionUs, long contentDurationUs) { + adGroupCount = adGroups.length; + this.adGroupTimesUs = adGroupTimesUs; + this.adGroups = adGroups; + this.adResumePositionUs = adResumePositionUs; + this.contentDurationUs = contentDurationUs; + } + + /** + * Returns the index of the ad group at or before {@code positionUs}, if that ad group is + * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has no + * ads remaining to be played, or if there is no such ad group. + * + * @param positionUs The position at or before which to find an ad group, in microseconds, or + * {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case the index of any + * unplayed postroll ad group will be returned). + * @return The index of the ad group, or {@link C#INDEX_UNSET}. + */ + public int getAdGroupIndexForPositionUs(long positionUs) { + // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. + // In practice we expect there to be few ad groups so the search shouldn't be expensive. + int index = adGroupTimesUs.length - 1; + while (index >= 0 && isPositionBeforeAdGroup(positionUs, index)) { + index--; + } + return index >= 0 && adGroups[index].hasUnplayedAds() ? index : C.INDEX_UNSET; + } + + /** + * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be + * played. Returns {@link C#INDEX_UNSET} if there is no such ad group. + * + * @param positionUs The position after which to find an ad group, in microseconds, or {@link + * C#TIME_END_OF_SOURCE} for the end of the stream (in which case there can be no ad group + * after the position). + * @param periodDurationUs The duration of the containing period in microseconds, or {@link + * C#TIME_UNSET} if not known. + * @return The index of the ad group, or {@link C#INDEX_UNSET}. + */ + public int getAdGroupIndexAfterPositionUs(long positionUs, long periodDurationUs) { + if (positionUs == C.TIME_END_OF_SOURCE + || (periodDurationUs != C.TIME_UNSET && positionUs >= periodDurationUs)) { + return C.INDEX_UNSET; + } + // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. + // In practice we expect there to be few ad groups so the search shouldn't be expensive. + int index = 0; + while (index < adGroupTimesUs.length + && adGroupTimesUs[index] != C.TIME_END_OF_SOURCE + && (positionUs >= adGroupTimesUs[index] || !adGroups[index].hasUnplayedAds())) { + index++; + } + return index < adGroupTimesUs.length ? index : C.INDEX_UNSET; + } + + /** + * Returns an instance with the number of ads in {@code adGroupIndex} resolved to {@code adCount}. + * The ad count must be greater than zero. + */ + @CheckResult + public AdPlaybackState withAdCount(int adGroupIndex, int adCount) { + Assertions.checkArgument(adCount > 0); + if (adGroups[adGroupIndex].count == adCount) { + return this; + } + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); + adGroups[adGroupIndex] = this.adGroups[adGroupIndex].withAdCount(adCount); + return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); + } + + /** Returns an instance with the specified ad URI. */ + @CheckResult + public AdPlaybackState withAdUri(int adGroupIndex, int adIndexInAdGroup, Uri uri) { + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); + adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdUri(uri, adIndexInAdGroup); + return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); + } + + /** Returns an instance with the specified ad marked as played. */ + @CheckResult + public AdPlaybackState withPlayedAd(int adGroupIndex, int adIndexInAdGroup) { + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); + adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_PLAYED, adIndexInAdGroup); + return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); + } + + /** Returns an instance with the specified ad marked as skipped. */ + @CheckResult + public AdPlaybackState withSkippedAd(int adGroupIndex, int adIndexInAdGroup) { + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); + adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_SKIPPED, adIndexInAdGroup); + return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); + } + + /** Returns an instance with the specified ad marked as having a load error. */ + @CheckResult + public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) { + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); + adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_ERROR, adIndexInAdGroup); + return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); + } + + /** + * Returns an instance with all ads in the specified ad group skipped (except for those already + * marked as played or in the error state). + */ + @CheckResult + public AdPlaybackState withSkippedAdGroup(int adGroupIndex) { + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); + adGroups[adGroupIndex] = adGroups[adGroupIndex].withAllAdsSkipped(); + return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); + } + + /** Returns an instance with the specified ad durations, in microseconds. */ + @CheckResult + public AdPlaybackState withAdDurationsUs(long[][] adDurationUs) { + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); + for (int adGroupIndex = 0; adGroupIndex < adGroupCount; adGroupIndex++) { + adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdDurationsUs(adDurationUs[adGroupIndex]); + } + return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); + } + + /** Returns an instance with the specified ad resume position, in microseconds. */ + @CheckResult + public AdPlaybackState withAdResumePositionUs(long adResumePositionUs) { + if (this.adResumePositionUs == adResumePositionUs) { + return this; + } else { + return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); + } + } + + /** Returns an instance with the specified content duration, in microseconds. */ + @CheckResult + public AdPlaybackState withContentDurationUs(long contentDurationUs) { + if (this.contentDurationUs == contentDurationUs) { + return this; + } else { + return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); + } + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AdPlaybackState that = (AdPlaybackState) o; + return adGroupCount == that.adGroupCount + && adResumePositionUs == that.adResumePositionUs + && contentDurationUs == that.contentDurationUs + && Arrays.equals(adGroupTimesUs, that.adGroupTimesUs) + && Arrays.equals(adGroups, that.adGroups); + } + + @Override + public int hashCode() { + int result = adGroupCount; + result = 31 * result + (int) adResumePositionUs; + result = 31 * result + (int) contentDurationUs; + result = 31 * result + Arrays.hashCode(adGroupTimesUs); + result = 31 * result + Arrays.hashCode(adGroups); + return result; + } + + private boolean isPositionBeforeAdGroup(long positionUs, int adGroupIndex) { + if (positionUs == C.TIME_END_OF_SOURCE) { + // The end of the content is at (but not before) any postroll ad, and after any other ads. + return false; + } + long adGroupPositionUs = adGroupTimesUs[adGroupIndex]; + if (adGroupPositionUs == C.TIME_END_OF_SOURCE) { + return contentDurationUs == C.TIME_UNSET || positionUs < contentDurationUs; + } else { + return positionUs < adGroupPositionUs; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsLoader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsLoader.java new file mode 100644 index 000000000000..12ffb8ec0d0d --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsLoader.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.ads; + +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import java.io.IOException; + +/** + * Interface for loaders of ads, which can be used with {@link AdsMediaSource}. + * + *

Ads loaders notify the {@link AdsMediaSource} about events via {@link EventListener}. In + * particular, implementations must call {@link EventListener#onAdPlaybackState(AdPlaybackState)} + * with a new copy of the current {@link AdPlaybackState} whenever further information about ads + * becomes known (for example, when an ad media URI is available, or an ad has played to the end). + * + *

{@link #start(EventListener, AdViewProvider)} will be called when the ads media source first + * initializes, at which point the loader can request ads. If the player enters the background, + * {@link #stop()} will be called. Loaders should maintain any ad playback state in preparation for + * a later call to {@link #start(EventListener, AdViewProvider)}. If an ad is playing when the + * player is detached, update the ad playback state with the current playback position using {@link + * AdPlaybackState#withAdResumePositionUs(long)}. + * + *

If {@link EventListener#onAdPlaybackState(AdPlaybackState)} has been called, the + * implementation of {@link #start(EventListener, AdViewProvider)} should invoke the same listener + * to provide the existing playback state to the new player. + */ +public interface AdsLoader { + + /** Listener for ads loader events. All methods are called on the main thread. */ + interface EventListener { + + /** + * Called when the ad playback state has been updated. + * + * @param adPlaybackState The new ad playback state. + */ + default void onAdPlaybackState(AdPlaybackState adPlaybackState) {} + + /** + * Called when there was an error loading ads. + * + * @param error The error. + * @param dataSpec The data spec associated with the load error. + */ + default void onAdLoadError(AdLoadException error, DataSpec dataSpec) {} + + /** Called when the user clicks through an ad (for example, following a 'learn more' link). */ + default void onAdClicked() {} + + /** Called when the user taps a non-clickthrough part of an ad. */ + default void onAdTapped() {} + } + + /** Provides views for the ad UI. */ + interface AdViewProvider { + + /** Returns the {@link ViewGroup} on top of the player that will show any ad UI. */ + ViewGroup getAdViewGroup(); + + /** + * Returns an array of views that are shown on top of the ad view group, but that are essential + * for controlling playback and should be excluded from ad viewability measurements by the + * {@link AdsLoader} (if it supports this). + * + *

Each view must be either a fully transparent overlay (for capturing touch events), or a + * small piece of transient UI that is essential to the user experience of playback (such as a + * button to pause/resume playback or a transient full-screen or cast button). For more + * information see the documentation for your ads loader. + */ + View[] getAdOverlayViews(); + } + + // Methods called by the application. + + /** + * Sets the player that will play the loaded ads. + * + *

This method must be called before the player is prepared with media using this ads loader. + * + *

This method must also be called on the main thread and only players which are accessed on + * the main thread are supported ({@code player.getApplicationLooper() == + * Looper.getMainLooper()}). + * + * @param player The player instance that will play the loaded ads. May be null to delete the + * reference to a previously set player. + */ + void setPlayer(@Nullable Player player); + + /** + * Releases the loader. Must be called by the application on the main thread when the instance is + * no longer needed. + */ + void release(); + + // Methods called by AdsMediaSource. + + /** + * Sets the supported content types for ad media. Must be called before the first call to {@link + * #start(EventListener, AdViewProvider)}. Subsequent calls may be ignored. Called on the main + * thread by {@link AdsMediaSource}. + * + * @param contentTypes The supported content types for ad media. Each element must be one of + * {@link C#TYPE_DASH}, {@link C#TYPE_HLS}, {@link C#TYPE_SS} and {@link C#TYPE_OTHER}. + */ + void setSupportedContentTypes(@C.ContentType int... contentTypes); + + /** + * Starts using the ads loader for playback. Called on the main thread by {@link AdsMediaSource}. + * + * @param eventListener Listener for ads loader events. + * @param adViewProvider Provider of views for the ad UI. + */ + void start(EventListener eventListener, AdViewProvider adViewProvider); + + /** + * Stops using the ads loader for playback and deregisters the event listener. Called on the main + * thread by {@link AdsMediaSource}. + */ + void stop(); + + /** + * Notifies the ads loader that the player was not able to prepare media for a given ad. + * Implementations should update the ad playback state as the specified ad has failed to load. + * Called on the main thread by {@link AdsMediaSource}. + * + * @param adGroupIndex The index of the ad group. + * @param adIndexInAdGroup The index of the ad in the ad group. + * @param exception The preparation error. + */ + void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsMediaSource.java new file mode 100644 index 000000000000..02c33a3d34e1 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -0,0 +1,439 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.ads; + +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.CompositeMediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MaskingMediaPeriod; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaPeriod; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ProgressiveMediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** + * A {@link MediaSource} that inserts ads linearly with a provided content media source. This source + * cannot be used as a child source in a composition. It must be the top-level source used to + * prepare the player. + */ +public final class AdsMediaSource extends CompositeMediaSource { + + /** + * Wrapper for exceptions that occur while loading ads, which are notified via {@link + * MediaSourceEventListener#onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, + * IOException, boolean)}. + */ + public static final class AdLoadException extends IOException { + + /** + * Types of ad load exceptions. One of {@link #TYPE_AD}, {@link #TYPE_AD_GROUP}, {@link + * #TYPE_ALL_ADS} or {@link #TYPE_UNEXPECTED}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_AD, TYPE_AD_GROUP, TYPE_ALL_ADS, TYPE_UNEXPECTED}) + public @interface Type {} + /** Type for when an ad failed to load. The ad will be skipped. */ + public static final int TYPE_AD = 0; + /** Type for when an ad group failed to load. The ad group will be skipped. */ + public static final int TYPE_AD_GROUP = 1; + /** Type for when all ad groups failed to load. All ads will be skipped. */ + public static final int TYPE_ALL_ADS = 2; + /** Type for when an unexpected error occurred while loading ads. All ads will be skipped. */ + public static final int TYPE_UNEXPECTED = 3; + + /** Returns a new ad load exception of {@link #TYPE_AD}. */ + public static AdLoadException createForAd(Exception error) { + return new AdLoadException(TYPE_AD, error); + } + + /** Returns a new ad load exception of {@link #TYPE_AD_GROUP}. */ + public static AdLoadException createForAdGroup(Exception error, int adGroupIndex) { + return new AdLoadException( + TYPE_AD_GROUP, new IOException("Failed to load ad group " + adGroupIndex, error)); + } + + /** Returns a new ad load exception of {@link #TYPE_ALL_ADS}. */ + public static AdLoadException createForAllAds(Exception error) { + return new AdLoadException(TYPE_ALL_ADS, error); + } + + /** Returns a new ad load exception of {@link #TYPE_UNEXPECTED}. */ + public static AdLoadException createForUnexpected(RuntimeException error) { + return new AdLoadException(TYPE_UNEXPECTED, error); + } + + /** The {@link Type} of the ad load exception. */ + public final @Type int type; + + private AdLoadException(@Type int type, Exception cause) { + super(cause); + this.type = type; + } + + /** + * Returns the {@link RuntimeException} that caused the exception if its type is {@link + * #TYPE_UNEXPECTED}. + */ + public RuntimeException getRuntimeExceptionForUnexpected() { + Assertions.checkState(type == TYPE_UNEXPECTED); + return (RuntimeException) Assertions.checkNotNull(getCause()); + } + } + + // Used to identify the content "child" source for CompositeMediaSource. + private static final MediaPeriodId DUMMY_CONTENT_MEDIA_PERIOD_ID = + new MediaPeriodId(/* periodUid= */ new Object()); + + private final MediaSource contentMediaSource; + private final MediaSourceFactory adMediaSourceFactory; + private final AdsLoader adsLoader; + private final AdsLoader.AdViewProvider adViewProvider; + private final Handler mainHandler; + private final Map> maskingMediaPeriodByAdMediaSource; + private final Timeline.Period period; + + // Accessed on the player thread. + @Nullable private ComponentListener componentListener; + @Nullable private Timeline contentTimeline; + @Nullable private AdPlaybackState adPlaybackState; + private @NullableType MediaSource[][] adGroupMediaSources; + private @NullableType Timeline[][] adGroupTimelines; + + /** + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. Ad media is loaded using {@link ProgressiveMediaSource}. + * + * @param contentMediaSource The {@link MediaSource} providing the content to play. + * @param dataSourceFactory Factory for data sources used to load ad media. + * @param adsLoader The loader for ads. + * @param adViewProvider Provider of views for the ad UI. + */ + public AdsMediaSource( + MediaSource contentMediaSource, + DataSource.Factory dataSourceFactory, + AdsLoader adsLoader, + AdsLoader.AdViewProvider adViewProvider) { + this( + contentMediaSource, + new ProgressiveMediaSource.Factory(dataSourceFactory), + adsLoader, + adViewProvider); + } + + /** + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. + * + * @param contentMediaSource The {@link MediaSource} providing the content to play. + * @param adMediaSourceFactory Factory for media sources used to load ad media. + * @param adsLoader The loader for ads. + * @param adViewProvider Provider of views for the ad UI. + */ + public AdsMediaSource( + MediaSource contentMediaSource, + MediaSourceFactory adMediaSourceFactory, + AdsLoader adsLoader, + AdsLoader.AdViewProvider adViewProvider) { + this.contentMediaSource = contentMediaSource; + this.adMediaSourceFactory = adMediaSourceFactory; + this.adsLoader = adsLoader; + this.adViewProvider = adViewProvider; + mainHandler = new Handler(Looper.getMainLooper()); + maskingMediaPeriodByAdMediaSource = new HashMap<>(); + period = new Timeline.Period(); + adGroupMediaSources = new MediaSource[0][]; + adGroupTimelines = new Timeline[0][]; + adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes()); + } + + @Override + @Nullable + public Object getTag() { + return contentMediaSource.getTag(); + } + + @Override + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + ComponentListener componentListener = new ComponentListener(); + this.componentListener = componentListener; + prepareChildSource(DUMMY_CONTENT_MEDIA_PERIOD_ID, contentMediaSource); + mainHandler.post(() -> adsLoader.start(componentListener, adViewProvider)); + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + AdPlaybackState adPlaybackState = Assertions.checkNotNull(this.adPlaybackState); + if (adPlaybackState.adGroupCount > 0 && id.isAd()) { + int adGroupIndex = id.adGroupIndex; + int adIndexInAdGroup = id.adIndexInAdGroup; + Uri adUri = + Assertions.checkNotNull(adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup]); + if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { + int adCount = adIndexInAdGroup + 1; + adGroupMediaSources[adGroupIndex] = + Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount); + adGroupTimelines[adGroupIndex] = Arrays.copyOf(adGroupTimelines[adGroupIndex], adCount); + } + MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; + if (mediaSource == null) { + mediaSource = adMediaSourceFactory.createMediaSource(adUri); + adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = mediaSource; + maskingMediaPeriodByAdMediaSource.put(mediaSource, new ArrayList<>()); + prepareChildSource(id, mediaSource); + } + MaskingMediaPeriod maskingMediaPeriod = + new MaskingMediaPeriod(mediaSource, id, allocator, startPositionUs); + maskingMediaPeriod.setPrepareErrorListener( + new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup)); + List mediaPeriods = maskingMediaPeriodByAdMediaSource.get(mediaSource); + if (mediaPeriods == null) { + Object periodUid = + Assertions.checkNotNull(adGroupTimelines[adGroupIndex][adIndexInAdGroup]) + .getUidOfPeriod(/* periodIndex= */ 0); + MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber); + maskingMediaPeriod.createPeriod(adSourceMediaPeriodId); + } else { + // Keep track of the masking media period so it can be populated with the real media period + // when the source's info becomes available. + mediaPeriods.add(maskingMediaPeriod); + } + return maskingMediaPeriod; + } else { + MaskingMediaPeriod mediaPeriod = + new MaskingMediaPeriod(contentMediaSource, id, allocator, startPositionUs); + mediaPeriod.createPeriod(id); + return mediaPeriod; + } + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + MaskingMediaPeriod maskingMediaPeriod = (MaskingMediaPeriod) mediaPeriod; + List mediaPeriods = + maskingMediaPeriodByAdMediaSource.get(maskingMediaPeriod.mediaSource); + if (mediaPeriods != null) { + mediaPeriods.remove(maskingMediaPeriod); + } + maskingMediaPeriod.releasePeriod(); + } + + @Override + protected void releaseSourceInternal() { + super.releaseSourceInternal(); + Assertions.checkNotNull(componentListener).release(); + componentListener = null; + maskingMediaPeriodByAdMediaSource.clear(); + contentTimeline = null; + adPlaybackState = null; + adGroupMediaSources = new MediaSource[0][]; + adGroupTimelines = new Timeline[0][]; + mainHandler.post(adsLoader::stop); + } + + @Override + protected void onChildSourceInfoRefreshed( + MediaPeriodId mediaPeriodId, MediaSource mediaSource, Timeline timeline) { + if (mediaPeriodId.isAd()) { + int adGroupIndex = mediaPeriodId.adGroupIndex; + int adIndexInAdGroup = mediaPeriodId.adIndexInAdGroup; + onAdSourceInfoRefreshed(mediaSource, adGroupIndex, adIndexInAdGroup, timeline); + } else { + onContentSourceInfoRefreshed(timeline); + } + } + + @Override + protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + MediaPeriodId childId, MediaPeriodId mediaPeriodId) { + // The child id for the content period is just DUMMY_CONTENT_MEDIA_PERIOD_ID. That's why we need + // to forward the reported mediaPeriodId in this case. + return childId.isAd() ? childId : mediaPeriodId; + } + + // Internal methods. + + private void onAdPlaybackState(AdPlaybackState adPlaybackState) { + if (this.adPlaybackState == null) { + adGroupMediaSources = new MediaSource[adPlaybackState.adGroupCount][]; + Arrays.fill(adGroupMediaSources, new MediaSource[0]); + adGroupTimelines = new Timeline[adPlaybackState.adGroupCount][]; + Arrays.fill(adGroupTimelines, new Timeline[0]); + } + this.adPlaybackState = adPlaybackState; + maybeUpdateSourceInfo(); + } + + private void onContentSourceInfoRefreshed(Timeline timeline) { + Assertions.checkArgument(timeline.getPeriodCount() == 1); + contentTimeline = timeline; + maybeUpdateSourceInfo(); + } + + private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex, + int adIndexInAdGroup, Timeline timeline) { + Assertions.checkArgument(timeline.getPeriodCount() == 1); + adGroupTimelines[adGroupIndex][adIndexInAdGroup] = timeline; + List mediaPeriods = maskingMediaPeriodByAdMediaSource.remove(mediaSource); + if (mediaPeriods != null) { + Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); + for (int i = 0; i < mediaPeriods.size(); i++) { + MaskingMediaPeriod mediaPeriod = mediaPeriods.get(i); + MediaPeriodId adSourceMediaPeriodId = + new MediaPeriodId(periodUid, mediaPeriod.id.windowSequenceNumber); + mediaPeriod.createPeriod(adSourceMediaPeriodId); + } + } + maybeUpdateSourceInfo(); + } + + private void maybeUpdateSourceInfo() { + Timeline contentTimeline = this.contentTimeline; + if (adPlaybackState != null && contentTimeline != null) { + adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurations(adGroupTimelines, period)); + Timeline timeline = + adPlaybackState.adGroupCount == 0 + ? contentTimeline + : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState); + refreshSourceInfo(timeline); + } + } + + private static long[][] getAdDurations( + @NullableType Timeline[][] adTimelines, Timeline.Period period) { + long[][] adDurations = new long[adTimelines.length][]; + for (int i = 0; i < adTimelines.length; i++) { + adDurations[i] = new long[adTimelines[i].length]; + for (int j = 0; j < adTimelines[i].length; j++) { + adDurations[i][j] = + adTimelines[i][j] == null + ? C.TIME_UNSET + : adTimelines[i][j].getPeriod(/* periodIndex= */ 0, period).getDurationUs(); + } + } + return adDurations; + } + + /** Listener for component events. All methods are called on the main thread. */ + private final class ComponentListener implements AdsLoader.EventListener { + + private final Handler playerHandler; + + private volatile boolean released; + + /** + * Creates new listener which forwards ad playback states on the creating thread and all other + * events on the external event listener thread. + */ + public ComponentListener() { + playerHandler = new Handler(); + } + + /** Releases the component listener. */ + public void release() { + released = true; + playerHandler.removeCallbacksAndMessages(null); + } + + @Override + public void onAdPlaybackState(final AdPlaybackState adPlaybackState) { + if (released) { + return; + } + playerHandler.post( + () -> { + if (released) { + return; + } + AdsMediaSource.this.onAdPlaybackState(adPlaybackState); + }); + } + + @Override + public void onAdLoadError(final AdLoadException error, DataSpec dataSpec) { + if (released) { + return; + } + createEventDispatcher(/* mediaPeriodId= */ null) + .loadError( + dataSpec, + dataSpec.uri, + /* responseHeaders= */ Collections.emptyMap(), + C.DATA_TYPE_AD, + C.TRACK_TYPE_UNKNOWN, + /* loadDurationMs= */ 0, + /* bytesLoaded= */ 0, + error, + /* wasCanceled= */ true); + } + } + + private final class AdPrepareErrorListener implements MaskingMediaPeriod.PrepareErrorListener { + + private final Uri adUri; + private final int adGroupIndex; + private final int adIndexInAdGroup; + + public AdPrepareErrorListener(Uri adUri, int adGroupIndex, int adIndexInAdGroup) { + this.adUri = adUri; + this.adGroupIndex = adGroupIndex; + this.adIndexInAdGroup = adIndexInAdGroup; + } + + @Override + public void onPrepareError(MediaPeriodId mediaPeriodId, final IOException exception) { + createEventDispatcher(mediaPeriodId) + .loadError( + new DataSpec(adUri), + adUri, + /* responseHeaders= */ Collections.emptyMap(), + C.DATA_TYPE_AD, + C.TRACK_TYPE_UNKNOWN, + /* loadDurationMs= */ 0, + /* bytesLoaded= */ 0, + AdLoadException.createForAd(exception), + /* wasCanceled= */ true); + mainHandler.post( + () -> adsLoader.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception)); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java new file mode 100644 index 000000000000..44f6d0bc6673 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.ads; + +import androidx.annotation.VisibleForTesting; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ForwardingTimeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; + +/** A {@link Timeline} for sources that have ads. */ +@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) +public final class SinglePeriodAdTimeline extends ForwardingTimeline { + + private final AdPlaybackState adPlaybackState; + + /** + * Creates a new timeline with a single period containing ads. + * + * @param contentTimeline The timeline of the content alongside which ads will be played. It must + * have one window and one period. + * @param adPlaybackState The state of the period's ads. + */ + public SinglePeriodAdTimeline(Timeline contentTimeline, AdPlaybackState adPlaybackState) { + super(contentTimeline); + Assertions.checkState(contentTimeline.getPeriodCount() == 1); + Assertions.checkState(contentTimeline.getWindowCount() == 1); + this.adPlaybackState = adPlaybackState; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + timeline.getPeriod(periodIndex, period, setIds); + period.set( + period.id, + period.uid, + period.windowIndex, + period.durationUs, + period.getPositionInWindowUs(), + adPlaybackState); + return period; + } + + @Override + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + window = super.getWindow(windowIndex, window, defaultPositionProjectionUs); + if (window.durationUs == C.TIME_UNSET) { + window.durationUs = adPlaybackState.contentDurationUs; + } + return window; + } + +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java index 1dd16b0cf75e..406cd1617a47 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java @@ -15,6 +15,8 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; @@ -24,6 +26,17 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; */ public abstract class BaseMediaChunk extends MediaChunk { + /** + * The time from which output will begin, or {@link C#TIME_UNSET} if output will begin from the + * start of the chunk. + */ + public final long clippedStartTimeUs; + /** + * The time from which output will end, or {@link C#TIME_UNSET} if output will end at the end of + * the chunk. + */ + public final long clippedEndTimeUs; + private BaseMediaChunkOutput output; private int[] firstSampleIndices; @@ -35,13 +48,27 @@ public abstract class BaseMediaChunk extends MediaChunk { * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. - * @param chunkIndex The index of the chunk. + * @param clippedStartTimeUs The time in the chunk from which output will begin, or {@link + * C#TIME_UNSET} to output from the start of the chunk. + * @param clippedEndTimeUs The time in the chunk from which output will end, or {@link + * C#TIME_UNSET} to output to the end of the chunk. + * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. */ - public BaseMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, - int chunkIndex) { + public BaseMediaChunk( + DataSource dataSource, + DataSpec dataSpec, + Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long startTimeUs, + long endTimeUs, + long clippedStartTimeUs, + long clippedEndTimeUs, + long chunkIndex) { super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); + this.clippedStartTimeUs = clippedStartTimeUs; + this.clippedEndTimeUs = clippedEndTimeUs; } /** diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java new file mode 100644 index 000000000000..398726057810 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk; + +import java.util.NoSuchElementException; + +/** + * Base class for {@link MediaChunkIterator}s. Handles {@link #next()} and {@link #isEnded()}, and + * provides a bounds check for child classes. + */ +public abstract class BaseMediaChunkIterator implements MediaChunkIterator { + + private final long fromIndex; + private final long toIndex; + + private long currentIndex; + + /** + * Creates base iterator. + * + * @param fromIndex The first available index. + * @param toIndex The last available index. + */ + @SuppressWarnings("method.invocation.invalid") + public BaseMediaChunkIterator(long fromIndex, long toIndex) { + this.fromIndex = fromIndex; + this.toIndex = toIndex; + reset(); + } + + @Override + public boolean isEnded() { + return currentIndex > toIndex; + } + + @Override + public boolean next() { + currentIndex++; + return !isEnded(); + } + + @Override + public void reset() { + currentIndex = fromIndex - 1; + } + + /** + * Verifies that the iterator points to a valid element. + * + * @throws NoSuchElementException If the iterator does not point to a valid element. + */ + protected final void checkInBounds() { + if (currentIndex < fromIndex || currentIndex > toIndex) { + throw new NoSuchElementException(); + } + } + + /** Returns the current index this iterator is pointing to. */ + protected final long getCurrentIndex() { + return currentIndex; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java index 510620bea964..5d1f93bf011b 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java @@ -15,36 +15,37 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk; -import android.util.Log; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultTrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DummyTrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; /** - * An output for {@link BaseMediaChunk}s. + * A {@link TrackOutputProvider} that provides {@link TrackOutput TrackOutputs} based on a + * predefined mapping from track type to output. */ -/* package */ final class BaseMediaChunkOutput implements TrackOutputProvider { +public final class BaseMediaChunkOutput implements TrackOutputProvider { private static final String TAG = "BaseMediaChunkOutput"; private final int[] trackTypes; - private final DefaultTrackOutput[] trackOutputs; + private final SampleQueue[] sampleQueues; /** * @param trackTypes The track types of the individual track outputs. - * @param trackOutputs The individual track outputs. + * @param sampleQueues The individual sample queues. */ - public BaseMediaChunkOutput(int[] trackTypes, DefaultTrackOutput[] trackOutputs) { + public BaseMediaChunkOutput(int[] trackTypes, SampleQueue[] sampleQueues) { this.trackTypes = trackTypes; - this.trackOutputs = trackOutputs; + this.sampleQueues = sampleQueues; } @Override public TrackOutput track(int id, int type) { for (int i = 0; i < trackTypes.length; i++) { if (type == trackTypes[i]) { - return trackOutputs[i]; + return sampleQueues[i]; } } Log.e(TAG, "Unmatched track of type: " + type); @@ -52,13 +53,13 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.ChunkEx } /** - * Returns the current absolute write indices of the individual track outputs. + * Returns the current absolute write indices of the individual sample queues. */ public int[] getWriteIndices() { - int[] writeIndices = new int[trackOutputs.length]; - for (int i = 0; i < trackOutputs.length; i++) { - if (trackOutputs[i] != null) { - writeIndices[i] = trackOutputs[i].getWriteIndex(); + int[] writeIndices = new int[sampleQueues.length]; + for (int i = 0; i < sampleQueues.length; i++) { + if (sampleQueues[i] != null) { + writeIndices[i] = sampleQueues[i].getWriteIndex(); } } return writeIndices; @@ -66,12 +67,12 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.ChunkEx /** * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples - * subsequently written to the track outputs. + * subsequently written to the sample queues. */ public void setSampleOffsetUs(long sampleOffsetUs) { - for (DefaultTrackOutput trackOutput : trackOutputs) { - if (trackOutput != null) { - trackOutput.setSampleOffsetUs(sampleOffsetUs); + for (SampleQueue sampleQueue : sampleQueues) { + if (sampleQueue != null) { + sampleQueue.setSampleOffsetUs(sampleOffsetUs); } } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.java index fb75d758100c..3f4450eddd4f 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.java @@ -15,12 +15,17 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk; +import android.net.Uri; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.Loadable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.StatsDataSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.util.List; +import java.util.Map; /** * An abstract base class for {@link Loadable} implementations that load chunks of data required @@ -51,7 +56,7 @@ public abstract class Chunk implements Loadable { * Optional data associated with the selection of the track to which this chunk belongs. Null if * the chunk does not belong to a track. */ - public final Object trackSelectionData; + @Nullable public final Object trackSelectionData; /** * The start time of the media contained by the chunk, or {@link C#TIME_UNSET} if the data * being loaded does not contain media samples. @@ -63,7 +68,7 @@ public abstract class Chunk implements Loadable { */ public final long endTimeUs; - protected final DataSource dataSource; + protected final StatsDataSource dataSource; /** * @param dataSource The source from which the data should be loaded. @@ -75,9 +80,16 @@ public abstract class Chunk implements Loadable { * @param startTimeUs See {@link #startTimeUs}. * @param endTimeUs See {@link #endTimeUs}. */ - public Chunk(DataSource dataSource, DataSpec dataSpec, int type, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs) { - this.dataSource = Assertions.checkNotNull(dataSource); + public Chunk( + DataSource dataSource, + DataSpec dataSpec, + int type, + Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long startTimeUs, + long endTimeUs) { + this.dataSource = new StatsDataSource(dataSource); this.dataSpec = Assertions.checkNotNull(dataSpec); this.type = type; this.trackFormat = trackFormat; @@ -95,8 +107,31 @@ public abstract class Chunk implements Loadable { } /** - * Returns the number of bytes that have been loaded. + * Returns the number of bytes that have been loaded. Must only be called after the load + * completed, failed, or was canceled. */ - public abstract long bytesLoaded(); + public final long bytesLoaded() { + return dataSource.getBytesRead(); + } + /** + * Returns the {@link Uri} associated with the last {@link DataSource#open} call. If redirection + * occurred, this is the redirected uri. Must only be called after the load completed, failed, or + * was canceled. + * + * @see DataSource#getUri() + */ + public final Uri getUri() { + return dataSource.getLastOpenedUri(); + } + + /** + * Returns the response headers associated with the last {@link DataSource#open} call. Must only + * be called after the load completed, failed, or was canceled. + * + * @see DataSource#getResponseHeaders() + */ + public final Map> getResponseHeaders() { + return dataSource.getLastResponseHeaders(); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index ed66714d3024..04cef9198c96 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -16,6 +16,7 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk; import android.util.SparseArray; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DummyTrackOutput; @@ -29,9 +30,10 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArr import java.io.IOException; /** - * An {@link Extractor} wrapper for loading chunks containing a single track. + * An {@link Extractor} wrapper for loading chunks that contain a single primary track, and possibly + * additional embedded tracks. *

- * The wrapper allows switching of the {@link TrackOutput} that receives parsed data. + * The wrapper allows switching of the {@link TrackOutput}s that receive parsed data. */ public final class ChunkExtractorWrapper implements ExtractorOutput { @@ -47,7 +49,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { * * @param id A track identifier. * @param type The type of the track. Typically one of the - * {@link org.mozilla.thirdparty.com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. + * {@link org.mozilla.thirdparty.com.google.android.exoplayer2C} {@code TRACK_TYPE_*} constants. * @return The {@link TrackOutput} for the given track identifier. */ TrackOutput track(int id, int type); @@ -56,22 +58,28 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { public final Extractor extractor; - private final Format manifestFormat; + private final int primaryTrackType; + private final Format primaryTrackManifestFormat; private final SparseArray bindingTrackOutputs; private boolean extractorInitialized; private TrackOutputProvider trackOutputProvider; + private long endTimeUs; private SeekMap seekMap; private Format[] sampleFormats; /** * @param extractor The extractor to wrap. - * @param manifestFormat A manifest defined {@link Format} whose data should be merged into any - * sample {@link Format} output from the {@link Extractor}. + * @param primaryTrackType The type of the primary track. Typically one of the + * {@link org.mozilla.thirdparty.com.google.android.exoplayer2C} {@code TRACK_TYPE_*} constants. + * @param primaryTrackManifestFormat A manifest defined {@link Format} whose data should be merged + * into any sample {@link Format} output from the {@link Extractor} for the primary track. */ - public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat) { + public ChunkExtractorWrapper(Extractor extractor, int primaryTrackType, + Format primaryTrackManifestFormat) { this.extractor = extractor; - this.manifestFormat = manifestFormat; + this.primaryTrackType = primaryTrackType; + this.primaryTrackManifestFormat = primaryTrackManifestFormat; bindingTrackOutputs = new SparseArray<>(); } @@ -90,20 +98,29 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { } /** - * Initializes the extractor to output to the provided {@link TrackOutput}, and configures it to - * receive data from a new chunk. + * Initializes the wrapper to output to {@link TrackOutput}s provided by the specified {@link + * TrackOutputProvider}, and configures the extractor to receive data from a new chunk. * * @param trackOutputProvider The provider of {@link TrackOutput}s that will receive sample data. + * @param startTimeUs The start position in the new chunk, or {@link C#TIME_UNSET} to output + * samples from the start of the chunk. + * @param endTimeUs The end position in the new chunk, or {@link C#TIME_UNSET} to output samples + * to the end of the chunk. */ - public void init(TrackOutputProvider trackOutputProvider) { + public void init( + @Nullable TrackOutputProvider trackOutputProvider, long startTimeUs, long endTimeUs) { this.trackOutputProvider = trackOutputProvider; + this.endTimeUs = endTimeUs; if (!extractorInitialized) { extractor.init(this); + if (startTimeUs != C.TIME_UNSET) { + extractor.seek(/* position= */ 0, startTimeUs); + } extractorInitialized = true; } else { - extractor.seek(0, 0); + extractor.seek(/* position= */ 0, startTimeUs == C.TIME_UNSET ? 0 : startTimeUs); for (int i = 0; i < bindingTrackOutputs.size(); i++) { - bindingTrackOutputs.valueAt(i).bind(trackOutputProvider); + bindingTrackOutputs.valueAt(i).bind(trackOutputProvider, endTimeUs); } } } @@ -116,8 +133,10 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { if (bindingTrackOutput == null) { // Assert that if we're seeing a new track we have not seen endTracks. Assertions.checkState(sampleFormats == null); - bindingTrackOutput = new BindingTrackOutput(id, type, manifestFormat); - bindingTrackOutput.bind(trackOutputProvider); + // TODO: Manifest formats for embedded tracks should also be passed here. + bindingTrackOutput = new BindingTrackOutput(id, type, + type == primaryTrackType ? primaryTrackManifestFormat : null); + bindingTrackOutput.bind(trackOutputProvider, endTimeUs); bindingTrackOutputs.put(id, bindingTrackOutput); } return bindingTrackOutput; @@ -144,32 +163,35 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { private final int id; private final int type; private final Format manifestFormat; + private final DummyTrackOutput dummyTrackOutput; public Format sampleFormat; private TrackOutput trackOutput; + private long endTimeUs; public BindingTrackOutput(int id, int type, Format manifestFormat) { this.id = id; this.type = type; this.manifestFormat = manifestFormat; + dummyTrackOutput = new DummyTrackOutput(); } - public void bind(TrackOutputProvider trackOutputProvider) { + public void bind(TrackOutputProvider trackOutputProvider, long endTimeUs) { if (trackOutputProvider == null) { - trackOutput = new DummyTrackOutput(); + trackOutput = dummyTrackOutput; return; } + this.endTimeUs = endTimeUs; trackOutput = trackOutputProvider.track(id, type); - if (trackOutput != null) { + if (sampleFormat != null) { trackOutput.format(sampleFormat); } } @Override public void format(Format format) { - // TODO: This should only happen for the primary track. Additional metadata/text tracks need - // to be copied with different manifest derived formats. - sampleFormat = format.copyWithManifestFormatInfo(manifestFormat); + sampleFormat = manifestFormat != null ? format.copyWithManifestFormatInfo(manifestFormat) + : format; trackOutput.format(sampleFormat); } @@ -186,8 +208,11 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { @Override public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { - trackOutput.sampleMetadata(timeUs, flags, size, offset, encryptionKey); + CryptoData cryptoData) { + if (endTimeUs != C.TIME_UNSET && timeUs >= endTimeUs) { + trackOutput = dummyTrackOutput; + } + trackOutput.sampleMetadata(timeUs, flags, size, offset, cryptoData); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkHolder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkHolder.java index da8c3d458da1..ef9daddd2c70 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkHolder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkHolder.java @@ -15,15 +15,15 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; + /** * Holds a chunk or an indication that the end of the stream has been reached. */ public final class ChunkHolder { - /** - * The chunk. - */ - public Chunk chunk; + /** The chunk. */ + @Nullable public Chunk chunk; /** * Indicates that the end of the stream has been reached. diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 99b6300143ee..a789805cd76d 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -15,20 +15,28 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultTrackOutput; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleStream; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SequenceableLoader; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; /** @@ -36,88 +44,130 @@ import java.util.List; * May also be configured to expose additional embedded {@link SampleStream}s. */ public class ChunkSampleStream implements SampleStream, SequenceableLoader, - Loader.Callback { + Loader.Callback, Loader.ReleaseCallback { - private final int primaryTrackType; - private final int[] embeddedTrackTypes; + /** A callback to be notified when a sample stream has finished being released. */ + public interface ReleaseCallback { + + /** + * Called when the {@link ChunkSampleStream} has finished being released. + * + * @param chunkSampleStream The released sample stream. + */ + void onSampleStreamReleased(ChunkSampleStream chunkSampleStream); + } + + private static final String TAG = "ChunkSampleStream"; + + public final int primaryTrackType; + + @Nullable private final int[] embeddedTrackTypes; + @Nullable private final Format[] embeddedTrackFormats; private final boolean[] embeddedTracksSelected; private final T chunkSource; private final SequenceableLoader.Callback> callback; private final EventDispatcher eventDispatcher; - private final int minLoadableRetryCount; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final Loader loader; private final ChunkHolder nextChunkHolder; - private final LinkedList mediaChunks; + private final ArrayList mediaChunks; private final List readOnlyMediaChunks; - private final DefaultTrackOutput primarySampleQueue; - private final DefaultTrackOutput[] embeddedSampleQueues; - private final BaseMediaChunkOutput mediaChunkOutput; + private final SampleQueue primarySampleQueue; + private final SampleQueue[] embeddedSampleQueues; + private final BaseMediaChunkOutput chunkOutput; private Format primaryDownstreamTrackFormat; + @Nullable private ReleaseCallback releaseCallback; private long pendingResetPositionUs; - /* package */ long lastSeekPositionUs; + private long lastSeekPositionUs; + private int nextNotifyPrimaryFormatMediaChunkIndex; + + /* package */ long decodeOnlyUntilPositionUs; /* package */ boolean loadingFinished; /** - * @param primaryTrackType The type of the primary track. One of the {@link C} - * {@code TRACK_TYPE_*} constants. + * Constructs an instance. + * + * @param primaryTrackType The type of the primary track. One of the {@link C} {@code + * TRACK_TYPE_*} constants. * @param embeddedTrackTypes The types of any embedded tracks, or null. + * @param embeddedTrackFormats The formats of the embedded tracks, or null. * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. * @param callback An {@link Callback} for the stream. * @param allocator An {@link Allocator} from which allocations can be obtained. * @param positionUs The position from which to start loading media. - * @param minLoadableRetryCount The minimum number of times that the source should retry a load - * before propagating an error. + * @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions} + * from. + * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. * @param eventDispatcher A dispatcher to notify of events. */ - public ChunkSampleStream(int primaryTrackType, int[] embeddedTrackTypes, T chunkSource, - Callback> callback, Allocator allocator, long positionUs, - int minLoadableRetryCount, EventDispatcher eventDispatcher) { + public ChunkSampleStream( + int primaryTrackType, + @Nullable int[] embeddedTrackTypes, + @Nullable Format[] embeddedTrackFormats, + T chunkSource, + Callback> callback, + Allocator allocator, + long positionUs, + DrmSessionManager drmSessionManager, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, + EventDispatcher eventDispatcher) { this.primaryTrackType = primaryTrackType; this.embeddedTrackTypes = embeddedTrackTypes; + this.embeddedTrackFormats = embeddedTrackFormats; this.chunkSource = chunkSource; this.callback = callback; this.eventDispatcher = eventDispatcher; - this.minLoadableRetryCount = minLoadableRetryCount; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; loader = new Loader("Loader:ChunkSampleStream"); nextChunkHolder = new ChunkHolder(); - mediaChunks = new LinkedList<>(); + mediaChunks = new ArrayList<>(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; - embeddedSampleQueues = new DefaultTrackOutput[embeddedTrackCount]; + embeddedSampleQueues = new SampleQueue[embeddedTrackCount]; embeddedTracksSelected = new boolean[embeddedTrackCount]; int[] trackTypes = new int[1 + embeddedTrackCount]; - DefaultTrackOutput[] sampleQueues = new DefaultTrackOutput[1 + embeddedTrackCount]; + SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount]; - primarySampleQueue = new DefaultTrackOutput(allocator); + primarySampleQueue = new SampleQueue(allocator, drmSessionManager); trackTypes[0] = primaryTrackType; sampleQueues[0] = primarySampleQueue; for (int i = 0; i < embeddedTrackCount; i++) { - DefaultTrackOutput trackOutput = new DefaultTrackOutput(allocator); - embeddedSampleQueues[i] = trackOutput; - sampleQueues[i + 1] = trackOutput; + SampleQueue sampleQueue = + new SampleQueue(allocator, DrmSessionManager.getDummyDrmSessionManager()); + embeddedSampleQueues[i] = sampleQueue; + sampleQueues[i + 1] = sampleQueue; trackTypes[i + 1] = embeddedTrackTypes[i]; } - mediaChunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); + chunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); pendingResetPositionUs = positionUs; lastSeekPositionUs = positionUs; } /** - * Discards buffered media for embedded tracks that are not currently selected, up to the - * specified position. + * Discards buffered media up to the specified position. * * @param positionUs The position to discard up to, in microseconds. + * @param toKeyframe If true then for each track discards samples up to the keyframe before or at + * the specified position, rather than any sample before or at that position. */ - public void discardUnselectedEmbeddedTracksTo(long positionUs) { - for (int i = 0; i < embeddedSampleQueues.length; i++) { - if (!embeddedTracksSelected[i]) { - embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true); + public void discardBuffer(long positionUs, boolean toKeyframe) { + if (isPendingReset()) { + return; + } + int oldFirstSampleIndex = primarySampleQueue.getFirstIndex(); + primarySampleQueue.discardTo(positionUs, toKeyframe, true); + int newFirstSampleIndex = primarySampleQueue.getFirstIndex(); + if (newFirstSampleIndex > oldFirstSampleIndex) { + long discardToUs = primarySampleQueue.getFirstTimestampUs(); + for (int i = 0; i < embeddedSampleQueues.length; i++) { + embeddedSampleQueues[i].discardTo(discardToUs, toKeyframe, embeddedTracksSelected[i]); } } + discardDownstreamMediaChunks(newFirstSampleIndex); } /** @@ -135,7 +185,7 @@ public class ChunkSampleStream implements SampleStream, S if (embeddedTrackTypes[i] == trackType) { Assertions.checkState(!embeddedTracksSelected[i]); embeddedTracksSelected[i] = true; - embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true); + embeddedSampleQueues[i].seekTo(positionUs, /* allowTimeBeyondBuffer= */ true); return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i); } } @@ -156,6 +206,7 @@ public class ChunkSampleStream implements SampleStream, S * @return An estimate of the absolute position in microseconds up to which data is buffered, or * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered. */ + @Override public long getBufferedPositionUs() { if (loadingFinished) { return C.TIME_END_OF_SOURCE; @@ -163,7 +214,7 @@ public class ChunkSampleStream implements SampleStream, S return pendingResetPositionUs; } else { long bufferedPositionUs = lastSeekPositionUs; - BaseMediaChunk lastMediaChunk = mediaChunks.getLast(); + BaseMediaChunk lastMediaChunk = getLastMediaChunk(); BaseMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null; if (lastCompletedMediaChunk != null) { @@ -173,6 +224,18 @@ public class ChunkSampleStream implements SampleStream, S } } + /** + * Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used + * as sync points. + * + * @param positionUs The seek position in microseconds. + * @param seekParameters Parameters that control how the seek is performed. + * @return The adjusted seek position, in microseconds. + */ + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return chunkSource.getAdjustedSeekPositionUs(positionUs, seekParameters); + } + /** * Seeks to the specified position in microseconds. * @@ -180,32 +243,63 @@ public class ChunkSampleStream implements SampleStream, S */ public void seekToUs(long positionUs) { lastSeekPositionUs = positionUs; - // If we're not pending a reset, see if we can seek within the primary sample queue. - boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.skipToKeyframeBefore( - positionUs, positionUs < getNextLoadPositionUs()); - if (seekInsideBuffer) { - // We succeeded. We need to discard any chunks that we've moved past and perform the seek for - // any embedded streams as well. - while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex(0) <= primarySampleQueue.getReadIndex()) { - mediaChunks.removeFirst(); + if (isPendingReset()) { + // A reset is already pending. We only need to update its position. + pendingResetPositionUs = positionUs; + return; + } + + // Detect whether the seek is to the start of a chunk that's at least partially buffered. + BaseMediaChunk seekToMediaChunk = null; + for (int i = 0; i < mediaChunks.size(); i++) { + BaseMediaChunk mediaChunk = mediaChunks.get(i); + long mediaChunkStartTimeUs = mediaChunk.startTimeUs; + if (mediaChunkStartTimeUs == positionUs && mediaChunk.clippedStartTimeUs == C.TIME_UNSET) { + seekToMediaChunk = mediaChunk; + break; + } else if (mediaChunkStartTimeUs > positionUs) { + // We're not going to find a chunk with a matching start time. + break; } - // TODO: For this to work correctly, the embedded streams must not discard anything from their - // sample queues beyond the current read position of the primary stream. - for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.skipToKeyframeBefore(positionUs, true); + } + + // See if we can seek inside the primary sample queue. + boolean seekInsideBuffer; + if (seekToMediaChunk != null) { + // When seeking to the start of a chunk we use the index of the first sample in the chunk + // rather than the seek position. This ensures we seek to the keyframe at the start of the + // chunk even if the sample timestamps are slightly offset from the chunk start times. + seekInsideBuffer = primarySampleQueue.seekTo(seekToMediaChunk.getFirstSampleIndex(0)); + decodeOnlyUntilPositionUs = 0; + } else { + seekInsideBuffer = + primarySampleQueue.seekTo( + positionUs, /* allowTimeBeyondBuffer= */ positionUs < getNextLoadPositionUs()); + decodeOnlyUntilPositionUs = lastSeekPositionUs; + } + + if (seekInsideBuffer) { + // We can seek inside the buffer. + nextNotifyPrimaryFormatMediaChunkIndex = + primarySampleIndexToMediaChunkIndex( + primarySampleQueue.getReadIndex(), /* minChunkIndex= */ 0); + // Seek the embedded sample queues. + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ true); } } else { - // We failed, and need to restart. + // We can't seek inside the buffer, and so need to reset. pendingResetPositionUs = positionUs; loadingFinished = false; mediaChunks.clear(); + nextNotifyPrimaryFormatMediaChunkIndex = 0; if (loader.isLoading()) { loader.cancelLoading(); } else { - primarySampleQueue.reset(true); - for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.reset(true); + loader.clearFatalError(); + primarySampleQueue.reset(); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(); } } } @@ -213,27 +307,55 @@ public class ChunkSampleStream implements SampleStream, S /** * Releases the stream. - *

- * This method should be called when the stream is no longer required. + * + *

This method should be called when the stream is no longer required. Either this method or + * {@link #release(ReleaseCallback)} can be used to release this stream. */ public void release() { - primarySampleQueue.disable(); - for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.disable(); + release(null); + } + + /** + * Releases the stream. + * + *

This method should be called when the stream is no longer required. Either this method or + * {@link #release()} can be used to release this stream. + * + * @param callback An optional callback to be called on the loading thread once the loader has + * been released. + */ + public void release(@Nullable ReleaseCallback callback) { + this.releaseCallback = callback; + // Discard as much as we can synchronously. + primarySampleQueue.preRelease(); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.preRelease(); + } + loader.release(this); + } + + @Override + public void onLoaderReleased() { + primarySampleQueue.release(); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.release(); + } + if (releaseCallback != null) { + releaseCallback.onSampleStreamReleased(this); } - loader.release(); } // SampleStream implementation. @Override public boolean isReady() { - return loadingFinished || (!isPendingReset() && !primarySampleQueue.isEmpty()); + return !isPendingReset() && primarySampleQueue.isReady(loadingFinished); } @Override public void maybeThrowError() throws IOException { loader.maybeThrowError(); + primarySampleQueue.maybeThrowError(); if (!loader.isLoading()) { chunkSource.maybeThrowError(); } @@ -245,18 +367,25 @@ public class ChunkSampleStream implements SampleStream, S if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - discardDownstreamMediaChunks(primarySampleQueue.getReadIndex()); - return primarySampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, - lastSeekPositionUs); + maybeNotifyPrimaryTrackFormatChanged(); + + return primarySampleQueue.read( + formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); } @Override - public void skipData(long positionUs) { - if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { - primarySampleQueue.skipAll(); - } else { - primarySampleQueue.skipToKeyframeBefore(positionUs, true); + public int skipData(long positionUs) { + if (isPendingReset()) { + return 0; } + int skipCount; + if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { + skipCount = primarySampleQueue.advanceToEnd(); + } else { + skipCount = primarySampleQueue.advanceTo(positionUs); + } + maybeNotifyPrimaryTrackFormatChanged(); + return skipCount; } // Loader.Callback implementation. @@ -264,9 +393,19 @@ public class ChunkSampleStream implements SampleStream, S @Override public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) { chunkSource.onChunkLoadCompleted(loadable); - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, primaryTrackType, - loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, - loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, + eventDispatcher.loadCompleted( + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + loadable.type, + primaryTrackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); callback.onContinueLoadingRequested(this); } @@ -274,68 +413,121 @@ public class ChunkSampleStream implements SampleStream, S @Override public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled(loadable.dataSpec, loadable.type, primaryTrackType, - loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, - loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, + eventDispatcher.loadCanceled( + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + loadable.type, + primaryTrackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); if (!released) { - primarySampleQueue.reset(true); - for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.reset(true); + primarySampleQueue.reset(); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(); } callback.onContinueLoadingRequested(this); } } @Override - public int onLoadError(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, - IOException error) { + public LoadErrorAction onLoadError( + Chunk loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error, + int errorCount) { long bytesLoaded = loadable.bytesLoaded(); boolean isMediaChunk = isMediaChunk(loadable); - boolean cancelable = !isMediaChunk || bytesLoaded == 0 || mediaChunks.size() > 1; - boolean canceled = false; - if (chunkSource.onChunkLoadError(loadable, cancelable, error)) { - canceled = true; - if (isMediaChunk) { - BaseMediaChunk removed = mediaChunks.removeLast(); - Assertions.checkState(removed == loadable); - primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); - for (int i = 0; i < embeddedSampleQueues.length; i++) { - embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); - } - if (mediaChunks.isEmpty()) { - pendingResetPositionUs = lastSeekPositionUs; + int lastChunkIndex = mediaChunks.size() - 1; + boolean cancelable = + bytesLoaded == 0 || !isMediaChunk || !haveReadFromMediaChunk(lastChunkIndex); + long blacklistDurationMs = + cancelable + ? loadErrorHandlingPolicy.getBlacklistDurationMsFor( + loadable.type, loadDurationMs, error, errorCount) + : C.TIME_UNSET; + LoadErrorAction loadErrorAction = null; + if (chunkSource.onChunkLoadError(loadable, cancelable, error, blacklistDurationMs)) { + if (cancelable) { + loadErrorAction = Loader.DONT_RETRY; + if (isMediaChunk) { + BaseMediaChunk removed = discardUpstreamMediaChunksFromIndex(lastChunkIndex); + Assertions.checkState(removed == loadable); + if (mediaChunks.isEmpty()) { + pendingResetPositionUs = lastSeekPositionUs; + } } + } else { + Log.w(TAG, "Ignoring attempt to cancel non-cancelable load."); } } - eventDispatcher.loadError(loadable.dataSpec, loadable.type, primaryTrackType, - loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, - loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, bytesLoaded, - error, canceled); + + if (loadErrorAction == null) { + // The load was not cancelled. Either the load must be retried or the error propagated. + long retryDelayMs = + loadErrorHandlingPolicy.getRetryDelayMsFor( + loadable.type, loadDurationMs, error, errorCount); + loadErrorAction = + retryDelayMs != C.TIME_UNSET + ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs) + : Loader.DONT_RETRY_FATAL; + } + + boolean canceled = !loadErrorAction.isRetry(); + eventDispatcher.loadError( + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + loadable.type, + primaryTrackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded, + error, + canceled); if (canceled) { callback.onContinueLoadingRequested(this); - return Loader.DONT_RETRY; - } else { - return Loader.RETRY; } + return loadErrorAction; } // SequenceableLoader implementation @Override public boolean continueLoading(long positionUs) { - if (loadingFinished || loader.isLoading()) { + if (loadingFinished || loader.isLoading() || loader.hasFatalError()) { return false; } - chunkSource.getNextChunk(mediaChunks.isEmpty() ? null : mediaChunks.getLast(), - pendingResetPositionUs != C.TIME_UNSET ? pendingResetPositionUs : positionUs, - nextChunkHolder); + boolean pendingReset = isPendingReset(); + List chunkQueue; + long loadPositionUs; + if (pendingReset) { + chunkQueue = Collections.emptyList(); + loadPositionUs = pendingResetPositionUs; + } else { + chunkQueue = readOnlyMediaChunks; + loadPositionUs = getLastMediaChunk().endTimeUs; + } + chunkSource.getNextChunk(positionUs, loadPositionUs, chunkQueue, nextChunkHolder); boolean endOfStream = nextChunkHolder.endOfStream; Chunk loadable = nextChunkHolder.chunk; nextChunkHolder.clear(); if (endOfStream) { + pendingResetPositionUs = C.TIME_UNSET; loadingFinished = true; return true; } @@ -345,55 +537,128 @@ public class ChunkSampleStream implements SampleStream, S } if (isMediaChunk(loadable)) { - pendingResetPositionUs = C.TIME_UNSET; BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable; - mediaChunk.init(mediaChunkOutput); + if (pendingReset) { + boolean resetToMediaChunk = mediaChunk.startTimeUs == pendingResetPositionUs; + // Only enable setting of the decode only flag if we're not resetting to a chunk boundary. + decodeOnlyUntilPositionUs = resetToMediaChunk ? 0 : pendingResetPositionUs; + pendingResetPositionUs = C.TIME_UNSET; + } + mediaChunk.init(chunkOutput); mediaChunks.add(mediaChunk); + } else if (loadable instanceof InitializationChunk) { + ((InitializationChunk) loadable).init(chunkOutput); } - long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount); - eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, primaryTrackType, - loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, - loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs); + long elapsedRealtimeMs = + loader.startLoading( + loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)); + eventDispatcher.loadStarted( + loadable.dataSpec, + loadable.type, + primaryTrackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs); return true; } + @Override + public boolean isLoading() { + return loader.isLoading(); + } + @Override public long getNextLoadPositionUs() { if (isPendingReset()) { return pendingResetPositionUs; } else { - return loadingFinished ? C.TIME_END_OF_SOURCE : mediaChunks.getLast().endTimeUs; + return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs; } } + @Override + public void reevaluateBuffer(long positionUs) { + if (loader.isLoading() || loader.hasFatalError() || isPendingReset()) { + return; + } + + int currentQueueSize = mediaChunks.size(); + int preferredQueueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); + if (currentQueueSize <= preferredQueueSize) { + return; + } + + int newQueueSize = currentQueueSize; + for (int i = preferredQueueSize; i < currentQueueSize; i++) { + if (!haveReadFromMediaChunk(i)) { + newQueueSize = i; + break; + } + } + if (newQueueSize == currentQueueSize) { + return; + } + + long endTimeUs = getLastMediaChunk().endTimeUs; + BaseMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(newQueueSize); + if (mediaChunks.isEmpty()) { + pendingResetPositionUs = lastSeekPositionUs; + } + loadingFinished = false; + eventDispatcher.upstreamDiscarded(primaryTrackType, firstRemovedChunk.startTimeUs, endTimeUs); + } + // Internal methods - // TODO[REFACTOR]: Call maybeDiscardUpstream for DASH and SmoothStreaming. - /** - * Discards media chunks from the back of the buffer if conditions have changed such that it's - * preferable to re-buffer the media at a different quality. - * - * @param positionUs The current playback position in microseconds. - */ - private void maybeDiscardUpstream(long positionUs) { - int queueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); - discardUpstreamMediaChunks(Math.max(1, queueSize)); - } - private boolean isMediaChunk(Chunk chunk) { return chunk instanceof BaseMediaChunk; } + /** Returns whether samples have been read from media chunk at given index. */ + private boolean haveReadFromMediaChunk(int mediaChunkIndex) { + BaseMediaChunk mediaChunk = mediaChunks.get(mediaChunkIndex); + if (primarySampleQueue.getReadIndex() > mediaChunk.getFirstSampleIndex(0)) { + return true; + } + for (int i = 0; i < embeddedSampleQueues.length; i++) { + if (embeddedSampleQueues[i].getReadIndex() > mediaChunk.getFirstSampleIndex(i + 1)) { + return true; + } + } + return false; + } + /* package */ boolean isPendingReset() { return pendingResetPositionUs != C.TIME_UNSET; } - private void discardDownstreamMediaChunks(int primaryStreamReadIndex) { - while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex(0) <= primaryStreamReadIndex) { - mediaChunks.removeFirst(); + private void discardDownstreamMediaChunks(int discardToSampleIndex) { + int discardToMediaChunkIndex = + primarySampleIndexToMediaChunkIndex(discardToSampleIndex, /* minChunkIndex= */ 0); + // Don't discard any chunks that we haven't reported the primary format change for yet. + discardToMediaChunkIndex = + Math.min(discardToMediaChunkIndex, nextNotifyPrimaryFormatMediaChunkIndex); + if (discardToMediaChunkIndex > 0) { + Util.removeRange(mediaChunks, /* fromIndex= */ 0, /* toIndex= */ discardToMediaChunkIndex); + nextNotifyPrimaryFormatMediaChunkIndex -= discardToMediaChunkIndex; } - BaseMediaChunk currentChunk = mediaChunks.getFirst(); + } + + private void maybeNotifyPrimaryTrackFormatChanged() { + int readSampleIndex = primarySampleQueue.getReadIndex(); + int notifyToMediaChunkIndex = + primarySampleIndexToMediaChunkIndex( + readSampleIndex, /* minChunkIndex= */ nextNotifyPrimaryFormatMediaChunkIndex - 1); + while (nextNotifyPrimaryFormatMediaChunkIndex <= notifyToMediaChunkIndex) { + maybeNotifyPrimaryTrackFormatChanged(nextNotifyPrimaryFormatMediaChunkIndex++); + } + } + + private void maybeNotifyPrimaryTrackFormatChanged(int mediaChunkReadIndex) { + BaseMediaChunk currentChunk = mediaChunks.get(mediaChunkReadIndex); Format trackFormat = currentChunk.trackFormat; if (!trackFormat.equals(primaryDownstreamTrackFormat)) { eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat, @@ -404,29 +669,47 @@ public class ChunkSampleStream implements SampleStream, S } /** - * Discard upstream media chunks until the queue length is equal to the length specified. + * Returns the media chunk index corresponding to a given primary sample index. * - * @param queueLength The desired length of the queue. - * @return Whether chunks were discarded. + * @param primarySampleIndex The primary sample index for which the corresponding media chunk + * index is required. + * @param minChunkIndex A minimum chunk index from which to start searching, or -1 if no hint can + * be provided. + * @return The index of the media chunk corresponding to the sample index, or -1 if the list of + * media chunks is empty, or {@code minChunkIndex} if the sample precedes the first chunk in + * the search (i.e. the chunk at {@code minChunkIndex}, or at index 0 if {@code minChunkIndex} + * is -1. */ - private boolean discardUpstreamMediaChunks(int queueLength) { - if (mediaChunks.size() <= queueLength) { - return false; + private int primarySampleIndexToMediaChunkIndex(int primarySampleIndex, int minChunkIndex) { + for (int i = minChunkIndex + 1; i < mediaChunks.size(); i++) { + if (mediaChunks.get(i).getFirstSampleIndex(0) > primarySampleIndex) { + return i - 1; + } } - long startTimeUs = 0; - long endTimeUs = mediaChunks.getLast().endTimeUs; - BaseMediaChunk removed = null; - while (mediaChunks.size() > queueLength) { - removed = mediaChunks.removeLast(); - startTimeUs = removed.startTimeUs; - loadingFinished = false; - } - primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); + return mediaChunks.size() - 1; + } + + private BaseMediaChunk getLastMediaChunk() { + return mediaChunks.get(mediaChunks.size() - 1); + } + + /** + * Discard upstream media chunks from {@code chunkIndex} and corresponding samples from sample + * queues. + * + * @param chunkIndex The index of the first chunk to discard. + * @return The chunk at given index. + */ + private BaseMediaChunk discardUpstreamMediaChunksFromIndex(int chunkIndex) { + BaseMediaChunk firstRemovedChunk = mediaChunks.get(chunkIndex); + Util.removeRange(mediaChunks, /* fromIndex= */ chunkIndex, /* toIndex= */ mediaChunks.size()); + nextNotifyPrimaryFormatMediaChunkIndex = + Math.max(nextNotifyPrimaryFormatMediaChunkIndex, mediaChunks.size()); + primarySampleQueue.discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(0)); for (int i = 0; i < embeddedSampleQueues.length; i++) { - embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); + embeddedSampleQueues[i].discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(i + 1)); } - eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs); - return true; + return firstRemovedChunk; } /** @@ -436,11 +719,12 @@ public class ChunkSampleStream implements SampleStream, S public final ChunkSampleStream parent; - private final DefaultTrackOutput sampleQueue; + private final SampleQueue sampleQueue; private final int index; - public EmbeddedSampleStream(ChunkSampleStream parent, DefaultTrackOutput sampleQueue, - int index) { + private boolean notifiedDownstreamFormat; + + public EmbeddedSampleStream(ChunkSampleStream parent, SampleQueue sampleQueue, int index) { this.parent = parent; this.sampleQueue = sampleQueue; this.index = index; @@ -448,16 +732,22 @@ public class ChunkSampleStream implements SampleStream, S @Override public boolean isReady() { - return loadingFinished || (!isPendingReset() && !sampleQueue.isEmpty()); + return !isPendingReset() && sampleQueue.isReady(loadingFinished); } @Override - public void skipData(long positionUs) { - if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - sampleQueue.skipAll(); - } else { - sampleQueue.skipToKeyframeBefore(positionUs, true); + public int skipData(long positionUs) { + if (isPendingReset()) { + return 0; } + maybeNotifyDownstreamFormat(); + int skipCount; + if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { + skipCount = sampleQueue.advanceToEnd(); + } else { + skipCount = sampleQueue.advanceTo(positionUs); + } + return skipCount; } @Override @@ -471,8 +761,13 @@ public class ChunkSampleStream implements SampleStream, S if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - return sampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, - lastSeekPositionUs); + maybeNotifyDownstreamFormat(); + return sampleQueue.read( + formatHolder, + buffer, + formatRequired, + loadingFinished, + decodeOnlyUntilPositionUs); } public void release() { @@ -480,6 +775,17 @@ public class ChunkSampleStream implements SampleStream, S embeddedTracksSelected[index] = false; } + private void maybeNotifyDownstreamFormat() { + if (!notifiedDownstreamFormat) { + eventDispatcher.downstreamFormatChanged( + embeddedTrackTypes[index], + embeddedTrackFormats[index], + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + lastSeekPositionUs); + notifiedDownstreamFormat = true; + } + } } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.java index 1015953da632..33cee8e20ec2 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.java @@ -15,6 +15,8 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters; import java.io.IOException; import java.util.List; @@ -23,6 +25,16 @@ import java.util.List; */ public interface ChunkSource { + /** + * Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used + * as sync points. + * + * @param positionUs The seek position in microseconds. + * @param seekParameters Parameters that control how the seek is performed. + * @return The adjusted seek position, in microseconds. + */ + long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters); + /** * If the source is currently having difficulty providing chunks, then this method throws the * underlying error. Otherwise does nothing. @@ -48,24 +60,32 @@ public interface ChunkSource { /** * Returns the next chunk to load. - *

- * If a chunk is available then {@link ChunkHolder#chunk} is set. If the end of the stream has + * + *

If a chunk is available then {@link ChunkHolder#chunk} is set. If the end of the stream has * been reached then {@link ChunkHolder#endOfStream} is set. If a chunk is not available but the * end of the stream has not been reached, the {@link ChunkHolder} is not modified. * - * @param previous The most recently loaded media chunk. - * @param playbackPositionUs The current playback position. If {@code previous} is null then this - * parameter is the position from which playback is expected to start (or restart) and hence - * should be interpreted as a seek position. + * @param playbackPositionUs The current playback position in microseconds. If playback of the + * period to which this chunk source belongs has not yet started, the value will be the + * starting position in the period minus the duration of any media in previous periods still + * to be played. + * @param loadPositionUs The current load position in microseconds. If {@code queue} is empty, + * this is the starting position from which chunks should be provided. Else it's equal to + * {@link MediaChunk#endTimeUs} of the last chunk in the {@code queue}. + * @param queue The queue of buffered {@link MediaChunk}s. * @param out A holder to populate. */ - void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out); + void getNextChunk( + long playbackPositionUs, + long loadPositionUs, + List queue, + ChunkHolder out); /** * Called when the {@link ChunkSampleStream} has finished loading a chunk obtained from this * source. - *

- * This method should only be called when the source is enabled. + * + *

This method should only be called when the source is enabled. * * @param chunk The chunk whose load has been completed. */ @@ -74,14 +94,18 @@ public interface ChunkSource { /** * Called when the {@link ChunkSampleStream} encounters an error loading a chunk obtained from * this source. - *

- * This method should only be called when the source is enabled. + * + *

This method should only be called when the source is enabled. * * @param chunk The chunk whose load encountered the error. * @param cancelable Whether the load can be canceled. * @param e The error. - * @return Whether the load should be canceled. + * @param blacklistDurationMs The duration for which the associated track may be blacklisted, or + * {@link C#TIME_UNSET} if the track may not be blacklisted. + * @return Whether the load should be canceled so that a replacement chunk can be loaded instead. + * Must be {@code false} if {@code cancelable} is {@code false}. If {@code true}, {@link + * #getNextChunk(long, long, List, ChunkHolder)} will be called to obtain the replacement + * chunk. */ - boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e); - + boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e, long blacklistDurationMs); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java deleted file mode 100644 index 9ee99439c00f..000000000000 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk; - -import android.util.Log; -import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException; - -/** - * Helper class for blacklisting tracks in a {@link TrackSelection} when 404 (Not Found) and 410 - * (Gone) HTTP response codes are encountered. - */ -public final class ChunkedTrackBlacklistUtil { - - /** - * The default duration for which a track is blacklisted in milliseconds. - */ - public static final long DEFAULT_TRACK_BLACKLIST_MS = 60000; - - private static final String TAG = "ChunkedTrackBlacklist"; - - /** - * Blacklists {@code trackSelectionIndex} in {@code trackSelection} for - * {@link #DEFAULT_TRACK_BLACKLIST_MS} if {@code e} is an {@link InvalidResponseCodeException} - * with {@link InvalidResponseCodeException#responseCode} equal to 404 or 410. Else does nothing. - * Note that blacklisting will fail if the track is the only non-blacklisted track in the - * selection. - * - * @param trackSelection The track selection. - * @param trackSelectionIndex The index in the selection to consider blacklisting. - * @param e The error to inspect. - * @return Whether the track was blacklisted in the selection. - */ - public static boolean maybeBlacklistTrack(TrackSelection trackSelection, int trackSelectionIndex, - Exception e) { - return maybeBlacklistTrack(trackSelection, trackSelectionIndex, e, DEFAULT_TRACK_BLACKLIST_MS); - } - - /** - * Blacklists {@code trackSelectionIndex} in {@code trackSelection} for - * {@code blacklistDurationMs} if calling {@link #shouldBlacklist(Exception)} for {@code e} - * returns true. Else does nothing. Note that blacklisting will fail if the track is the only - * non-blacklisted track in the selection. - * - * @param trackSelection The track selection. - * @param trackSelectionIndex The index in the selection to consider blacklisting. - * @param e The error to inspect. - * @param blacklistDurationMs The duration to blacklist the track for, if it is blacklisted. - * @return Whether the track was blacklisted. - */ - public static boolean maybeBlacklistTrack(TrackSelection trackSelection, int trackSelectionIndex, - Exception e, long blacklistDurationMs) { - if (shouldBlacklist(e)) { - boolean blacklisted = trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs); - int responseCode = ((InvalidResponseCodeException) e).responseCode; - if (blacklisted) { - Log.w(TAG, "Blacklisted: duration=" + blacklistDurationMs + ", responseCode=" - + responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex)); - } else { - Log.w(TAG, "Blacklisting failed (cannot blacklist last enabled track): responseCode=" - + responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex)); - } - return blacklisted; - } - return false; - } - - /** - * Returns whether a loading error is an {@link InvalidResponseCodeException} with - * {@link InvalidResponseCodeException#responseCode} equal to 404 or 410. - * - * @param e The loading error. - * @return Wheter the loading error is an {@link InvalidResponseCodeException} with - * {@link InvalidResponseCodeException#responseCode} equal to 404 or 410. - */ - public static boolean shouldBlacklist(Exception e) { - if (e instanceof InvalidResponseCodeException) { - int responseCode = ((InvalidResponseCodeException) e).responseCode; - return responseCode == 404 || responseCode == 410; - } - return false; - } - - private ChunkedTrackBlacklistUtil() {} - -} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java index feeab2768651..98865e8b0eac 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java @@ -15,10 +15,13 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultExtractorInput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; @@ -30,13 +33,15 @@ import java.io.IOException; */ public class ContainerMediaChunk extends BaseMediaChunk { + private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder(); + private final int chunkCount; private final long sampleOffsetUs; private final ChunkExtractorWrapper extractorWrapper; - private volatile int bytesLoaded; + private long nextLoadPosition; private volatile boolean loadCanceled; - private volatile boolean loadCompleted; + private boolean loadCompleted; /** * @param dataSource The source from which the data should be loaded. @@ -46,25 +51,49 @@ public class ContainerMediaChunk extends BaseMediaChunk { * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. - * @param chunkIndex The index of the chunk. + * @param clippedStartTimeUs The time in the chunk from which output will begin, or {@link + * C#TIME_UNSET} to output from the start of the chunk. + * @param clippedEndTimeUs The time in the chunk from which output will end, or {@link + * C#TIME_UNSET} to output to the end of the chunk. + * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. * @param chunkCount The number of chunks in the underlying media that are spanned by this * instance. Normally equal to one, but may be larger if multiple chunks as defined by the * underlying media are being merged into a single load. * @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor. * @param extractorWrapper A wrapped extractor to use for parsing the data. */ - public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, - int chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper) { - super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, - endTimeUs, chunkIndex); + public ContainerMediaChunk( + DataSource dataSource, + DataSpec dataSpec, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long startTimeUs, + long endTimeUs, + long clippedStartTimeUs, + long clippedEndTimeUs, + long chunkIndex, + int chunkCount, + long sampleOffsetUs, + ChunkExtractorWrapper extractorWrapper) { + super( + dataSource, + dataSpec, + trackFormat, + trackSelectionReason, + trackSelectionData, + startTimeUs, + endTimeUs, + clippedStartTimeUs, + clippedEndTimeUs, + chunkIndex); this.chunkCount = chunkCount; this.sampleOffsetUs = sampleOffsetUs; this.extractorWrapper = extractorWrapper; } @Override - public int getNextChunkIndex() { + public long getNextChunkIndex() { return chunkIndex + chunkCount; } @@ -73,11 +102,6 @@ public class ContainerMediaChunk extends BaseMediaChunk { return loadCompleted; } - @Override - public final long bytesLoaded() { - return bytesLoaded; - } - // Loadable implementation. @Override @@ -85,35 +109,34 @@ public class ContainerMediaChunk extends BaseMediaChunk { loadCanceled = true; } - @Override - public final boolean isLoadCanceled() { - return loadCanceled; - } - @SuppressWarnings("NonAtomicVolatileUpdate") @Override public final void load() throws IOException, InterruptedException { - DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); + if (nextLoadPosition == 0) { + // Configure the output and set it as the target for the extractor wrapper. + BaseMediaChunkOutput output = getOutput(); + output.setSampleOffsetUs(sampleOffsetUs); + extractorWrapper.init( + getTrackOutputProvider(output), + clippedStartTimeUs == C.TIME_UNSET ? C.TIME_UNSET : (clippedStartTimeUs - sampleOffsetUs), + clippedEndTimeUs == C.TIME_UNSET ? C.TIME_UNSET : (clippedEndTimeUs - sampleOffsetUs)); + } try { // Create and open the input. - ExtractorInput input = new DefaultExtractorInput(dataSource, - loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); - if (bytesLoaded == 0) { - // Configure the output and set it as the target for the extractor wrapper. - BaseMediaChunkOutput output = getOutput(); - output.setSampleOffsetUs(sampleOffsetUs); - extractorWrapper.init(output); - } + DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); + ExtractorInput input = + new DefaultExtractorInput( + dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); // Load and decode the sample data. try { Extractor extractor = extractorWrapper.extractor; int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractor.read(input, null); + result = extractor.read(input, DUMMY_POSITION_HOLDER); } Assertions.checkState(result != Extractor.RESULT_SEEK); } finally { - bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); + nextLoadPosition = input.getPosition() - dataSpec.absoluteStreamPosition; } } finally { Util.closeQuietly(dataSource); @@ -121,4 +144,14 @@ public class ContainerMediaChunk extends BaseMediaChunk { loadCompleted = true; } + /** + * Returns the {@link TrackOutputProvider} to be used by the wrapped extractor. + * + * @param baseMediaChunkOutput The {@link BaseMediaChunkOutput} most recently passed to {@link + * #init(BaseMediaChunkOutput)}. + * @return A {@link TrackOutputProvider} to be used by the wrapped extractor. + */ + protected TrackOutputProvider getTrackOutputProvider(BaseMediaChunkOutput baseMediaChunkOutput) { + return baseMediaChunkOutput; + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java index 444dac185e2b..583f8ceeee9a 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java @@ -15,6 +15,7 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; @@ -32,7 +33,6 @@ public abstract class DataChunk extends Chunk { private static final int READ_GRANULARITY = 16 * 1024; private byte[] data; - private int limit; private volatile boolean loadCanceled; @@ -45,8 +45,14 @@ public abstract class DataChunk extends Chunk { * @param trackSelectionData See {@link #trackSelectionData}. * @param data An optional recycled array that can be used as a holder for the data. */ - public DataChunk(DataSource dataSource, DataSpec dataSpec, int type, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, byte[] data) { + public DataChunk( + DataSource dataSource, + DataSpec dataSpec, + int type, + Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + byte[] data) { super(dataSource, dataSpec, type, trackFormat, trackSelectionReason, trackSelectionData, C.TIME_UNSET, C.TIME_UNSET); this.data = data; @@ -63,11 +69,6 @@ public abstract class DataChunk extends Chunk { return data; } - @Override - public long bytesLoaded() { - return limit; - } - // Loadable implementation @Override @@ -75,19 +76,14 @@ public abstract class DataChunk extends Chunk { loadCanceled = true; } - @Override - public final boolean isLoadCanceled() { - return loadCanceled; - } - @Override public final void load() throws IOException, InterruptedException { try { dataSource.open(dataSpec); - limit = 0; + int limit = 0; int bytesRead = 0; while (bytesRead != C.RESULT_END_OF_INPUT && !loadCanceled) { - maybeExpandData(); + maybeExpandData(limit); bytesRead = dataSource.read(data, limit, READ_GRANULARITY); if (bytesRead != -1) { limit += bytesRead; @@ -111,7 +107,7 @@ public abstract class DataChunk extends Chunk { */ protected abstract void consume(byte[] data, int limit) throws IOException; - private void maybeExpandData() { + private void maybeExpandData(int limit) { if (data == null) { data = new byte[READ_GRANULARITY]; } else if (data.length < limit + READ_GRANULARITY) { @@ -120,5 +116,4 @@ public abstract class DataChunk extends Chunk { data = Arrays.copyOf(data, data.length + READ_GRANULARITY); } } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java index 7d416aa0fc66..db6e82c2c783 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java @@ -15,25 +15,32 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultExtractorInput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link Chunk} that uses an {@link Extractor} to decode initialization data for single track. */ public final class InitializationChunk extends Chunk { + private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder(); + private final ChunkExtractorWrapper extractorWrapper; - private volatile int bytesLoaded; + @MonotonicNonNull private TrackOutputProvider trackOutputProvider; + private long nextLoadPosition; private volatile boolean loadCanceled; /** @@ -44,17 +51,27 @@ public final class InitializationChunk extends Chunk { * @param trackSelectionData See {@link #trackSelectionData}. * @param extractorWrapper A wrapped extractor to use for parsing the initialization data. */ - public InitializationChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, + public InitializationChunk( + DataSource dataSource, + DataSpec dataSpec, + Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, ChunkExtractorWrapper extractorWrapper) { super(dataSource, dataSpec, C.DATA_TYPE_MEDIA_INITIALIZATION, trackFormat, trackSelectionReason, trackSelectionData, C.TIME_UNSET, C.TIME_UNSET); this.extractorWrapper = extractorWrapper; } - @Override - public long bytesLoaded() { - return bytesLoaded; + /** + * Initializes the chunk for loading, setting a {@link TrackOutputProvider} for track outputs to + * which formats will be written as they are loaded. + * + * @param trackOutputProvider The {@link TrackOutputProvider} for track outputs to which formats + * will be written as they are loaded. + */ + public void init(TrackOutputProvider trackOutputProvider) { + this.trackOutputProvider = trackOutputProvider; } // Loadable implementation. @@ -64,36 +81,32 @@ public final class InitializationChunk extends Chunk { loadCanceled = true; } - @Override - public boolean isLoadCanceled() { - return loadCanceled; - } - @SuppressWarnings("NonAtomicVolatileUpdate") @Override public void load() throws IOException, InterruptedException { - DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); + if (nextLoadPosition == 0) { + extractorWrapper.init( + trackOutputProvider, /* startTimeUs= */ C.TIME_UNSET, /* endTimeUs= */ C.TIME_UNSET); + } try { // Create and open the input. - ExtractorInput input = new DefaultExtractorInput(dataSource, - loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); - if (bytesLoaded == 0) { - extractorWrapper.init(null); - } + DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); + ExtractorInput input = + new DefaultExtractorInput( + dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); // Load and decode the initialization data. try { Extractor extractor = extractorWrapper.extractor; int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractor.read(input, null); + result = extractor.read(input, DUMMY_POSITION_HOLDER); } Assertions.checkState(result != Extractor.RESULT_SEEK); } finally { - bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); + nextLoadPosition = input.getPosition() - dataSpec.absoluteStreamPosition; } } finally { Util.closeQuietly(dataSource); } } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.java index 81bc4fbc7931..81c9d216b912 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.java @@ -15,6 +15,7 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; @@ -26,10 +27,8 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; */ public abstract class MediaChunk extends Chunk { - /** - * The chunk index. - */ - public final int chunkIndex; + /** The chunk index, or {@link C#INDEX_UNSET} if it is not known. */ + public final long chunkIndex; /** * @param dataSource The source from which the data should be loaded. @@ -39,22 +38,26 @@ public abstract class MediaChunk extends Chunk { * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. - * @param chunkIndex The index of the chunk. + * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. */ - public MediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, - int chunkIndex) { + public MediaChunk( + DataSource dataSource, + DataSpec dataSpec, + Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long startTimeUs, + long endTimeUs, + long chunkIndex) { super(dataSource, dataSpec, C.DATA_TYPE_MEDIA, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs); Assertions.checkNotNull(trackFormat); this.chunkIndex = chunkIndex; } - /** - * Returns the next chunk index. - */ - public int getNextChunkIndex() { - return chunkIndex + 1; + /** Returns the next chunk index or {@link C#INDEX_UNSET} if it is not known. */ + public long getNextChunkIndex() { + return chunkIndex != C.INDEX_UNSET ? chunkIndex + 1 : C.INDEX_UNSET; } /** diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java new file mode 100644 index 000000000000..c6f5b1d41eff --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import java.util.NoSuchElementException; + +/** + * Iterator for media chunk sequences. + * + *

The iterator initially points in front of the first available element. The first call to + * {@link #next()} moves the iterator to the first element. Check the return value of {@link + * #next()} or {@link #isEnded()} to determine whether the iterator reached the end of the available + * data. + */ +public interface MediaChunkIterator { + + /** An empty media chunk iterator without available data. */ + MediaChunkIterator EMPTY = + new MediaChunkIterator() { + @Override + public boolean isEnded() { + return true; + } + + @Override + public boolean next() { + return false; + } + + @Override + public DataSpec getDataSpec() { + throw new NoSuchElementException(); + } + + @Override + public long getChunkStartTimeUs() { + throw new NoSuchElementException(); + } + + @Override + public long getChunkEndTimeUs() { + throw new NoSuchElementException(); + } + + @Override + public void reset() { + // Do nothing. + } + }; + + /** Returns whether the iteration has reached the end of the available data. */ + boolean isEnded(); + + /** + * Moves the iterator to the next media chunk. + * + *

Check the return value or {@link #isEnded()} to determine whether the iterator reached the + * end of the available data. + * + * @return Whether the iterator points to a media chunk with available data. + */ + boolean next(); + + /** + * Returns the {@link DataSpec} used to load the media chunk. + * + * @throws java.util.NoSuchElementException If the method is called before the first call to + * {@link #next()} or when {@link #isEnded()} is true. + */ + DataSpec getDataSpec(); + + /** + * Returns the media start time of the chunk, in microseconds. + * + * @throws java.util.NoSuchElementException If the method is called before the first call to + * {@link #next()} or when {@link #isEnded()} is true. + */ + long getChunkStartTimeUs(); + + /** + * Returns the media end time of the chunk, in microseconds. + * + * @throws java.util.NoSuchElementException If the method is called before the first call to + * {@link #next()} or when {@link #isEnded()} is true. + */ + long getChunkEndTimeUs(); + + /** Resets the iterator to the initial position. */ + void reset(); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java new file mode 100644 index 000000000000..1b3004418e94 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import java.util.List; + +/** A {@link MediaChunkIterator} which iterates over a {@link List} of {@link MediaChunk}s. */ +public final class MediaChunkListIterator extends BaseMediaChunkIterator { + + private final List chunks; + private final boolean reverseOrder; + + /** + * Creates iterator. + * + * @param chunks The list of chunks to iterate over. + * @param reverseOrder Whether to iterate in reverse order. + */ + public MediaChunkListIterator(List chunks, boolean reverseOrder) { + super(0, chunks.size() - 1); + this.chunks = chunks; + this.reverseOrder = reverseOrder; + } + + @Override + public DataSpec getDataSpec() { + return getCurrentChunk().dataSpec; + } + + @Override + public long getChunkStartTimeUs() { + return getCurrentChunk().startTimeUs; + } + + @Override + public long getChunkEndTimeUs() { + return getCurrentChunk().endTimeUs; + } + + private MediaChunk getCurrentChunk() { + int index = (int) super.getCurrentIndex(); + if (reverseOrder) { + index = chunks.size() - 1 - index; + } + return chunks.get(index); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java index 41acce4c8b38..b3d30408ee02 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java @@ -33,9 +33,8 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { private final int trackType; private final Format sampleFormat; - private volatile int bytesLoaded; - private volatile boolean loadCanceled; - private volatile boolean loadCompleted; + private long nextLoadPosition; + private boolean loadCompleted; /** * @param dataSource The source from which the data should be loaded. @@ -45,16 +44,33 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. - * @param chunkIndex The index of the chunk. + * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. * @param trackType The type of the chunk. Typically one of the {@link C} {@code TRACK_TYPE_*} * constants. * @param sampleFormat The {@link Format} of the sample in the chunk. */ - public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, - int chunkIndex, int trackType, Format sampleFormat) { - super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, - endTimeUs, chunkIndex); + public SingleSampleMediaChunk( + DataSource dataSource, + DataSpec dataSpec, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long startTimeUs, + long endTimeUs, + long chunkIndex, + int trackType, + Format sampleFormat) { + super( + dataSource, + dataSpec, + trackFormat, + trackSelectionReason, + trackSelectionData, + startTimeUs, + endTimeUs, + /* clippedStartTimeUs= */ C.TIME_UNSET, + /* clippedEndTimeUs= */ C.TIME_UNSET, + chunkIndex); this.trackType = trackType; this.sampleFormat = sampleFormat; } @@ -65,50 +81,40 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { return loadCompleted; } - @Override - public long bytesLoaded() { - return bytesLoaded; - } - // Loadable implementation. @Override public void cancelLoad() { - loadCanceled = true; - } - - @Override - public boolean isLoadCanceled() { - return loadCanceled; + // Do nothing. } @SuppressWarnings("NonAtomicVolatileUpdate") @Override public void load() throws IOException, InterruptedException { - DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); + BaseMediaChunkOutput output = getOutput(); + output.setSampleOffsetUs(0); + TrackOutput trackOutput = output.track(0, trackType); + trackOutput.format(sampleFormat); try { // Create and open the input. + DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); long length = dataSource.open(loadDataSpec); if (length != C.LENGTH_UNSET) { - length += bytesLoaded; + length += nextLoadPosition; } - ExtractorInput extractorInput = new DefaultExtractorInput(dataSource, bytesLoaded, length); - BaseMediaChunkOutput output = getOutput(); - output.setSampleOffsetUs(0); - TrackOutput trackOutput = output.track(0, trackType); - trackOutput.format(sampleFormat); + ExtractorInput extractorInput = + new DefaultExtractorInput(dataSource, nextLoadPosition, length); // Load the sample data. int result = 0; while (result != C.RESULT_END_OF_INPUT) { - bytesLoaded += result; + nextLoadPosition += result; result = trackOutput.sampleData(extractorInput, Integer.MAX_VALUE, true); } - int sampleSize = bytesLoaded; + int sampleSize = (int) nextLoadPosition; trackOutput.sampleMetadata(startTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); } finally { Util.closeQuietly(dataSource); } loadCompleted = true; } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/Aes128DataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/Aes128DataSource.java index c7e7eab4b289..4643c0402c5a 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/Aes128DataSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/Aes128DataSource.java @@ -16,10 +16,12 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls; import android.net.Uri; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSourceInputStream; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; @@ -27,6 +29,8 @@ import java.security.InvalidKeyException; import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.spec.AlgorithmParameterSpec; +import java.util.List; +import java.util.Map; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.NoSuchPaddingException; @@ -36,18 +40,18 @@ import javax.crypto.spec.SecretKeySpec; /** * A {@link DataSource} that decrypts data read from an upstream source, encrypted with AES-128 with * a 128-bit key and PKCS7 padding. - *

- * Note that this {@link DataSource} does not support being opened from arbitrary offsets. It is + * + *

Note that this {@link DataSource} does not support being opened from arbitrary offsets. It is * designed specifically for reading whole files as defined in an HLS media playlist. For this * reason the implementation is private to the HLS package. */ -/* package */ final class Aes128DataSource implements DataSource { +/* package */ class Aes128DataSource implements DataSource { private final DataSource upstream; private final byte[] encryptionKey; private final byte[] encryptionIv; - private CipherInputStream cipherInputStream; + @Nullable private CipherInputStream cipherInputStream; /** * @param upstream The upstream {@link DataSource}. @@ -61,10 +65,15 @@ import javax.crypto.spec.SecretKeySpec; } @Override - public long open(DataSpec dataSpec) throws IOException { + public final void addTransferListener(TransferListener transferListener) { + upstream.addTransferListener(transferListener); + } + + @Override + public final long open(DataSpec dataSpec) throws IOException { Cipher cipher; try { - cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); + cipher = getCipherInstance(); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { throw new RuntimeException(e); } @@ -78,21 +87,16 @@ import javax.crypto.spec.SecretKeySpec; throw new RuntimeException(e); } - cipherInputStream = new CipherInputStream( - new DataSourceInputStream(upstream, dataSpec), cipher); + DataSourceInputStream inputStream = new DataSourceInputStream(upstream, dataSpec); + cipherInputStream = new CipherInputStream(inputStream, cipher); + inputStream.open(); return C.LENGTH_UNSET; } @Override - public void close() throws IOException { - cipherInputStream = null; - upstream.close(); - } - - @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { - Assertions.checkState(cipherInputStream != null); + public final int read(byte[] buffer, int offset, int readLength) throws IOException { + Assertions.checkNotNull(cipherInputStream); int bytesRead = cipherInputStream.read(buffer, offset, readLength); if (bytesRead < 0) { return C.RESULT_END_OF_INPUT; @@ -101,8 +105,25 @@ import javax.crypto.spec.SecretKeySpec; } @Override - public Uri getUri() { + @Nullable + public final Uri getUri() { return upstream.getUri(); } + @Override + public final Map> getResponseHeaders() { + return upstream.getResponseHeaders(); + } + + @Override + public void close() throws IOException { + if (cipherInputStream != null) { + cipherInputStream = null; + upstream.close(); + } + } + + protected Cipher getCipherInstance() throws NoSuchPaddingException, NoSuchAlgorithmException { + return Cipher.getInstance("AES/CBC/PKCS7Padding"); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java new file mode 100644 index 000000000000..6f39e1bff8fd --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls; + +import android.net.Uri; +import android.text.TextUtils; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.Ac3Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.Ac4Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.AdtsExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import java.io.EOFException; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Default {@link HlsExtractorFactory} implementation. + */ +public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { + + public static final String AAC_FILE_EXTENSION = ".aac"; + public static final String AC3_FILE_EXTENSION = ".ac3"; + public static final String EC3_FILE_EXTENSION = ".ec3"; + public static final String AC4_FILE_EXTENSION = ".ac4"; + public static final String MP3_FILE_EXTENSION = ".mp3"; + public static final String MP4_FILE_EXTENSION = ".mp4"; + public static final String M4_FILE_EXTENSION_PREFIX = ".m4"; + public static final String MP4_FILE_EXTENSION_PREFIX = ".mp4"; + public static final String CMF_FILE_EXTENSION_PREFIX = ".cmf"; + public static final String VTT_FILE_EXTENSION = ".vtt"; + public static final String WEBVTT_FILE_EXTENSION = ".webvtt"; + + @DefaultTsPayloadReaderFactory.Flags private final int payloadReaderFactoryFlags; + private final boolean exposeCea608WhenMissingDeclarations; + + /** + * Equivalent to {@link #DefaultHlsExtractorFactory(int, boolean) new + * DefaultHlsExtractorFactory(payloadReaderFactoryFlags = 0, exposeCea608WhenMissingDeclarations = + * true)} + */ + public DefaultHlsExtractorFactory() { + this(/* payloadReaderFactoryFlags= */ 0, /* exposeCea608WhenMissingDeclarations */ true); + } + + /** + * Creates a factory for HLS segment extractors. + * + * @param payloadReaderFactoryFlags Flags to add when constructing any {@link + * DefaultTsPayloadReaderFactory} instances. Other flags may be added on top of {@code + * payloadReaderFactoryFlags} when creating {@link DefaultTsPayloadReaderFactory}. + * @param exposeCea608WhenMissingDeclarations Whether created {@link TsExtractor} instances should + * expose a CEA-608 track should the master playlist contain no Closed Captions declarations. + * If the master playlist contains any Closed Captions declarations, this flag is ignored. + */ + public DefaultHlsExtractorFactory( + int payloadReaderFactoryFlags, boolean exposeCea608WhenMissingDeclarations) { + this.payloadReaderFactoryFlags = payloadReaderFactoryFlags; + this.exposeCea608WhenMissingDeclarations = exposeCea608WhenMissingDeclarations; + } + + @Override + public Result createExtractor( + @Nullable Extractor previousExtractor, + Uri uri, + Format format, + @Nullable List muxedCaptionFormats, + TimestampAdjuster timestampAdjuster, + Map> responseHeaders, + ExtractorInput extractorInput) + throws InterruptedException, IOException { + + if (previousExtractor != null) { + // A extractor has already been successfully used. Return one of the same type. + if (isReusable(previousExtractor)) { + return buildResult(previousExtractor); + } else { + Result result = + buildResultForSameExtractorType(previousExtractor, format, timestampAdjuster); + if (result == null) { + throw new IllegalArgumentException( + "Unexpected previousExtractor type: " + previousExtractor.getClass().getSimpleName()); + } + } + } + + // Try selecting the extractor by the file extension. + Extractor extractorByFileExtension = + createExtractorByFileExtension(uri, format, muxedCaptionFormats, timestampAdjuster); + extractorInput.resetPeekPosition(); + if (sniffQuietly(extractorByFileExtension, extractorInput)) { + return buildResult(extractorByFileExtension); + } + + // We need to manually sniff each known type, without retrying the one selected by file + // extension. + + if (!(extractorByFileExtension instanceof WebvttExtractor)) { + WebvttExtractor webvttExtractor = new WebvttExtractor(format.language, timestampAdjuster); + if (sniffQuietly(webvttExtractor, extractorInput)) { + return buildResult(webvttExtractor); + } + } + + if (!(extractorByFileExtension instanceof AdtsExtractor)) { + AdtsExtractor adtsExtractor = new AdtsExtractor(); + if (sniffQuietly(adtsExtractor, extractorInput)) { + return buildResult(adtsExtractor); + } + } + + if (!(extractorByFileExtension instanceof Ac3Extractor)) { + Ac3Extractor ac3Extractor = new Ac3Extractor(); + if (sniffQuietly(ac3Extractor, extractorInput)) { + return buildResult(ac3Extractor); + } + } + + if (!(extractorByFileExtension instanceof Ac4Extractor)) { + Ac4Extractor ac4Extractor = new Ac4Extractor(); + if (sniffQuietly(ac4Extractor, extractorInput)) { + return buildResult(ac4Extractor); + } + } + + if (!(extractorByFileExtension instanceof Mp3Extractor)) { + Mp3Extractor mp3Extractor = + new Mp3Extractor(/* flags= */ 0, /* forcedFirstSampleTimestampUs= */ 0); + if (sniffQuietly(mp3Extractor, extractorInput)) { + return buildResult(mp3Extractor); + } + } + + if (!(extractorByFileExtension instanceof FragmentedMp4Extractor)) { + FragmentedMp4Extractor fragmentedMp4Extractor = + createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats); + if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) { + return buildResult(fragmentedMp4Extractor); + } + } + + if (!(extractorByFileExtension instanceof TsExtractor)) { + TsExtractor tsExtractor = + createTsExtractor( + payloadReaderFactoryFlags, + exposeCea608WhenMissingDeclarations, + format, + muxedCaptionFormats, + timestampAdjuster); + if (sniffQuietly(tsExtractor, extractorInput)) { + return buildResult(tsExtractor); + } + } + + // Fall back on the extractor created by file extension. + return buildResult(extractorByFileExtension); + } + + private Extractor createExtractorByFileExtension( + Uri uri, + Format format, + @Nullable List muxedCaptionFormats, + TimestampAdjuster timestampAdjuster) { + String lastPathSegment = uri.getLastPathSegment(); + if (lastPathSegment == null) { + lastPathSegment = ""; + } + if (MimeTypes.TEXT_VTT.equals(format.sampleMimeType) + || lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) + || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { + return new WebvttExtractor(format.language, timestampAdjuster); + } else if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) { + return new AdtsExtractor(); + } else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION) + || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) { + return new Ac3Extractor(); + } else if (lastPathSegment.endsWith(AC4_FILE_EXTENSION)) { + return new Ac4Extractor(); + } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { + return new Mp3Extractor(/* flags= */ 0, /* forcedFirstSampleTimestampUs= */ 0); + } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) + || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4) + || lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5) + || lastPathSegment.startsWith(CMF_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) { + return createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats); + } else { + // For any other file extension, we assume TS format. + return createTsExtractor( + payloadReaderFactoryFlags, + exposeCea608WhenMissingDeclarations, + format, + muxedCaptionFormats, + timestampAdjuster); + } + } + + private static TsExtractor createTsExtractor( + @DefaultTsPayloadReaderFactory.Flags int userProvidedPayloadReaderFactoryFlags, + boolean exposeCea608WhenMissingDeclarations, + Format format, + @Nullable List muxedCaptionFormats, + TimestampAdjuster timestampAdjuster) { + @DefaultTsPayloadReaderFactory.Flags + int payloadReaderFactoryFlags = + DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM + | userProvidedPayloadReaderFactoryFlags; + if (muxedCaptionFormats != null) { + // The playlist declares closed caption renditions, we should ignore descriptors. + payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS; + } else if (exposeCea608WhenMissingDeclarations) { + // The playlist does not provide any closed caption information. We preemptively declare a + // closed caption track on channel 0. + muxedCaptionFormats = + Collections.singletonList( + Format.createTextSampleFormat( + /* id= */ null, + MimeTypes.APPLICATION_CEA608, + /* selectionFlags= */ 0, + /* language= */ null)); + } else { + muxedCaptionFormats = Collections.emptyList(); + } + String codecs = format.codecs; + if (!TextUtils.isEmpty(codecs)) { + // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really + // exist. If we know from the codec attribute that they don't exist, then we can + // explicitly ignore them even if they're declared. + if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) { + payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM; + } + if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) { + payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM; + } + } + + return new TsExtractor( + TsExtractor.MODE_HLS, + timestampAdjuster, + new DefaultTsPayloadReaderFactory(payloadReaderFactoryFlags, muxedCaptionFormats)); + } + + private static FragmentedMp4Extractor createFragmentedMp4Extractor( + TimestampAdjuster timestampAdjuster, + Format format, + @Nullable List muxedCaptionFormats) { + // Only enable the EMSG TrackOutput if this is the 'variant' track (i.e. the main one) to avoid + // creating a separate EMSG track for every audio track in a video stream. + return new FragmentedMp4Extractor( + /* flags= */ isFmp4Variant(format) ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK : 0, + timestampAdjuster, + /* sideloadedTrack= */ null, + muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList()); + } + + /** Returns true if this {@code format} represents a 'variant' track (i.e. the main one). */ + private static boolean isFmp4Variant(Format format) { + Metadata metadata = format.metadata; + if (metadata == null) { + return false; + } + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof HlsTrackMetadataEntry) { + return !((HlsTrackMetadataEntry) entry).variantInfos.isEmpty(); + } + } + return false; + } + + @Nullable + private static Result buildResultForSameExtractorType( + Extractor previousExtractor, Format format, TimestampAdjuster timestampAdjuster) { + if (previousExtractor instanceof WebvttExtractor) { + return buildResult(new WebvttExtractor(format.language, timestampAdjuster)); + } else if (previousExtractor instanceof AdtsExtractor) { + return buildResult(new AdtsExtractor()); + } else if (previousExtractor instanceof Ac3Extractor) { + return buildResult(new Ac3Extractor()); + } else if (previousExtractor instanceof Ac4Extractor) { + return buildResult(new Ac4Extractor()); + } else if (previousExtractor instanceof Mp3Extractor) { + return buildResult(new Mp3Extractor()); + } else { + return null; + } + } + + private static Result buildResult(Extractor extractor) { + return new Result( + extractor, + extractor instanceof AdtsExtractor + || extractor instanceof Ac3Extractor + || extractor instanceof Ac4Extractor + || extractor instanceof Mp3Extractor, + isReusable(extractor)); + } + + private static boolean sniffQuietly(Extractor extractor, ExtractorInput input) + throws InterruptedException, IOException { + boolean result = false; + try { + result = extractor.sniff(input); + } catch (EOFException e) { + // Do nothing. + } finally { + input.resetPeekPosition(); + } + return result; + } + + private static boolean isReusable(Extractor previousExtractor) { + return previousExtractor instanceof TsExtractor + || previousExtractor instanceof FragmentedMp4Extractor; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/FullSegmentEncryptionKeyCache.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/FullSegmentEncryptionKeyCache.java new file mode 100644 index 000000000000..eab538582d1a --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/FullSegmentEncryptionKeyCache.java @@ -0,0 +1,85 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls; + +import android.net.Uri; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * LRU cache that holds up to {@code maxSize} full-segment-encryption keys. Which each addition, + * once the cache's size exceeds {@code maxSize}, the oldest item (according to insertion order) is + * removed. + */ +/* package */ final class FullSegmentEncryptionKeyCache { + + private final LinkedHashMap backingMap; + + public FullSegmentEncryptionKeyCache(int maxSize) { + backingMap = + new LinkedHashMap( + /* initialCapacity= */ maxSize + 1, /* loadFactor= */ 1, /* accessOrder= */ false) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > maxSize; + } + }; + } + + /** + * Returns the {@code encryptionKey} cached against this {@code uri}, or null if {@code uri} is + * null or not present in the cache. + */ + @Nullable + public byte[] get(@Nullable Uri uri) { + if (uri == null) { + return null; + } + return backingMap.get(uri); + } + + /** + * Inserts an entry into the cache. + * + * @throws NullPointerException if {@code uri} or {@code encryptionKey} are null. + */ + @Nullable + public byte[] put(Uri uri, byte[] encryptionKey) { + return backingMap.put(Assertions.checkNotNull(uri), Assertions.checkNotNull(encryptionKey)); + } + + /** + * Returns true if {@code uri} is present in the cache. + * + * @throws NullPointerException if {@code uri} is null. + */ + public boolean containsUri(Uri uri) { + return backingMap.containsKey(Assertions.checkNotNull(uri)); + } + + /** + * Removes {@code uri} from the cache. If {@code uri} was present in the cahce, this returns the + * corresponding {@code encryptionKey}, otherwise null. + * + * @throws NullPointerException if {@code uri} is null. + */ + @Nullable + public byte[] remove(Uri uri) { + return backingMap.remove(Assertions.checkNotNull(uri)); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 035eb6e30e9b..da935389d80c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -17,14 +17,16 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls; import android.net.Uri; import android.os.SystemClock; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.BehindLiveWindowException; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.Chunk; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.DataChunk; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; @@ -32,18 +34,17 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.BaseT import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.UriUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.math.BigInteger; import java.util.Arrays; import java.util.List; -import java.util.Locale; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** - * Source of Hls (possibly adaptive) chunks. - */ +/** Source of Hls (possibly adaptive) chunks. */ /* package */ class HlsChunkSource { /** @@ -55,20 +56,16 @@ import java.util.Locale; clear(); } - /** - * The chunk to be loaded next. - */ - public Chunk chunk; + /** The chunk to be loaded next. */ + @Nullable public Chunk chunk; /** * Indicates that the end of the stream has been reached. */ public boolean endOfStream; - /** - * Indicates that the chunk source is waiting for the referred playlist to be refreshed. - */ - public HlsUrl playlist; + /** Indicates that the chunk source is waiting for the referred playlist to be refreshed. */ + @Nullable public Uri playlistUrl; /** * Clears the holder. @@ -76,59 +73,86 @@ import java.util.Locale; public void clear() { chunk = null; endOfStream = false; - playlist = null; + playlistUrl = null; } } + /** + * The maximum number of keys that the key cache can hold. This value must be 2 or greater in + * order to hold initialization segment and media segment keys simultaneously. + */ + private static final int KEY_CACHE_SIZE = 4; + + private final HlsExtractorFactory extractorFactory; private final DataSource mediaDataSource; private final DataSource encryptionDataSource; private final TimestampAdjusterProvider timestampAdjusterProvider; - private final HlsUrl[] variants; + private final Uri[] playlistUrls; + private final Format[] playlistFormats; private final HlsPlaylistTracker playlistTracker; private final TrackGroup trackGroup; - private final List muxedCaptionFormats; + @Nullable private final List muxedCaptionFormats; + private final FullSegmentEncryptionKeyCache keyCache; private boolean isTimestampMaster; private byte[] scratchSpace; - private IOException fatalError; - - private Uri encryptionKeyUri; - private byte[] encryptionKey; - private String encryptionIvString; - private byte[] encryptionIv; + @Nullable private IOException fatalError; + @Nullable private Uri expectedPlaylistUrl; + private boolean independentSegments; // Note: The track group in the selection is typically *not* equal to trackGroup. This is due to // the way in which HlsSampleStreamWrapper generates track groups. Use only index based methods // in TrackSelection to avoid unexpected behavior. private TrackSelection trackSelection; + private long liveEdgeInPeriodTimeUs; + private boolean seenExpectedPlaylistError; /** + * @param extractorFactory An {@link HlsExtractorFactory} from which to obtain the extractors for + * media chunks. * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists. - * @param variants The available variants. + * @param playlistUrls The {@link Uri}s of the media playlists that can be adapted between by this + * chunk source. + * @param playlistFormats The {@link Format Formats} corresponding to the media playlists. * @param dataSourceFactory An {@link HlsDataSourceFactory} to create {@link DataSource}s for the * chunks. - * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If - * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the - * same provider. - * @param muxedCaptionFormats List of muxed caption {@link Format}s. + * @param mediaTransferListener The transfer listener which should be informed of any media data + * transfers. May be null if no listener is available. + * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If multiple + * {@link HlsChunkSource}s are used for a single playback, they should all share the same + * provider. + * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption + * information is available in the master playlist. */ - public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants, - HlsDataSourceFactory dataSourceFactory, TimestampAdjusterProvider timestampAdjusterProvider, - List muxedCaptionFormats) { + public HlsChunkSource( + HlsExtractorFactory extractorFactory, + HlsPlaylistTracker playlistTracker, + Uri[] playlistUrls, + Format[] playlistFormats, + HlsDataSourceFactory dataSourceFactory, + @Nullable TransferListener mediaTransferListener, + TimestampAdjusterProvider timestampAdjusterProvider, + @Nullable List muxedCaptionFormats) { + this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; - this.variants = variants; + this.playlistUrls = playlistUrls; + this.playlistFormats = playlistFormats; this.timestampAdjusterProvider = timestampAdjusterProvider; this.muxedCaptionFormats = muxedCaptionFormats; - Format[] variantFormats = new Format[variants.length]; - int[] initialTrackSelection = new int[variants.length]; - for (int i = 0; i < variants.length; i++) { - variantFormats[i] = variants[i].format; + keyCache = new FullSegmentEncryptionKeyCache(KEY_CACHE_SIZE); + scratchSpace = Util.EMPTY_BYTE_ARRAY; + liveEdgeInPeriodTimeUs = C.TIME_UNSET; + mediaDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_MEDIA); + if (mediaTransferListener != null) { + mediaDataSource.addTransferListener(mediaTransferListener); + } + encryptionDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_DRM); + trackGroup = new TrackGroup(playlistFormats); + int[] initialTrackSelection = new int[playlistUrls.length]; + for (int i = 0; i < playlistUrls.length; i++) { initialTrackSelection[i] = i; } - mediaDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_MEDIA); - encryptionDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_DRM); - trackGroup = new TrackGroup(variantFormats); trackSelection = new InitializationTrackSelection(trackGroup, initialTrackSelection); } @@ -142,6 +166,9 @@ import java.util.Locale; if (fatalError != null) { throw fatalError; } + if (expectedPlaylistUrl != null && seenExpectedPlaylistError) { + playlistTracker.maybeThrowPlaylistRefreshError(expectedPlaylistUrl); + } } /** @@ -152,14 +179,19 @@ import java.util.Locale; } /** - * Selects tracks for use. + * Sets the current track selection. * - * @param trackSelection The track selection. + * @param trackSelection The {@link TrackSelection}. */ - public void selectTracks(TrackSelection trackSelection) { + public void setTrackSelection(TrackSelection trackSelection) { this.trackSelection = trackSelection; } + /** Returns the current {@link TrackSelection}. */ + public TrackSelection getTrackSelection() { + return trackSelection; + } + /** * Resets the source. */ @@ -179,119 +211,148 @@ import java.util.Locale; /** * Returns the next chunk to load. - *

- * If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream has - * been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available but - * the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to - * contain the {@link HlsUrl} that refers to the playlist that needs refreshing. * - * @param previous The most recently loaded media chunk. - * @param playbackPositionUs The current playback position. If {@code previous} is null then this - * parameter is the position from which playback is expected to start (or restart) and hence - * should be interpreted as a seek position. + *

If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream + * has been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available + * but the end of the stream has not been reached, {@link HlsChunkHolder#playlistUrl} is set to + * contain the {@link Uri} that refers to the playlist that needs refreshing. + * + * @param playbackPositionUs The current playback position relative to the period start in + * microseconds. If playback of the period to which this chunk source belongs has not yet + * started, the value will be the starting position in the period minus the duration of any + * media in previous periods still to be played. + * @param loadPositionUs The current load position relative to the period start in microseconds. + * @param queue The queue of buffered {@link HlsMediaChunk}s. + * @param allowEndOfStream Whether {@link HlsChunkHolder#endOfStream} is allowed to be set for + * non-empty media playlists. If {@code false}, the last available chunk is returned instead. + * If the media playlist is empty, {@link HlsChunkHolder#endOfStream} is always set. * @param out A holder to populate. */ - public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChunkHolder out) { - int oldVariantIndex = previous == null ? C.INDEX_UNSET - : trackGroup.indexOf(previous.trackFormat); - // Use start time of the previous chunk rather than its end time because switching format will - // require downloading overlapping segments. - long bufferedDurationUs = previous == null ? 0 - : Math.max(0, previous.startTimeUs - playbackPositionUs); + public void getNextChunk( + long playbackPositionUs, + long loadPositionUs, + List queue, + boolean allowEndOfStream, + HlsChunkHolder out) { + HlsMediaChunk previous = queue.isEmpty() ? null : queue.get(queue.size() - 1); + int oldTrackIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); + long bufferedDurationUs = loadPositionUs - playbackPositionUs; + long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs); + if (previous != null && !independentSegments) { + // Unless segments are known to be independent, switching tracks requires downloading + // overlapping segments. Hence we subtract the previous segment's duration from the buffered + // duration. + // This may affect the live-streaming adaptive track selection logic, when we compare the + // buffered duration to time-to-live-edge to decide whether to switch. Therefore, we subtract + // the duration of the last loaded segment from timeToLiveEdgeUs as well. + long subtractedDurationUs = previous.getDurationUs(); + bufferedDurationUs = Math.max(0, bufferedDurationUs - subtractedDurationUs); + if (timeToLiveEdgeUs != C.TIME_UNSET) { + timeToLiveEdgeUs = Math.max(0, timeToLiveEdgeUs - subtractedDurationUs); + } + } - // Select the variant. - trackSelection.updateSelectedTrack(bufferedDurationUs); - int selectedVariantIndex = trackSelection.getSelectedIndexInTrackGroup(); + // Select the track. + MediaChunkIterator[] mediaChunkIterators = createMediaChunkIterators(previous, loadPositionUs); + trackSelection.updateSelectedTrack( + playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, mediaChunkIterators); + int selectedTrackIndex = trackSelection.getSelectedIndexInTrackGroup(); - boolean switchingVariant = oldVariantIndex != selectedVariantIndex; - HlsUrl selectedUrl = variants[selectedVariantIndex]; - if (!playlistTracker.isSnapshotValid(selectedUrl)) { - out.playlist = selectedUrl; + boolean switchingTrack = oldTrackIndex != selectedTrackIndex; + Uri selectedPlaylistUrl = playlistUrls[selectedTrackIndex]; + if (!playlistTracker.isSnapshotValid(selectedPlaylistUrl)) { + out.playlistUrl = selectedPlaylistUrl; + seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl); + expectedPlaylistUrl = selectedPlaylistUrl; // Retry when playlist is refreshed. return; } - HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); + HlsMediaPlaylist mediaPlaylist = + playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true); + // playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be non-null. + Assertions.checkNotNull(mediaPlaylist); + independentSegments = mediaPlaylist.hasIndependentSegments; + + updateLiveEdgeTimeUs(mediaPlaylist); // Select the chunk. - int chunkMediaSequence; - if (previous == null || switchingVariant) { - long targetPositionUs = previous == null ? playbackPositionUs : previous.startTimeUs; - if (!mediaPlaylist.hasEndTag && targetPositionUs > mediaPlaylist.getEndTimeUs()) { - // If the playlist is too old to contain the chunk, we need to refresh it. - chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); - } else { - chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, - targetPositionUs - mediaPlaylist.startTimeUs, true, - !playlistTracker.isLive() || previous == null) + mediaPlaylist.mediaSequence; - if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null) { - // We try getting the next chunk without adapting in case that's the reason for falling - // behind the live window. - selectedVariantIndex = oldVariantIndex; - selectedUrl = variants[selectedVariantIndex]; - mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); - chunkMediaSequence = previous.getNextChunkIndex(); - } - } - } else { - chunkMediaSequence = previous.getNextChunkIndex(); + long startOfPlaylistInPeriodUs = + mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); + long chunkMediaSequence = + getChunkMediaSequence( + previous, switchingTrack, mediaPlaylist, startOfPlaylistInPeriodUs, loadPositionUs); + if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null && switchingTrack) { + // We try getting the next chunk without adapting in case that's the reason for falling + // behind the live window. + selectedTrackIndex = oldTrackIndex; + selectedPlaylistUrl = playlistUrls[selectedTrackIndex]; + mediaPlaylist = + playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true); + // playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be + // non-null. + Assertions.checkNotNull(mediaPlaylist); + startOfPlaylistInPeriodUs = + mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); + chunkMediaSequence = previous.getNextChunkIndex(); } + if (chunkMediaSequence < mediaPlaylist.mediaSequence) { fatalError = new BehindLiveWindowException(); return; } - int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence; - if (chunkIndex >= mediaPlaylist.segments.size()) { + int segmentIndexInPlaylist = (int) (chunkMediaSequence - mediaPlaylist.mediaSequence); + int availableSegmentCount = mediaPlaylist.segments.size(); + if (segmentIndexInPlaylist >= availableSegmentCount) { if (mediaPlaylist.hasEndTag) { - out.endOfStream = true; + if (allowEndOfStream || availableSegmentCount == 0) { + out.endOfStream = true; + return; + } + segmentIndexInPlaylist = availableSegmentCount - 1; } else /* Live */ { - out.playlist = selectedUrl; + out.playlistUrl = selectedPlaylistUrl; + seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl); + expectedPlaylistUrl = selectedPlaylistUrl; + return; } + } + // We have a valid playlist snapshot, we can discard any playlist errors at this point. + seenExpectedPlaylistError = false; + expectedPlaylistUrl = null; + + // Handle encryption. + HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(segmentIndexInPlaylist); + + // Check if the segment or its initialization segment are fully encrypted. + Uri initSegmentKeyUri = getFullEncryptionKeyUri(mediaPlaylist, segment.initializationSegment); + out.chunk = maybeCreateEncryptionChunkFor(initSegmentKeyUri, selectedTrackIndex); + if (out.chunk != null) { + return; + } + Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(mediaPlaylist, segment); + out.chunk = maybeCreateEncryptionChunkFor(mediaSegmentKeyUri, selectedTrackIndex); + if (out.chunk != null) { return; } - // Handle encryption. - HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); - - // Check if encryption is specified. - if (segment.isEncrypted) { - Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); - if (!keyUri.equals(encryptionKeyUri)) { - // Encryption is specified and the key has changed. - out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex, - trackSelection.getSelectionReason(), trackSelection.getSelectionData()); - return; - } - if (!Util.areEqual(segment.encryptionIV, encryptionIvString)) { - setEncryptionData(keyUri, segment.encryptionIV, encryptionKey); - } - } else { - clearEncryptionData(); - } - - DataSpec initDataSpec = null; - Segment initSegment = mediaPlaylist.initializationSegment; - if (initSegment != null) { - Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url); - initDataSpec = new DataSpec(initSegmentUri, initSegment.byterangeOffset, - initSegment.byterangeLength, null); - } - - // Compute start time of the next chunk. - long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs; - int discontinuitySequence = mediaPlaylist.discontinuitySequence - + segment.relativeDiscontinuitySequence; - TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( - discontinuitySequence); - - // Configure the data source and spec for the chunk. - Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); - DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, - null); - out.chunk = new HlsMediaChunk(mediaDataSource, dataSpec, initDataSpec, selectedUrl, - muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(), - startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence, - isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv); + out.chunk = + HlsMediaChunk.createInstance( + extractorFactory, + mediaDataSource, + playlistFormats[selectedTrackIndex], + startOfPlaylistInPeriodUs, + mediaPlaylist, + segmentIndexInPlaylist, + selectedPlaylistUrl, + muxedCaptionFormats, + trackSelection.getSelectionReason(), + trackSelection.getSelectionData(), + isTimestampMaster, + timestampAdjusterProvider, + previous, + /* mediaSegmentKey= */ keyCache.get(mediaSegmentKeyUri), + /* initSegmentKey= */ keyCache.get(initSegmentKeyUri)); } /** @@ -304,75 +365,177 @@ import java.util.Locale; if (chunk instanceof EncryptionKeyChunk) { EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk; scratchSpace = encryptionKeyChunk.getDataHolder(); - setEncryptionData(encryptionKeyChunk.dataSpec.uri, encryptionKeyChunk.iv, - encryptionKeyChunk.getResult()); + keyCache.put( + encryptionKeyChunk.dataSpec.uri, Assertions.checkNotNull(encryptionKeyChunk.getResult())); } } /** - * Called when the {@link HlsSampleStreamWrapper} encounters an error loading a chunk obtained - * from this source. + * Attempts to blacklist the track associated with the given chunk. Blacklisting will fail if the + * track is the only non-blacklisted track in the selection. * - * @param chunk The chunk whose load encountered the error. - * @param cancelable Whether the load can be canceled. - * @param error The error. - * @return Whether the load should be canceled. + * @param chunk The chunk whose load caused the blacklisting attempt. + * @param blacklistDurationMs The number of milliseconds for which the track selection should be + * blacklisted. + * @return Whether the blacklisting succeeded. */ - public boolean onChunkLoadError(Chunk chunk, boolean cancelable, IOException error) { - return cancelable && ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection, - trackSelection.indexOf(trackGroup.indexOf(chunk.trackFormat)), error); + public boolean maybeBlacklistTrack(Chunk chunk, long blacklistDurationMs) { + return trackSelection.blacklist( + trackSelection.indexOf(trackGroup.indexOf(chunk.trackFormat)), blacklistDurationMs); } /** - * Called when a playlist is blacklisted. + * Called when a playlist load encounters an error. * - * @param url The url that references the blacklisted playlist. - * @param blacklistMs The amount of milliseconds for which the playlist was blacklisted. + * @param playlistUrl The {@link Uri} of the playlist whose load encountered an error. + * @param blacklistDurationMs The duration for which the playlist should be blacklisted. Or {@link + * C#TIME_UNSET} if the playlist should not be blacklisted. + * @return True if blacklisting did not encounter errors. False otherwise. */ - public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { - int trackGroupIndex = trackGroup.indexOf(url.format); - if (trackGroupIndex != C.INDEX_UNSET) { - int trackSelectionIndex = trackSelection.indexOf(trackGroupIndex); - if (trackSelectionIndex != C.INDEX_UNSET) { - trackSelection.blacklist(trackSelectionIndex, blacklistMs); + public boolean onPlaylistError(Uri playlistUrl, long blacklistDurationMs) { + int trackGroupIndex = C.INDEX_UNSET; + for (int i = 0; i < playlistUrls.length; i++) { + if (playlistUrls[i].equals(playlistUrl)) { + trackGroupIndex = i; + break; } } + if (trackGroupIndex == C.INDEX_UNSET) { + return true; + } + int trackSelectionIndex = trackSelection.indexOf(trackGroupIndex); + if (trackSelectionIndex == C.INDEX_UNSET) { + return true; + } + seenExpectedPlaylistError |= playlistUrl.equals(expectedPlaylistUrl); + return blacklistDurationMs == C.TIME_UNSET + || trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs); + } + + /** + * Returns an array of {@link MediaChunkIterator}s for upcoming media chunks. + * + * @param previous The previous media chunk. May be null. + * @param loadPositionUs The position at which the iterators will start. + * @return Array of {@link MediaChunkIterator}s for each track. + */ + public MediaChunkIterator[] createMediaChunkIterators( + @Nullable HlsMediaChunk previous, long loadPositionUs) { + int oldTrackIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); + MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()]; + for (int i = 0; i < chunkIterators.length; i++) { + int trackIndex = trackSelection.getIndexInTrackGroup(i); + Uri playlistUrl = playlistUrls[trackIndex]; + if (!playlistTracker.isSnapshotValid(playlistUrl)) { + chunkIterators[i] = MediaChunkIterator.EMPTY; + continue; + } + HlsMediaPlaylist playlist = + playlistTracker.getPlaylistSnapshot(playlistUrl, /* isForPlayback= */ false); + // Playlist snapshot is valid (checked by if() above) so playlist must be non-null. + Assertions.checkNotNull(playlist); + long startOfPlaylistInPeriodUs = + playlist.startTimeUs - playlistTracker.getInitialStartTimeUs(); + boolean switchingTrack = trackIndex != oldTrackIndex; + long chunkMediaSequence = + getChunkMediaSequence( + previous, switchingTrack, playlist, startOfPlaylistInPeriodUs, loadPositionUs); + if (chunkMediaSequence < playlist.mediaSequence) { + chunkIterators[i] = MediaChunkIterator.EMPTY; + continue; + } + int chunkIndex = (int) (chunkMediaSequence - playlist.mediaSequence); + chunkIterators[i] = + new HlsMediaPlaylistSegmentIterator(playlist, startOfPlaylistInPeriodUs, chunkIndex); + } + return chunkIterators; } // Private methods. - private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex, - int trackSelectionReason, Object trackSelectionData) { - DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP); - return new EncryptionKeyChunk(encryptionDataSource, dataSpec, variants[variantIndex].format, - trackSelectionReason, trackSelectionData, scratchSpace, iv); + /** + * Returns the media sequence number of the segment to load next in {@code mediaPlaylist}. + * + * @param previous The last (at least partially) loaded segment. + * @param switchingTrack Whether the segment to load is not preceded by a segment in the same + * track. + * @param mediaPlaylist The media playlist to which the segment to load belongs. + * @param startOfPlaylistInPeriodUs The start of {@code mediaPlaylist} relative to the period + * start in microseconds. + * @param loadPositionUs The current load position relative to the period start in microseconds. + * @return The media sequence of the segment to load. + */ + private long getChunkMediaSequence( + @Nullable HlsMediaChunk previous, + boolean switchingTrack, + HlsMediaPlaylist mediaPlaylist, + long startOfPlaylistInPeriodUs, + long loadPositionUs) { + if (previous == null || switchingTrack) { + long endOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs + mediaPlaylist.durationUs; + long targetPositionInPeriodUs = + (previous == null || independentSegments) ? loadPositionUs : previous.startTimeUs; + if (!mediaPlaylist.hasEndTag && targetPositionInPeriodUs >= endOfPlaylistInPeriodUs) { + // If the playlist is too old to contain the chunk, we need to refresh it. + return mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); + } + long targetPositionInPlaylistUs = targetPositionInPeriodUs - startOfPlaylistInPeriodUs; + return Util.binarySearchFloor( + mediaPlaylist.segments, + /* value= */ targetPositionInPlaylistUs, + /* inclusive= */ true, + /* stayInBounds= */ !playlistTracker.isLive() || previous == null) + + mediaPlaylist.mediaSequence; + } + // We ignore the case of previous not having loaded completely, in which case we load the next + // segment. + return previous.getNextChunkIndex(); } - private void setEncryptionData(Uri keyUri, String iv, byte[] secretKey) { - String trimmedIv; - if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) { - trimmedIv = iv.substring(2); - } else { - trimmedIv = iv; + private long resolveTimeToLiveEdgeUs(long playbackPositionUs) { + final boolean resolveTimeToLiveEdgePossible = liveEdgeInPeriodTimeUs != C.TIME_UNSET; + return resolveTimeToLiveEdgePossible + ? liveEdgeInPeriodTimeUs - playbackPositionUs + : C.TIME_UNSET; + } + + private void updateLiveEdgeTimeUs(HlsMediaPlaylist mediaPlaylist) { + liveEdgeInPeriodTimeUs = + mediaPlaylist.hasEndTag + ? C.TIME_UNSET + : (mediaPlaylist.getEndTimeUs() - playlistTracker.getInitialStartTimeUs()); + } + + @Nullable + private Chunk maybeCreateEncryptionChunkFor(@Nullable Uri keyUri, int selectedTrackIndex) { + if (keyUri == null) { + return null; } - byte[] ivData = new BigInteger(trimmedIv, 16).toByteArray(); - byte[] ivDataWithPadding = new byte[16]; - int offset = ivData.length > 16 ? ivData.length - 16 : 0; - System.arraycopy(ivData, offset, ivDataWithPadding, ivDataWithPadding.length - ivData.length - + offset, ivData.length - offset); - - encryptionKeyUri = keyUri; - encryptionKey = secretKey; - encryptionIvString = iv; - encryptionIv = ivDataWithPadding; + byte[] encryptionKey = keyCache.remove(keyUri); + if (encryptionKey != null) { + // The key was present in the key cache. We re-insert it to prevent it from being evicted by + // the following key addition. Note that removal of the key is necessary to affect the + // eviction order. + keyCache.put(keyUri, encryptionKey); + return null; + } + DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP); + return new EncryptionKeyChunk( + encryptionDataSource, + dataSpec, + playlistFormats[selectedTrackIndex], + trackSelection.getSelectionReason(), + trackSelection.getSelectionData(), + scratchSpace); } - private void clearEncryptionData() { - encryptionKeyUri = null; - encryptionKey = null; - encryptionIvString = null; - encryptionIv = null; + @Nullable + private static Uri getFullEncryptionKeyUri(HlsMediaPlaylist playlist, @Nullable Segment segment) { + if (segment == null || segment.fullSegmentEncryptionKeyUri == null) { + return null; + } + return UriUtil.resolveToUri(playlist.baseUri, segment.fullSegmentEncryptionKeyUri); } // Private classes. @@ -390,7 +553,12 @@ import java.util.Locale; } @Override - public void updateSelectedTrack(long bufferedDurationUs) { + public void updateSelectedTrack( + long playbackPositionUs, + long bufferedDurationUs, + long availableDurationUs, + List queue, + MediaChunkIterator[] mediaChunkIterators) { long nowMs = SystemClock.elapsedRealtime(); if (!isBlacklisted(selectedIndex, nowMs)) { return; @@ -417,6 +585,7 @@ import java.util.Locale; } @Override + @Nullable public Object getSelectionData() { return null; } @@ -425,26 +594,75 @@ import java.util.Locale; private static final class EncryptionKeyChunk extends DataChunk { - public final String iv; + private byte @MonotonicNonNull [] result; - private byte[] result; - - public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, byte[] scratchSpace, String iv) { + public EncryptionKeyChunk( + DataSource dataSource, + DataSpec dataSpec, + Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + byte[] scratchSpace) { super(dataSource, dataSpec, C.DATA_TYPE_DRM, trackFormat, trackSelectionReason, trackSelectionData, scratchSpace); - this.iv = iv; } @Override - protected void consume(byte[] data, int limit) throws IOException { + protected void consume(byte[] data, int limit) { result = Arrays.copyOf(data, limit); } + /** Return the result of this chunk, or null if loading is not complete. */ + @Nullable public byte[] getResult() { return result; } } + /** {@link MediaChunkIterator} wrapping a {@link HlsMediaPlaylist}. */ + private static final class HlsMediaPlaylistSegmentIterator extends BaseMediaChunkIterator { + + private final HlsMediaPlaylist playlist; + private final long startOfPlaylistInPeriodUs; + + /** + * Creates iterator. + * + * @param playlist The {@link HlsMediaPlaylist} to wrap. + * @param startOfPlaylistInPeriodUs The start time of the playlist in the period, in + * microseconds. + * @param chunkIndex The index of the first available chunk in the playlist. + */ + public HlsMediaPlaylistSegmentIterator( + HlsMediaPlaylist playlist, long startOfPlaylistInPeriodUs, int chunkIndex) { + super(/* fromIndex= */ chunkIndex, /* toIndex= */ playlist.segments.size() - 1); + this.playlist = playlist; + this.startOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs; + } + + @Override + public DataSpec getDataSpec() { + checkInBounds(); + Segment segment = playlist.segments.get((int) getCurrentIndex()); + Uri chunkUri = UriUtil.resolveToUri(playlist.baseUri, segment.url); + return new DataSpec( + chunkUri, segment.byterangeOffset, segment.byterangeLength, /* key= */ null); + } + + @Override + public long getChunkStartTimeUs() { + checkInBounds(); + Segment segment = playlist.segments.get((int) getCurrentIndex()); + return startOfPlaylistInPeriodUs + segment.relativeStartTimeUs; + } + + @Override + public long getChunkEndTimeUs() { + checkInBounds(); + Segment segment = playlist.segments.get((int) getCurrentIndex()); + long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + segment.relativeStartTimeUs; + return segmentStartTimeInPeriodUs + segment.durationUs; + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java new file mode 100644 index 000000000000..8f445f97ed7e --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls; + +import android.net.Uri; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * Factory for HLS media chunk extractors. + */ +public interface HlsExtractorFactory { + + /** Holds an {@link Extractor} and associated parameters. */ + final class Result { + + /** The created extractor; */ + public final Extractor extractor; + /** Whether the segments for which {@link #extractor} is created are packed audio segments. */ + public final boolean isPackedAudioExtractor; + /** + * Whether {@link #extractor} may be reused for following continuous (no immediately preceding + * discontinuities) segments of the same variant. + */ + public final boolean isReusable; + + /** + * Creates a result. + * + * @param extractor See {@link #extractor}. + * @param isPackedAudioExtractor See {@link #isPackedAudioExtractor}. + * @param isReusable See {@link #isReusable}. + */ + public Result(Extractor extractor, boolean isPackedAudioExtractor, boolean isReusable) { + this.extractor = extractor; + this.isPackedAudioExtractor = isPackedAudioExtractor; + this.isReusable = isReusable; + } + } + + HlsExtractorFactory DEFAULT = new DefaultHlsExtractorFactory(); + + /** + * Creates an {@link Extractor} for extracting HLS media chunks. + * + * @param previousExtractor A previously used {@link Extractor} which can be reused if the current + * chunk is a continuation of the previously extracted chunk, or null otherwise. It is the + * responsibility of implementers to only reuse extractors that are suited for reusage. + * @param uri The URI of the media chunk. + * @param format A {@link Format} associated with the chunk to extract. + * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption + * information is available in the master playlist. + * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. + * @param responseHeaders The HTTP response headers associated with the media segment or + * initialization section to extract. + * @param sniffingExtractorInput The first extractor input that will be passed to the returned + * extractor's {@link Extractor#read(ExtractorInput, PositionHolder)}. Must only be used to + * call {@link Extractor#sniff(ExtractorInput)}. + * @return A {@link Result}. + * @throws InterruptedException If the thread is interrupted while sniffing. + * @throws IOException If an I/O error is encountered while sniffing. + */ + Result createExtractor( + @Nullable Extractor previousExtractor, + Uri uri, + Format format, + @Nullable List muxedCaptionFormats, + TimestampAdjuster timestampAdjuster, + Map> responseHeaders, + ExtractorInput sniffingExtractorInput) + throws InterruptedException, IOException; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 4521c4e78d27..173e53faade6 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -15,51 +15,173 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls; -import android.text.TextUtils; +import android.net.Uri; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultExtractorInput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.Ac3Extractor; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.AdtsExtractor; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsExtractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder; import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.PrivFrame; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.UriUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.EOFException; import java.io.IOException; +import java.math.BigInteger; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * An HLS {@link MediaChunk}. */ /* package */ final class HlsMediaChunk extends MediaChunk { - private static final AtomicInteger UID_SOURCE = new AtomicInteger(); + /** + * Creates a new instance. + * + * @param extractorFactory A {@link HlsExtractorFactory} from which the HLS media chunk extractor + * is obtained. + * @param dataSource The source from which the data should be loaded. + * @param format The chunk format. + * @param startOfPlaylistInPeriodUs The position of the playlist in the period in microseconds. + * @param mediaPlaylist The media playlist from which this chunk was obtained. + * @param playlistUrl The url of the playlist from which this chunk was obtained. + * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption + * information is available in the master playlist. + * @param trackSelectionReason See {@link #trackSelectionReason}. + * @param trackSelectionData See {@link #trackSelectionData}. + * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster. + * @param timestampAdjusterProvider The provider from which to obtain the {@link + * TimestampAdjuster}. + * @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null. + * @param mediaSegmentKey The media segment decryption key, if fully encrypted. Null otherwise. + * @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null + * otherwise. + */ + public static HlsMediaChunk createInstance( + HlsExtractorFactory extractorFactory, + DataSource dataSource, + Format format, + long startOfPlaylistInPeriodUs, + HlsMediaPlaylist mediaPlaylist, + int segmentIndexInPlaylist, + Uri playlistUrl, + @Nullable List muxedCaptionFormats, + int trackSelectionReason, + @Nullable Object trackSelectionData, + boolean isMasterTimestampSource, + TimestampAdjusterProvider timestampAdjusterProvider, + @Nullable HlsMediaChunk previousChunk, + @Nullable byte[] mediaSegmentKey, + @Nullable byte[] initSegmentKey) { + // Media segment. + HlsMediaPlaylist.Segment mediaSegment = mediaPlaylist.segments.get(segmentIndexInPlaylist); + DataSpec dataSpec = + new DataSpec( + UriUtil.resolveToUri(mediaPlaylist.baseUri, mediaSegment.url), + mediaSegment.byterangeOffset, + mediaSegment.byterangeLength, + /* key= */ null); + boolean mediaSegmentEncrypted = mediaSegmentKey != null; + byte[] mediaSegmentIv = + mediaSegmentEncrypted + ? getEncryptionIvArray(Assertions.checkNotNull(mediaSegment.encryptionIV)) + : null; + DataSource mediaDataSource = buildDataSource(dataSource, mediaSegmentKey, mediaSegmentIv); - private static final String PRIV_TIMESTAMP_FRAME_OWNER = + // Init segment. + HlsMediaPlaylist.Segment initSegment = mediaSegment.initializationSegment; + DataSpec initDataSpec = null; + boolean initSegmentEncrypted = false; + DataSource initDataSource = null; + if (initSegment != null) { + initSegmentEncrypted = initSegmentKey != null; + byte[] initSegmentIv = + initSegmentEncrypted + ? getEncryptionIvArray(Assertions.checkNotNull(initSegment.encryptionIV)) + : null; + Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url); + initDataSpec = + new DataSpec( + initSegmentUri, + initSegment.byterangeOffset, + initSegment.byterangeLength, + /* key= */ null); + initDataSource = buildDataSource(dataSource, initSegmentKey, initSegmentIv); + } + + long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + mediaSegment.relativeStartTimeUs; + long segmentEndTimeInPeriodUs = segmentStartTimeInPeriodUs + mediaSegment.durationUs; + int discontinuitySequenceNumber = + mediaPlaylist.discontinuitySequence + mediaSegment.relativeDiscontinuitySequence; + + Extractor previousExtractor = null; + Id3Decoder id3Decoder; + ParsableByteArray scratchId3Data; + boolean shouldSpliceIn; + if (previousChunk != null) { + id3Decoder = previousChunk.id3Decoder; + scratchId3Data = previousChunk.scratchId3Data; + shouldSpliceIn = + !playlistUrl.equals(previousChunk.playlistUrl) || !previousChunk.loadCompleted; + previousExtractor = + previousChunk.isExtractorReusable + && previousChunk.discontinuitySequenceNumber == discontinuitySequenceNumber + && !shouldSpliceIn + ? previousChunk.extractor + : null; + } else { + id3Decoder = new Id3Decoder(); + scratchId3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); + shouldSpliceIn = false; + } + + return new HlsMediaChunk( + extractorFactory, + mediaDataSource, + dataSpec, + format, + mediaSegmentEncrypted, + initDataSource, + initDataSpec, + initSegmentEncrypted, + playlistUrl, + muxedCaptionFormats, + trackSelectionReason, + trackSelectionData, + segmentStartTimeInPeriodUs, + segmentEndTimeInPeriodUs, + /* chunkMediaSequence= */ mediaPlaylist.mediaSequence + segmentIndexInPlaylist, + discontinuitySequenceNumber, + mediaSegment.hasGapTag, + isMasterTimestampSource, + /* timestampAdjuster= */ timestampAdjusterProvider.getAdjuster(discontinuitySequenceNumber), + mediaSegment.drmInitData, + previousExtractor, + id3Decoder, + scratchId3Data, + shouldSpliceIn); + } + + public static final String PRIV_TIMESTAMP_FRAME_OWNER = "com.apple.streaming.transportStreamTimestamp"; + private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder(); - private static final String AAC_FILE_EXTENSION = ".aac"; - private static final String AC3_FILE_EXTENSION = ".ac3"; - private static final String EC3_FILE_EXTENSION = ".ec3"; - private static final String MP3_FILE_EXTENSION = ".mp3"; - private static final String MP4_FILE_EXTENSION = ".mp4"; - private static final String M4_FILE_EXTENSION_PREFIX = ".m4"; - private static final String VTT_FILE_EXTENSION = ".vtt"; - private static final String WEBVTT_FILE_EXTENSION = ".webvtt"; + private static final AtomicInteger uidSource = new AtomicInteger(); /** * A unique identifier for the chunk. @@ -71,89 +193,87 @@ import java.util.concurrent.atomic.AtomicInteger; */ public final int discontinuitySequenceNumber; - /** - * The url of the playlist from which this chunk was obtained. - */ - public final HlsUrl hlsUrl; + /** The url of the playlist from which this chunk was obtained. */ + public final Uri playlistUrl; + + @Nullable private final DataSource initDataSource; + @Nullable private final DataSpec initDataSpec; + @Nullable private final Extractor previousExtractor; - private final DataSource initDataSource; - private final DataSpec initDataSpec; - private final boolean isEncrypted; private final boolean isMasterTimestampSource; + private final boolean hasGapTag; private final TimestampAdjuster timestampAdjuster; - private final String lastPathSegment; - private final Extractor previousExtractor; private final boolean shouldSpliceIn; - private final boolean needNewExtractor; - private final List muxedCaptionFormats; - - private final boolean isPackedAudio; + private final HlsExtractorFactory extractorFactory; + @Nullable private final List muxedCaptionFormats; + @Nullable private final DrmInitData drmInitData; private final Id3Decoder id3Decoder; - private final ParsableByteArray id3Data; + private final ParsableByteArray scratchId3Data; + private final boolean mediaSegmentEncrypted; + private final boolean initSegmentEncrypted; - private Extractor extractor; - private int initSegmentBytesLoaded; - private int bytesLoaded; - private boolean initLoadCompleted; - private HlsSampleStreamWrapper extractorOutput; + @MonotonicNonNull private Extractor extractor; + private boolean isExtractorReusable; + @MonotonicNonNull private HlsSampleStreamWrapper output; + // nextLoadPosition refers to the init segment if initDataLoadRequired is true. + // Otherwise, nextLoadPosition refers to the media segment. + private int nextLoadPosition; + private boolean initDataLoadRequired; private volatile boolean loadCanceled; - private volatile boolean loadCompleted; + private boolean loadCompleted; - /** - * @param dataSource The source from which the data should be loaded. - * @param dataSpec Defines the data to be loaded. - * @param initDataSpec Defines the initialization data to be fed to new extractors. May be null. - * @param hlsUrl The url of the playlist from which this chunk was obtained. - * @param muxedCaptionFormats List of muxed caption {@link Format}s. - * @param trackSelectionReason See {@link #trackSelectionReason}. - * @param trackSelectionData See {@link #trackSelectionData}. - * @param startTimeUs The start time of the chunk in microseconds. - * @param endTimeUs The end time of the chunk in microseconds. - * @param chunkIndex The media sequence number of the chunk. - * @param discontinuitySequenceNumber The discontinuity sequence number of the chunk. - * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster. - * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. - * @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null. - * @param encryptionKey For AES encryption chunks, the encryption key. - * @param encryptionIv For AES encryption chunks, the encryption initialization vector. - */ - public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec, - HlsUrl hlsUrl, List muxedCaptionFormats, int trackSelectionReason, - Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex, - int discontinuitySequenceNumber, boolean isMasterTimestampSource, - TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, byte[] encryptionKey, - byte[] encryptionIv) { - super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format, - trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); + private HlsMediaChunk( + HlsExtractorFactory extractorFactory, + DataSource mediaDataSource, + DataSpec dataSpec, + Format format, + boolean mediaSegmentEncrypted, + @Nullable DataSource initDataSource, + @Nullable DataSpec initDataSpec, + boolean initSegmentEncrypted, + Uri playlistUrl, + @Nullable List muxedCaptionFormats, + int trackSelectionReason, + @Nullable Object trackSelectionData, + long startTimeUs, + long endTimeUs, + long chunkMediaSequence, + int discontinuitySequenceNumber, + boolean hasGapTag, + boolean isMasterTimestampSource, + TimestampAdjuster timestampAdjuster, + @Nullable DrmInitData drmInitData, + @Nullable Extractor previousExtractor, + Id3Decoder id3Decoder, + ParsableByteArray scratchId3Data, + boolean shouldSpliceIn) { + super( + mediaDataSource, + dataSpec, + format, + trackSelectionReason, + trackSelectionData, + startTimeUs, + endTimeUs, + chunkMediaSequence); + this.mediaSegmentEncrypted = mediaSegmentEncrypted; this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.initDataSpec = initDataSpec; - this.hlsUrl = hlsUrl; - this.muxedCaptionFormats = muxedCaptionFormats; + this.initDataSource = initDataSource; + this.initDataLoadRequired = initDataSpec != null; + this.initSegmentEncrypted = initSegmentEncrypted; + this.playlistUrl = playlistUrl; this.isMasterTimestampSource = isMasterTimestampSource; this.timestampAdjuster = timestampAdjuster; - // Note: this.dataSource and dataSource may be different. - this.isEncrypted = this.dataSource instanceof Aes128DataSource; - lastPathSegment = dataSpec.uri.getLastPathSegment(); - isPackedAudio = lastPathSegment.endsWith(AAC_FILE_EXTENSION) - || lastPathSegment.endsWith(AC3_FILE_EXTENSION) - || lastPathSegment.endsWith(EC3_FILE_EXTENSION) - || lastPathSegment.endsWith(MP3_FILE_EXTENSION); - if (previousChunk != null) { - id3Decoder = previousChunk.id3Decoder; - id3Data = previousChunk.id3Data; - previousExtractor = previousChunk.extractor; - shouldSpliceIn = previousChunk.hlsUrl != hlsUrl; - needNewExtractor = previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber - || shouldSpliceIn; - } else { - id3Decoder = isPackedAudio ? new Id3Decoder() : null; - id3Data = isPackedAudio ? new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH) : null; - previousExtractor = null; - shouldSpliceIn = false; - needNewExtractor = true; - } - initDataSource = dataSource; - uid = UID_SOURCE.getAndIncrement(); + this.hasGapTag = hasGapTag; + this.extractorFactory = extractorFactory; + this.muxedCaptionFormats = muxedCaptionFormats; + this.drmInitData = drmInitData; + this.previousExtractor = previousExtractor; + this.id3Decoder = id3Decoder; + this.scratchId3Data = scratchId3Data; + this.shouldSpliceIn = shouldSpliceIn; + uid = uidSource.getAndIncrement(); } /** @@ -163,7 +283,7 @@ import java.util.concurrent.atomic.AtomicInteger; * @param output The output that will receive the loaded samples. */ public void init(HlsSampleStreamWrapper output) { - extractorOutput = output; + this.output = output; output.init(uid, shouldSpliceIn); } @@ -172,11 +292,6 @@ import java.util.concurrent.atomic.AtomicInteger; return loadCompleted; } - @Override - public long bytesLoaded() { - return bytesLoaded; - } - // Loadable implementation @Override @@ -184,92 +299,128 @@ import java.util.concurrent.atomic.AtomicInteger; loadCanceled = true; } - @Override - public boolean isLoadCanceled() { - return loadCanceled; - } - @Override public void load() throws IOException, InterruptedException { - if (extractor == null && !isPackedAudio) { - // See HLS spec, version 20, Section 3.4 for more information on packed audio extraction. - extractor = createExtractor(); + // output == null means init() hasn't been called. + Assertions.checkNotNull(output); + if (extractor == null && previousExtractor != null) { + extractor = previousExtractor; + isExtractorReusable = true; + initDataLoadRequired = false; } maybeLoadInitData(); if (!loadCanceled) { - loadMedia(); + if (!hasGapTag) { + loadMedia(); + } + loadCompleted = true; } } - // Internal loading methods. + // Internal methods. + @RequiresNonNull("output") private void maybeLoadInitData() throws IOException, InterruptedException { - if (previousExtractor == extractor || initLoadCompleted || initDataSpec == null) { - // According to spec, for packed audio, initDataSpec is expected to be null. + if (!initDataLoadRequired) { return; } - DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded); - try { - ExtractorInput input = new DefaultExtractorInput(initDataSource, - initSegmentDataSpec.absoluteStreamPosition, initDataSource.open(initSegmentDataSpec)); - try { - int result = Extractor.RESULT_CONTINUE; - while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractor.read(input, null); - } - } finally { - initSegmentBytesLoaded = (int) (input.getPosition() - initDataSpec.absoluteStreamPosition); - } - } finally { - Util.closeQuietly(dataSource); - } - initLoadCompleted = true; + // initDataLoadRequired => initDataSource != null && initDataSpec != null + Assertions.checkNotNull(initDataSource); + Assertions.checkNotNull(initDataSpec); + feedDataToExtractor(initDataSource, initDataSpec, initSegmentEncrypted); + nextLoadPosition = 0; + initDataLoadRequired = false; } + @RequiresNonNull("output") private void loadMedia() throws IOException, InterruptedException { - // If we previously fed part of this chunk to the extractor, we need to skip it this time. For - // encrypted content we need to skip the data by reading it through the source, so as to ensure - // correct decryption of the remainder of the chunk. For clear content, we can request the - // remainder of the chunk directly. - DataSpec loadDataSpec; - boolean skipLoadedBytes; - if (isEncrypted) { - loadDataSpec = dataSpec; - skipLoadedBytes = bytesLoaded != 0; - } else { - loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); - skipLoadedBytes = false; - } if (!isMasterTimestampSource) { timestampAdjuster.waitUntilInitialized(); } else if (timestampAdjuster.getFirstSampleTimestampUs() == TimestampAdjuster.DO_NOT_OFFSET) { // We're the master and we haven't set the desired first sample timestamp yet. timestampAdjuster.setFirstSampleTimestampUs(startTimeUs); } + feedDataToExtractor(dataSource, dataSpec, mediaSegmentEncrypted); + } + + /** + * Attempts to feed the given {@code dataSpec} to {@code this.extractor}. Whenever the operation + * concludes (because of a thrown exception or because the operation finishes), the number of fed + * bytes is written to {@code nextLoadPosition}. + */ + @RequiresNonNull("output") + private void feedDataToExtractor( + DataSource dataSource, DataSpec dataSpec, boolean dataIsEncrypted) + throws IOException, InterruptedException { + // If we previously fed part of this chunk to the extractor, we need to skip it this time. For + // encrypted content we need to skip the data by reading it through the source, so as to ensure + // correct decryption of the remainder of the chunk. For clear content, we can request the + // remainder of the chunk directly. + DataSpec loadDataSpec; + boolean skipLoadedBytes; + if (dataIsEncrypted) { + loadDataSpec = dataSpec; + skipLoadedBytes = nextLoadPosition != 0; + } else { + loadDataSpec = dataSpec.subrange(nextLoadPosition); + skipLoadedBytes = false; + } try { - ExtractorInput input = new DefaultExtractorInput(dataSource, - loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); - if (extractor == null) { - // Media segment format is packed audio. - long id3Timestamp = peekId3PrivTimestamp(input); - extractor = buildPackedAudioExtractor(id3Timestamp != C.TIME_UNSET - ? timestampAdjuster.adjustTsTimestamp(id3Timestamp) : startTimeUs); - } + ExtractorInput input = prepareExtraction(dataSource, loadDataSpec); if (skipLoadedBytes) { - input.skipFully(bytesLoaded); + input.skipFully(nextLoadPosition); } try { int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractor.read(input, null); + result = extractor.read(input, DUMMY_POSITION_HOLDER); } } finally { - bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); + nextLoadPosition = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); } } finally { Util.closeQuietly(dataSource); } - loadCompleted = true; + } + + @RequiresNonNull("output") + @EnsuresNonNull("extractor") + private DefaultExtractorInput prepareExtraction(DataSource dataSource, DataSpec dataSpec) + throws IOException, InterruptedException { + long bytesToRead = dataSource.open(dataSpec); + DefaultExtractorInput extractorInput = + new DefaultExtractorInput(dataSource, dataSpec.absoluteStreamPosition, bytesToRead); + + if (extractor == null) { + long id3Timestamp = peekId3PrivTimestamp(extractorInput); + extractorInput.resetPeekPosition(); + + HlsExtractorFactory.Result result = + extractorFactory.createExtractor( + previousExtractor, + dataSpec.uri, + trackFormat, + muxedCaptionFormats, + timestampAdjuster, + dataSource.getResponseHeaders(), + extractorInput); + extractor = result.extractor; + isExtractorReusable = result.isReusable; + if (result.isPackedAudioExtractor) { + output.setSampleOffsetUs( + id3Timestamp != C.TIME_UNSET + ? timestampAdjuster.adjustTsTimestamp(id3Timestamp) + : startTimeUs); + } else { + // In case the container format changes mid-stream to non-packed-audio, we need to reset + // the timestamp offset. + output.setSampleOffsetUs(/* sampleOffsetUs= */ 0L); + } + output.onNewExtractor(); + extractor.init(output); + } + output.setDrmInitData(drmInitData); + return extractorInput; } /** @@ -284,26 +435,27 @@ import java.util.concurrent.atomic.AtomicInteger; */ private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, InterruptedException { input.resetPeekPosition(); - if (!input.peekFully(id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH, true)) { + try { + input.peekFully(scratchId3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH); + } catch (EOFException e) { + // The input isn't long enough for there to be any ID3 data. return C.TIME_UNSET; } - id3Data.reset(Id3Decoder.ID3_HEADER_LENGTH); - int id = id3Data.readUnsignedInt24(); + scratchId3Data.reset(Id3Decoder.ID3_HEADER_LENGTH); + int id = scratchId3Data.readUnsignedInt24(); if (id != Id3Decoder.ID3_TAG) { return C.TIME_UNSET; } - id3Data.skipBytes(3); // version(2), flags(1). - int id3Size = id3Data.readSynchSafeInt(); + scratchId3Data.skipBytes(3); // version(2), flags(1). + int id3Size = scratchId3Data.readSynchSafeInt(); int requiredCapacity = id3Size + Id3Decoder.ID3_HEADER_LENGTH; - if (requiredCapacity > id3Data.capacity()) { - byte[] data = id3Data.data; - id3Data.reset(requiredCapacity); - System.arraycopy(data, 0, id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH); + if (requiredCapacity > scratchId3Data.capacity()) { + byte[] data = scratchId3Data.data; + scratchId3Data.reset(requiredCapacity); + System.arraycopy(data, 0, scratchId3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH); } - if (!input.peekFully(id3Data.data, Id3Decoder.ID3_HEADER_LENGTH, id3Size, true)) { - return C.TIME_UNSET; - } - Metadata metadata = id3Decoder.decode(id3Data.data, id3Size); + input.peekFully(scratchId3Data.data, Id3Decoder.ID3_HEADER_LENGTH, id3Size); + Metadata metadata = id3Decoder.decode(scratchId3Data.data, id3Size); if (metadata == null) { return C.TIME_UNSET; } @@ -313,88 +465,55 @@ import java.util.concurrent.atomic.AtomicInteger; if (frame instanceof PrivFrame) { PrivFrame privFrame = (PrivFrame) frame; if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { - System.arraycopy(privFrame.privateData, 0, id3Data.data, 0, 8 /* timestamp size */); - id3Data.reset(8); - return id3Data.readLong(); + System.arraycopy( + privFrame.privateData, 0, scratchId3Data.data, 0, 8 /* timestamp size */); + scratchId3Data.reset(8); + // The top 31 bits should be zeros, but explicitly zero them to wrap in the case that the + // streaming provider forgot. See: https://github.com/google/ExoPlayer/pull/3495. + return scratchId3Data.readLong() & 0x1FFFFFFFFL; } } } return C.TIME_UNSET; } - // Internal factory methods. + // Internal methods. + + private static byte[] getEncryptionIvArray(String ivString) { + String trimmedIv; + if (Util.toLowerInvariant(ivString).startsWith("0x")) { + trimmedIv = ivString.substring(2); + } else { + trimmedIv = ivString; + } + + byte[] ivData = new BigInteger(trimmedIv, /* radix= */ 16).toByteArray(); + byte[] ivDataWithPadding = new byte[16]; + int offset = ivData.length > 16 ? ivData.length - 16 : 0; + System.arraycopy( + ivData, + offset, + ivDataWithPadding, + ivDataWithPadding.length - ivData.length + offset, + ivData.length - offset); + return ivDataWithPadding; + } /** - * If the content is encrypted, returns an {@link Aes128DataSource} that wraps the original in - * order to decrypt the loaded data. Else returns the original. + * If the segment is fully encrypted, returns an {@link Aes128DataSource} that wraps the original + * in order to decrypt the loaded data. Else returns the original. + * + *

{@code fullSegmentEncryptionKey} & {@code encryptionIv} can either both be null, or neither. */ - private static DataSource buildDataSource(DataSource dataSource, byte[] encryptionKey, - byte[] encryptionIv) { - if (encryptionKey == null || encryptionIv == null) { - return dataSource; + private static DataSource buildDataSource( + DataSource dataSource, + @Nullable byte[] fullSegmentEncryptionKey, + @Nullable byte[] encryptionIv) { + if (fullSegmentEncryptionKey != null) { + Assertions.checkNotNull(encryptionIv); + return new Aes128DataSource(dataSource, fullSegmentEncryptionKey, encryptionIv); } - return new Aes128DataSource(dataSource, encryptionKey, encryptionIv); - } - - private Extractor createExtractor() { - // Select the extractor that will read the chunk. - Extractor extractor; - boolean usingNewExtractor = true; - if (MimeTypes.TEXT_VTT.equals(hlsUrl.format.sampleMimeType) - || lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) - || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { - extractor = new WebvttExtractor(trackFormat.language, timestampAdjuster); - } else if (!needNewExtractor) { - // Only reuse TS and fMP4 extractors. - usingNewExtractor = false; - extractor = previousExtractor; - } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) - || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { - extractor = new FragmentedMp4Extractor(0, timestampAdjuster); - } else { - // MPEG-2 TS segments, but we need a new extractor. - // This flag ensures the change of pid between streams does not affect the sample queues. - @DefaultTsPayloadReaderFactory.Flags - int esReaderFactoryFlags = DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM; - if (!muxedCaptionFormats.isEmpty()) { - // The playlist declares closed caption renditions, we should ignore descriptors. - esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS; - } - String codecs = trackFormat.codecs; - if (!TextUtils.isEmpty(codecs)) { - // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really - // exist. If we know from the codec attribute that they don't exist, then we can - // explicitly ignore them even if they're declared. - if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) { - esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM; - } - if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) { - esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM; - } - } - extractor = new TsExtractor(TsExtractor.MODE_HLS, timestampAdjuster, - new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats)); - } - if (usingNewExtractor) { - extractor.init(extractorOutput); - } - return extractor; - } - - private Extractor buildPackedAudioExtractor(long startTimeUs) { - Extractor extractor; - if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) { - extractor = new AdtsExtractor(startTimeUs); - } else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION) - || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) { - extractor = new Ac3Extractor(startTimeUs); - } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { - extractor = new Mp3Extractor(0, startTimeUs); - } else { - throw new IllegalArgumentException("Unkown extension for audio file: " + lastPathSegment); - } - extractor.init(extractorOutput); - return extractor; + return dataSource; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 59c3a27dfc4a..60aa5298c37b 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -15,27 +15,47 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls; -import android.os.Handler; +import android.net.Uri; import android.text.TextUtils; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.CompositeSequenceableLoader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaPeriod; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleStream; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SequenceableLoader; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Rendition; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; +import java.util.Map; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link MediaPeriod} that loads an HLS stream. @@ -43,72 +63,204 @@ import java.util.List; public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper.Callback, HlsPlaylistTracker.PlaylistEventListener { + private final HlsExtractorFactory extractorFactory; private final HlsPlaylistTracker playlistTracker; private final HlsDataSourceFactory dataSourceFactory; - private final int minLoadableRetryCount; + @Nullable private final TransferListener mediaTransferListener; + private final DrmSessionManager drmSessionManager; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Allocator allocator; private final IdentityHashMap streamWrapperIndices; private final TimestampAdjusterProvider timestampAdjusterProvider; - private final Handler continueLoadingHandler; - private final long preparePositionUs; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final boolean allowChunklessPreparation; + private final @HlsMediaSource.MetadataType int metadataType; + private final boolean useSessionKeys; - private Callback callback; + @Nullable private Callback callback; private int pendingPrepareCount; - private boolean seenFirstTrackSelection; - private TrackGroupArray trackGroups; + private @MonotonicNonNull TrackGroupArray trackGroups; private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; - private CompositeSequenceableLoader sequenceableLoader; + // Maps sample stream wrappers to variant/rendition index by matching array positions. + private int[][] manifestUrlIndicesPerWrapper; + private SequenceableLoader compositeSequenceableLoader; + private boolean notifiedReadingStarted; - public HlsMediaPeriod(HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory, - int minLoadableRetryCount, EventDispatcher eventDispatcher, Allocator allocator, - long positionUs) { + /** + * Creates an HLS media period. + * + * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the segments. + * @param playlistTracker A tracker for HLS playlists. + * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for segments + * and keys. + * @param mediaTransferListener The transfer listener to inform of any media data transfers. May + * be null if no listener is available. + * @param drmSessionManager The {@link DrmSessionManager} to acquire {@link DrmSession + * DrmSessions} with. + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @param eventDispatcher A dispatcher to notify of events. + * @param allocator An {@link Allocator} from which to obtain media buffer allocations. + * @param compositeSequenceableLoaderFactory A factory to create composite {@link + * SequenceableLoader}s for when this media source loads data from multiple streams. + * @param allowChunklessPreparation Whether chunkless preparation is allowed. + * @param useSessionKeys Whether to use #EXT-X-SESSION-KEY tags. + */ + public HlsMediaPeriod( + HlsExtractorFactory extractorFactory, + HlsPlaylistTracker playlistTracker, + HlsDataSourceFactory dataSourceFactory, + @Nullable TransferListener mediaTransferListener, + DrmSessionManager drmSessionManager, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, + EventDispatcher eventDispatcher, + Allocator allocator, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + boolean allowChunklessPreparation, + @HlsMediaSource.MetadataType int metadataType, + boolean useSessionKeys) { + this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; this.dataSourceFactory = dataSourceFactory; - this.minLoadableRetryCount = minLoadableRetryCount; + this.mediaTransferListener = mediaTransferListener; + this.drmSessionManager = drmSessionManager; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.allocator = allocator; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + this.allowChunklessPreparation = allowChunklessPreparation; + this.metadataType = metadataType; + this.useSessionKeys = useSessionKeys; + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(); streamWrapperIndices = new IdentityHashMap<>(); timestampAdjusterProvider = new TimestampAdjusterProvider(); - continueLoadingHandler = new Handler(); - preparePositionUs = positionUs; + sampleStreamWrappers = new HlsSampleStreamWrapper[0]; + enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0]; + manifestUrlIndicesPerWrapper = new int[0][]; + eventDispatcher.mediaPeriodCreated(); } public void release() { playlistTracker.removeListener(this); - continueLoadingHandler.removeCallbacksAndMessages(null); - if (sampleStreamWrappers != null) { - for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { - sampleStreamWrapper.release(); - } + for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { + sampleStreamWrapper.release(); } + callback = null; + eventDispatcher.mediaPeriodReleased(); } @Override - public void prepare(Callback callback) { - playlistTracker.addListener(this); + public void prepare(Callback callback, long positionUs) { this.callback = callback; - buildAndPrepareSampleStreamWrappers(); + playlistTracker.addListener(this); + buildAndPrepareSampleStreamWrappers(positionUs); } @Override public void maybeThrowPrepareError() throws IOException { - if (sampleStreamWrappers != null) { - for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { - sampleStreamWrapper.maybeThrowPrepareError(); - } + for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { + sampleStreamWrapper.maybeThrowPrepareError(); } } @Override public TrackGroupArray getTrackGroups() { - return trackGroups; + // trackGroups will only be null if period hasn't been prepared or has been released. + return Assertions.checkNotNull(trackGroups); + } + + // TODO: When the master playlist does not de-duplicate variants by URL and allows Renditions with + // null URLs, this method must be updated to calculate stream keys that are compatible with those + // that may already be persisted for offline. + @Override + public List getStreamKeys(List trackSelections) { + // See HlsMasterPlaylist.copy for interpretation of StreamKeys. + HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist()); + boolean hasVariants = !masterPlaylist.variants.isEmpty(); + int audioWrapperOffset = hasVariants ? 1 : 0; + // Subtitle sample stream wrappers are held last. + int subtitleWrapperOffset = sampleStreamWrappers.length - masterPlaylist.subtitles.size(); + + TrackGroupArray mainWrapperTrackGroups; + int mainWrapperPrimaryGroupIndex; + int[] mainWrapperVariantIndices; + if (hasVariants) { + HlsSampleStreamWrapper mainWrapper = sampleStreamWrappers[0]; + mainWrapperVariantIndices = manifestUrlIndicesPerWrapper[0]; + mainWrapperTrackGroups = mainWrapper.getTrackGroups(); + mainWrapperPrimaryGroupIndex = mainWrapper.getPrimaryTrackGroupIndex(); + } else { + mainWrapperVariantIndices = new int[0]; + mainWrapperTrackGroups = TrackGroupArray.EMPTY; + mainWrapperPrimaryGroupIndex = 0; + } + + List streamKeys = new ArrayList<>(); + boolean needsPrimaryTrackGroupSelection = false; + boolean hasPrimaryTrackGroupSelection = false; + for (TrackSelection trackSelection : trackSelections) { + TrackGroup trackSelectionGroup = trackSelection.getTrackGroup(); + int mainWrapperTrackGroupIndex = mainWrapperTrackGroups.indexOf(trackSelectionGroup); + if (mainWrapperTrackGroupIndex != C.INDEX_UNSET) { + if (mainWrapperTrackGroupIndex == mainWrapperPrimaryGroupIndex) { + // Primary group in main wrapper. + hasPrimaryTrackGroupSelection = true; + for (int i = 0; i < trackSelection.length(); i++) { + int variantIndex = mainWrapperVariantIndices[trackSelection.getIndexInTrackGroup(i)]; + streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, variantIndex)); + } + } else { + // Embedded group in main wrapper. + needsPrimaryTrackGroupSelection = true; + } + } else { + // Audio or subtitle group. + for (int i = audioWrapperOffset; i < sampleStreamWrappers.length; i++) { + TrackGroupArray wrapperTrackGroups = sampleStreamWrappers[i].getTrackGroups(); + int selectedTrackGroupIndex = wrapperTrackGroups.indexOf(trackSelectionGroup); + if (selectedTrackGroupIndex != C.INDEX_UNSET) { + int groupIndexType = + i < subtitleWrapperOffset + ? HlsMasterPlaylist.GROUP_INDEX_AUDIO + : HlsMasterPlaylist.GROUP_INDEX_SUBTITLE; + int[] selectedWrapperUrlIndices = manifestUrlIndicesPerWrapper[i]; + for (int trackIndex = 0; trackIndex < trackSelection.length(); trackIndex++) { + int renditionIndex = + selectedWrapperUrlIndices[trackSelection.getIndexInTrackGroup(trackIndex)]; + streamKeys.add(new StreamKey(groupIndexType, renditionIndex)); + } + break; + } + } + } + } + if (needsPrimaryTrackGroupSelection && !hasPrimaryTrackGroupSelection) { + // A track selection includes a variant-embedded track, but no variant is added yet. We use + // the valid variant with the lowest bitrate to reduce overhead. + int lowestBitrateIndex = mainWrapperVariantIndices[0]; + int lowestBitrate = masterPlaylist.variants.get(mainWrapperVariantIndices[0]).format.bitrate; + for (int i = 1; i < mainWrapperVariantIndices.length; i++) { + int variantBitrate = + masterPlaylist.variants.get(mainWrapperVariantIndices[i]).format.bitrate; + if (variantBitrate < lowestBitrate) { + lowestBitrate = variantBitrate; + lowestBitrateIndex = mainWrapperVariantIndices[i]; + } + } + streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, lowestBitrateIndex)); + } + return streamKeys; } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { // Map each selection and stream onto a child period index. int[] streamChildIndices = new int[selections.length]; int[] selectionChildIndices = new int[selections.length]; @@ -126,110 +278,137 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } } } - boolean selectedNewTracks = false; + + boolean forceReset = false; streamWrapperIndices.clear(); // Select tracks for each child, copying the resulting streams back into a new streams array. SampleStream[] newStreams = new SampleStream[selections.length]; - SampleStream[] childStreams = new SampleStream[selections.length]; - TrackSelection[] childSelections = new TrackSelection[selections.length]; - ArrayList enabledSampleStreamWrapperList = new ArrayList<>( - sampleStreamWrappers.length); + @NullableType SampleStream[] childStreams = new SampleStream[selections.length]; + @NullableType TrackSelection[] childSelections = new TrackSelection[selections.length]; + int newEnabledSampleStreamWrapperCount = 0; + HlsSampleStreamWrapper[] newEnabledSampleStreamWrappers = + new HlsSampleStreamWrapper[sampleStreamWrappers.length]; for (int i = 0; i < sampleStreamWrappers.length; i++) { for (int j = 0; j < selections.length; j++) { childStreams[j] = streamChildIndices[j] == i ? streams[j] : null; childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null; } - selectedNewTracks |= sampleStreamWrappers[i].selectTracks(childSelections, - mayRetainStreamFlags, childStreams, streamResetFlags, !seenFirstTrackSelection); + HlsSampleStreamWrapper sampleStreamWrapper = sampleStreamWrappers[i]; + boolean wasReset = sampleStreamWrapper.selectTracks(childSelections, mayRetainStreamFlags, + childStreams, streamResetFlags, positionUs, forceReset); boolean wrapperEnabled = false; for (int j = 0; j < selections.length; j++) { + SampleStream childStream = childStreams[j]; if (selectionChildIndices[j] == i) { // Assert that the child provided a stream for the selection. - Assertions.checkState(childStreams[j] != null); - newStreams[j] = childStreams[j]; + Assertions.checkNotNull(childStream); + newStreams[j] = childStream; wrapperEnabled = true; - streamWrapperIndices.put(childStreams[j], i); + streamWrapperIndices.put(childStream, i); } else if (streamChildIndices[j] == i) { // Assert that the child cleared any previous stream. - Assertions.checkState(childStreams[j] == null); + Assertions.checkState(childStream == null); } } if (wrapperEnabled) { - enabledSampleStreamWrapperList.add(sampleStreamWrappers[i]); + newEnabledSampleStreamWrappers[newEnabledSampleStreamWrapperCount] = sampleStreamWrapper; + if (newEnabledSampleStreamWrapperCount++ == 0) { + // The first enabled wrapper is responsible for initializing timestamp adjusters. This + // way, if enabled, variants are responsible. Else audio renditions. Else text renditions. + sampleStreamWrapper.setIsTimestampMaster(true); + if (wasReset || enabledSampleStreamWrappers.length == 0 + || sampleStreamWrapper != enabledSampleStreamWrappers[0]) { + // The wrapper responsible for initializing the timestamp adjusters was reset or + // changed. We need to reset the timestamp adjuster provider and all other wrappers. + timestampAdjusterProvider.reset(); + forceReset = true; + } + } else { + sampleStreamWrapper.setIsTimestampMaster(false); + } } } // Copy the new streams back into the streams array. System.arraycopy(newStreams, 0, streams, 0, newStreams.length); // Update the local state. - enabledSampleStreamWrappers = new HlsSampleStreamWrapper[enabledSampleStreamWrapperList.size()]; - enabledSampleStreamWrapperList.toArray(enabledSampleStreamWrappers); - - // The first enabled sample stream wrapper is responsible for intializing the timestamp - // adjuster. This way, if present, variants are responsible. Otherwise, audio renditions are. - // If only subtitles are present, then text renditions are used for timestamp adjustment - // initialization. - if (enabledSampleStreamWrappers.length > 0) { - enabledSampleStreamWrappers[0].setIsTimestampMaster(true); - for (int i = 1; i < enabledSampleStreamWrappers.length; i++) { - enabledSampleStreamWrappers[i].setIsTimestampMaster(false); - } - } - - sequenceableLoader = new CompositeSequenceableLoader(enabledSampleStreamWrappers); - if (seenFirstTrackSelection && selectedNewTracks) { - seekToUs(positionUs); - // We'll need to reset renderers consuming from all streams due to the seek. - for (int i = 0; i < selections.length; i++) { - if (streams[i] != null) { - streamResetFlags[i] = true; - } - } - } - seenFirstTrackSelection = true; + enabledSampleStreamWrappers = + Util.nullSafeArrayCopy(newEnabledSampleStreamWrappers, newEnabledSampleStreamWrapperCount); + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader( + enabledSampleStreamWrappers); return positionUs; } @Override - public void discardBuffer(long positionUs) { - // Do nothing. + public void discardBuffer(long positionUs, boolean toKeyframe) { + for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { + sampleStreamWrapper.discardBuffer(positionUs, toKeyframe); + } + } + + @Override + public void reevaluateBuffer(long positionUs) { + compositeSequenceableLoader.reevaluateBuffer(positionUs); } @Override public boolean continueLoading(long positionUs) { - return sequenceableLoader.continueLoading(positionUs); + if (trackGroups == null) { + // Preparation is still going on. + for (HlsSampleStreamWrapper wrapper : sampleStreamWrappers) { + wrapper.continuePreparing(); + } + return false; + } else { + return compositeSequenceableLoader.continueLoading(positionUs); + } + } + + @Override + public boolean isLoading() { + return compositeSequenceableLoader.isLoading(); } @Override public long getNextLoadPositionUs() { - return sequenceableLoader.getNextLoadPositionUs(); + return compositeSequenceableLoader.getNextLoadPositionUs(); } @Override public long readDiscontinuity() { + if (!notifiedReadingStarted) { + eventDispatcher.readingStarted(); + notifiedReadingStarted = true; + } return C.TIME_UNSET; } @Override public long getBufferedPositionUs() { - long bufferedPositionUs = Long.MAX_VALUE; - for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { - long rendererBufferedPositionUs = sampleStreamWrapper.getBufferedPositionUs(); - if (rendererBufferedPositionUs != C.TIME_END_OF_SOURCE) { - bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs); - } - } - return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs; + return compositeSequenceableLoader.getBufferedPositionUs(); } @Override public long seekToUs(long positionUs) { - timestampAdjusterProvider.reset(); - for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { - sampleStreamWrapper.seekTo(positionUs); + if (enabledSampleStreamWrappers.length > 0) { + // We need to reset all wrappers if the one responsible for initializing timestamp adjusters + // is reset. Else each wrapper can decide whether to reset independently. + boolean forceReset = enabledSampleStreamWrappers[0].seekToUs(positionUs, false); + for (int i = 1; i < enabledSampleStreamWrappers.length; i++) { + enabledSampleStreamWrappers[i].seekToUs(positionUs, forceReset); + } + if (forceReset) { + timestampAdjusterProvider.reset(); + } } return positionUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return positionUs; + } + // HlsSampleStreamWrapper.Callback implementation. @Override @@ -255,16 +434,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } @Override - public void onPlaylistRefreshRequired(HlsUrl url) { + public void onPlaylistRefreshRequired(Uri url) { playlistTracker.refreshPlaylist(url); } @Override public void onContinueLoadingRequested(HlsSampleStreamWrapper sampleStreamWrapper) { - if (trackGroups == null) { - // Still preparing. - return; - } callback.onContinueLoadingRequested(this); } @@ -272,112 +447,412 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public void onPlaylistChanged() { - continuePreparingOrLoading(); + callback.onContinueLoadingRequested(this); } @Override - public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { + public boolean onPlaylistError(Uri url, long blacklistDurationMs) { + boolean noBlacklistingFailure = true; for (HlsSampleStreamWrapper streamWrapper : sampleStreamWrappers) { - streamWrapper.onPlaylistBlacklisted(url, blacklistMs); + noBlacklistingFailure &= streamWrapper.onPlaylistError(url, blacklistDurationMs); } - continuePreparingOrLoading(); + callback.onContinueLoadingRequested(this); + return noBlacklistingFailure; } // Internal methods. - private void buildAndPrepareSampleStreamWrappers() { - HlsMasterPlaylist masterPlaylist = playlistTracker.getMasterPlaylist(); - // Build the default stream wrapper. - List selectedVariants = new ArrayList<>(masterPlaylist.variants); - ArrayList definiteVideoVariants = new ArrayList<>(); - ArrayList definiteAudioOnlyVariants = new ArrayList<>(); - for (int i = 0; i < selectedVariants.size(); i++) { - HlsUrl variant = selectedVariants.get(i); - if (variant.format.height > 0 || variantHasExplicitCodecWithPrefix(variant, "avc")) { - definiteVideoVariants.add(variant); - } else if (variantHasExplicitCodecWithPrefix(variant, "mp4a")) { - definiteAudioOnlyVariants.add(variant); - } - } - if (!definiteVideoVariants.isEmpty()) { - // We've identified some variants as definitely containing video. Assume variants within the - // master playlist are marked consistently, and hence that we have the full set. Filter out - // any other variants, which are likely to be audio only. - selectedVariants = definiteVideoVariants; - } else if (definiteAudioOnlyVariants.size() < selectedVariants.size()) { - // We've identified some variants, but not all, as being audio only. Filter them out to leave - // the remaining variants, which are likely to contain video. - selectedVariants.removeAll(definiteAudioOnlyVariants); - } else { - // Leave the enabled variants unchanged. They're likely either all video or all audio. - } - List audioRenditions = masterPlaylist.audios; - List subtitleRenditions = masterPlaylist.subtitles; - sampleStreamWrappers = new HlsSampleStreamWrapper[1 /* variants */ + audioRenditions.size() - + subtitleRenditions.size()]; - int currentWrapperIndex = 0; - pendingPrepareCount = sampleStreamWrappers.length; + private void buildAndPrepareSampleStreamWrappers(long positionUs) { + HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist()); + Map overridingDrmInitData = + useSessionKeys + ? deriveOverridingDrmInitData(masterPlaylist.sessionKeyDrmInitData) + : Collections.emptyMap(); - Assertions.checkArgument(!selectedVariants.isEmpty()); - HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()]; - selectedVariants.toArray(variants); - HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, - variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats); - sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; - sampleStreamWrapper.setIsTimestampMaster(true); - sampleStreamWrapper.continuePreparing(); + boolean hasVariants = !masterPlaylist.variants.isEmpty(); + List audioRenditions = masterPlaylist.audios; + List subtitleRenditions = masterPlaylist.subtitles; + + pendingPrepareCount = 0; + ArrayList sampleStreamWrappers = new ArrayList<>(); + ArrayList manifestUrlIndicesPerWrapper = new ArrayList<>(); + + if (hasVariants) { + buildAndPrepareMainSampleStreamWrapper( + masterPlaylist, + positionUs, + sampleStreamWrappers, + manifestUrlIndicesPerWrapper, + overridingDrmInitData); + } // TODO: Build video stream wrappers here. - // Build audio stream wrappers. - for (int i = 0; i < audioRenditions.size(); i++) { - sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, - new HlsUrl[] {audioRenditions.get(i)}, null, Collections.emptyList()); - sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; + buildAndPrepareAudioSampleStreamWrappers( + positionUs, + audioRenditions, + sampleStreamWrappers, + manifestUrlIndicesPerWrapper, + overridingDrmInitData); + + // Subtitle stream wrappers. We can always use master playlist information to prepare these. + for (int i = 0; i < subtitleRenditions.size(); i++) { + Rendition subtitleRendition = subtitleRenditions.get(i); + HlsSampleStreamWrapper sampleStreamWrapper = + buildSampleStreamWrapper( + C.TRACK_TYPE_TEXT, + new Uri[] {subtitleRendition.url}, + new Format[] {subtitleRendition.format}, + null, + Collections.emptyList(), + overridingDrmInitData, + positionUs); + manifestUrlIndicesPerWrapper.add(new int[] {i}); + sampleStreamWrappers.add(sampleStreamWrapper); + sampleStreamWrapper.prepareWithMasterPlaylistInfo( + new TrackGroup[] {new TrackGroup(subtitleRendition.format)}, + /* primaryTrackGroupIndex= */ 0); + } + + this.sampleStreamWrappers = sampleStreamWrappers.toArray(new HlsSampleStreamWrapper[0]); + this.manifestUrlIndicesPerWrapper = manifestUrlIndicesPerWrapper.toArray(new int[0][]); + pendingPrepareCount = this.sampleStreamWrappers.length; + // Set timestamp master and trigger preparation (if not already prepared) + this.sampleStreamWrappers[0].setIsTimestampMaster(true); + for (HlsSampleStreamWrapper sampleStreamWrapper : this.sampleStreamWrappers) { sampleStreamWrapper.continuePreparing(); } + // All wrappers are enabled during preparation. + enabledSampleStreamWrappers = this.sampleStreamWrappers; + } - // Build subtitle stream wrappers. - for (int i = 0; i < subtitleRenditions.size(); i++) { - HlsUrl url = subtitleRenditions.get(i); - sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null, - Collections.emptyList()); - sampleStreamWrapper.prepareSingleTrack(url.format); - sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; + /** + * This method creates and starts preparation of the main {@link HlsSampleStreamWrapper}. + * + *

The main sample stream wrapper is the first element of {@link #sampleStreamWrappers}. It + * provides {@link SampleStream}s for the variant urls in the master playlist. It may be adaptive + * and may contain multiple muxed tracks. + * + *

If chunkless preparation is allowed, the media period will try preparation without segment + * downloads. This is only possible if variants contain the CODECS attribute. If not, traditional + * preparation with segment downloads will take place. The following points apply to chunkless + * preparation: + * + *

    + *
  • A muxed audio track will be exposed if the codecs list contain an audio entry and the + * master playlist either contains an EXT-X-MEDIA tag without the URI attribute or does not + * contain any EXT-X-MEDIA tag. + *
  • Closed captions will only be exposed if they are declared by the master playlist. + *
  • An ID3 track is exposed preemptively, in case the segments contain an ID3 track. + *
+ * + * @param masterPlaylist The HLS master playlist. + * @param positionUs If preparation requires any chunk downloads, the position in microseconds at + * which downloading should start. Ignored otherwise. + * @param sampleStreamWrappers List to which the built main sample stream wrapper should be added. + * @param manifestUrlIndicesPerWrapper List to which the selected variant indices should be added. + * @param overridingDrmInitData Overriding {@link DrmInitData}, keyed by protection scheme type + * (i.e. {@link DrmInitData#schemeType}). + */ + private void buildAndPrepareMainSampleStreamWrapper( + HlsMasterPlaylist masterPlaylist, + long positionUs, + List sampleStreamWrappers, + List manifestUrlIndicesPerWrapper, + Map overridingDrmInitData) { + int[] variantTypes = new int[masterPlaylist.variants.size()]; + int videoVariantCount = 0; + int audioVariantCount = 0; + for (int i = 0; i < masterPlaylist.variants.size(); i++) { + Variant variant = masterPlaylist.variants.get(i); + Format format = variant.format; + if (format.height > 0 || Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_VIDEO) != null) { + variantTypes[i] = C.TRACK_TYPE_VIDEO; + videoVariantCount++; + } else if (Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_AUDIO) != null) { + variantTypes[i] = C.TRACK_TYPE_AUDIO; + audioVariantCount++; + } else { + variantTypes[i] = C.TRACK_TYPE_UNKNOWN; + } + } + boolean useVideoVariantsOnly = false; + boolean useNonAudioVariantsOnly = false; + int selectedVariantsCount = variantTypes.length; + if (videoVariantCount > 0) { + // We've identified some variants as definitely containing video. Assume variants within the + // master playlist are marked consistently, and hence that we have the full set. Filter out + // any other variants, which are likely to be audio only. + useVideoVariantsOnly = true; + selectedVariantsCount = videoVariantCount; + } else if (audioVariantCount < variantTypes.length) { + // We've identified some variants, but not all, as being audio only. Filter them out to leave + // the remaining variants, which are likely to contain video. + useNonAudioVariantsOnly = true; + selectedVariantsCount = variantTypes.length - audioVariantCount; + } + Uri[] selectedPlaylistUrls = new Uri[selectedVariantsCount]; + Format[] selectedPlaylistFormats = new Format[selectedVariantsCount]; + int[] selectedVariantIndices = new int[selectedVariantsCount]; + int outIndex = 0; + for (int i = 0; i < masterPlaylist.variants.size(); i++) { + if ((!useVideoVariantsOnly || variantTypes[i] == C.TRACK_TYPE_VIDEO) + && (!useNonAudioVariantsOnly || variantTypes[i] != C.TRACK_TYPE_AUDIO)) { + Variant variant = masterPlaylist.variants.get(i); + selectedPlaylistUrls[outIndex] = variant.url; + selectedPlaylistFormats[outIndex] = variant.format; + selectedVariantIndices[outIndex++] = i; + } + } + String codecs = selectedPlaylistFormats[0].codecs; + HlsSampleStreamWrapper sampleStreamWrapper = + buildSampleStreamWrapper( + C.TRACK_TYPE_DEFAULT, + selectedPlaylistUrls, + selectedPlaylistFormats, + masterPlaylist.muxedAudioFormat, + masterPlaylist.muxedCaptionFormats, + overridingDrmInitData, + positionUs); + sampleStreamWrappers.add(sampleStreamWrapper); + manifestUrlIndicesPerWrapper.add(selectedVariantIndices); + if (allowChunklessPreparation && codecs != null) { + boolean variantsContainVideoCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO) != null; + boolean variantsContainAudioCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO) != null; + List muxedTrackGroups = new ArrayList<>(); + if (variantsContainVideoCodecs) { + Format[] videoFormats = new Format[selectedVariantsCount]; + for (int i = 0; i < videoFormats.length; i++) { + videoFormats[i] = deriveVideoFormat(selectedPlaylistFormats[i]); + } + muxedTrackGroups.add(new TrackGroup(videoFormats)); + + if (variantsContainAudioCodecs + && (masterPlaylist.muxedAudioFormat != null || masterPlaylist.audios.isEmpty())) { + muxedTrackGroups.add( + new TrackGroup( + deriveAudioFormat( + selectedPlaylistFormats[0], + masterPlaylist.muxedAudioFormat, + /* isPrimaryTrackInVariant= */ false))); + } + List ccFormats = masterPlaylist.muxedCaptionFormats; + if (ccFormats != null) { + for (int i = 0; i < ccFormats.size(); i++) { + muxedTrackGroups.add(new TrackGroup(ccFormats.get(i))); + } + } + } else if (variantsContainAudioCodecs) { + // Variants only contain audio. + Format[] audioFormats = new Format[selectedVariantsCount]; + for (int i = 0; i < audioFormats.length; i++) { + audioFormats[i] = + deriveAudioFormat( + /* variantFormat= */ selectedPlaylistFormats[i], + masterPlaylist.muxedAudioFormat, + /* isPrimaryTrackInVariant= */ true); + } + muxedTrackGroups.add(new TrackGroup(audioFormats)); + } else { + // Variants contain codecs but no video or audio entries could be identified. + throw new IllegalArgumentException("Unexpected codecs attribute: " + codecs); + } + + TrackGroup id3TrackGroup = + new TrackGroup( + Format.createSampleFormat( + /* id= */ "ID3", + MimeTypes.APPLICATION_ID3, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* drmInitData= */ null)); + muxedTrackGroups.add(id3TrackGroup); + + sampleStreamWrapper.prepareWithMasterPlaylistInfo( + muxedTrackGroups.toArray(new TrackGroup[0]), + /* primaryTrackGroupIndex= */ 0, + /* optionalTrackGroupsIndices= */ muxedTrackGroups.indexOf(id3TrackGroup)); } } - private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, - Format muxedAudioFormat, List muxedCaptionFormats) { - HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, - dataSourceFactory, timestampAdjusterProvider, muxedCaptionFormats); - return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, - preparePositionUs, muxedAudioFormat, minLoadableRetryCount, eventDispatcher); + private void buildAndPrepareAudioSampleStreamWrappers( + long positionUs, + List audioRenditions, + List sampleStreamWrappers, + List manifestUrlsIndicesPerWrapper, + Map overridingDrmInitData) { + ArrayList scratchPlaylistUrls = + new ArrayList<>(/* initialCapacity= */ audioRenditions.size()); + ArrayList scratchPlaylistFormats = + new ArrayList<>(/* initialCapacity= */ audioRenditions.size()); + ArrayList scratchIndicesList = + new ArrayList<>(/* initialCapacity= */ audioRenditions.size()); + HashSet alreadyGroupedNames = new HashSet<>(); + for (int renditionByNameIndex = 0; + renditionByNameIndex < audioRenditions.size(); + renditionByNameIndex++) { + String name = audioRenditions.get(renditionByNameIndex).name; + if (!alreadyGroupedNames.add(name)) { + // This name already has a corresponding group. + continue; + } + + boolean renditionsHaveCodecs = true; + scratchPlaylistUrls.clear(); + scratchPlaylistFormats.clear(); + scratchIndicesList.clear(); + // Group all renditions with matching name. + for (int renditionIndex = 0; renditionIndex < audioRenditions.size(); renditionIndex++) { + if (Util.areEqual(name, audioRenditions.get(renditionIndex).name)) { + Rendition rendition = audioRenditions.get(renditionIndex); + scratchIndicesList.add(renditionIndex); + scratchPlaylistUrls.add(rendition.url); + scratchPlaylistFormats.add(rendition.format); + renditionsHaveCodecs &= rendition.format.codecs != null; + } + } + + HlsSampleStreamWrapper sampleStreamWrapper = + buildSampleStreamWrapper( + C.TRACK_TYPE_AUDIO, + scratchPlaylistUrls.toArray(Util.castNonNullTypeArray(new Uri[0])), + scratchPlaylistFormats.toArray(new Format[0]), + /* muxedAudioFormat= */ null, + /* muxedCaptionFormats= */ Collections.emptyList(), + overridingDrmInitData, + positionUs); + manifestUrlsIndicesPerWrapper.add(Util.toArray(scratchIndicesList)); + sampleStreamWrappers.add(sampleStreamWrapper); + + if (allowChunklessPreparation && renditionsHaveCodecs) { + Format[] renditionFormats = scratchPlaylistFormats.toArray(new Format[0]); + sampleStreamWrapper.prepareWithMasterPlaylistInfo( + new TrackGroup[] {new TrackGroup(renditionFormats)}, /* primaryTrackGroupIndex= */ 0); + } + } } - private void continuePreparingOrLoading() { - if (trackGroups != null) { - callback.onContinueLoadingRequested(this); + private HlsSampleStreamWrapper buildSampleStreamWrapper( + int trackType, + Uri[] playlistUrls, + Format[] playlistFormats, + @Nullable Format muxedAudioFormat, + @Nullable List muxedCaptionFormats, + Map overridingDrmInitData, + long positionUs) { + HlsChunkSource defaultChunkSource = + new HlsChunkSource( + extractorFactory, + playlistTracker, + playlistUrls, + playlistFormats, + dataSourceFactory, + mediaTransferListener, + timestampAdjusterProvider, + muxedCaptionFormats); + return new HlsSampleStreamWrapper( + trackType, + /* callback= */ this, + defaultChunkSource, + overridingDrmInitData, + allocator, + positionUs, + muxedAudioFormat, + drmSessionManager, + loadErrorHandlingPolicy, + eventDispatcher, + metadataType); + } + + private static Map deriveOverridingDrmInitData( + List sessionKeyDrmInitData) { + ArrayList mutableSessionKeyDrmInitData = new ArrayList<>(sessionKeyDrmInitData); + HashMap drmInitDataBySchemeType = new HashMap<>(); + for (int i = 0; i < mutableSessionKeyDrmInitData.size(); i++) { + DrmInitData drmInitData = sessionKeyDrmInitData.get(i); + String scheme = drmInitData.schemeType; + // Merge any subsequent drmInitData instances that have the same scheme type. This is valid + // due to the assumptions documented on HlsMediaSource.Builder.setUseSessionKeys, and is + // necessary to get data for different CDNs (e.g. Widevine and PlayReady) into a single + // drmInitData. + int j = i + 1; + while (j < mutableSessionKeyDrmInitData.size()) { + DrmInitData nextDrmInitData = mutableSessionKeyDrmInitData.get(j); + if (TextUtils.equals(nextDrmInitData.schemeType, scheme)) { + drmInitData = drmInitData.merge(nextDrmInitData); + mutableSessionKeyDrmInitData.remove(j); + } else { + j++; + } + } + drmInitDataBySchemeType.put(scheme, drmInitData); + } + return drmInitDataBySchemeType; + } + + private static Format deriveVideoFormat(Format variantFormat) { + String codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO); + String sampleMimeType = MimeTypes.getMediaMimeType(codecs); + return Format.createVideoContainerFormat( + variantFormat.id, + variantFormat.label, + variantFormat.containerMimeType, + sampleMimeType, + codecs, + variantFormat.metadata, + variantFormat.bitrate, + variantFormat.width, + variantFormat.height, + variantFormat.frameRate, + /* initializationData= */ null, + variantFormat.selectionFlags, + variantFormat.roleFlags); + } + + private static Format deriveAudioFormat( + Format variantFormat, @Nullable Format mediaTagFormat, boolean isPrimaryTrackInVariant) { + String codecs; + Metadata metadata; + int channelCount = Format.NO_VALUE; + int selectionFlags = 0; + int roleFlags = 0; + String language = null; + String label = null; + if (mediaTagFormat != null) { + codecs = mediaTagFormat.codecs; + metadata = mediaTagFormat.metadata; + channelCount = mediaTagFormat.channelCount; + selectionFlags = mediaTagFormat.selectionFlags; + roleFlags = mediaTagFormat.roleFlags; + language = mediaTagFormat.language; + label = mediaTagFormat.label; } else { - // Some of the wrappers were waiting for their media playlist to prepare. - for (HlsSampleStreamWrapper wrapper : sampleStreamWrappers) { - wrapper.continuePreparing(); + codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_AUDIO); + metadata = variantFormat.metadata; + if (isPrimaryTrackInVariant) { + channelCount = variantFormat.channelCount; + selectionFlags = variantFormat.selectionFlags; + roleFlags = variantFormat.roleFlags; + language = variantFormat.language; + label = variantFormat.label; } } - } - - private static boolean variantHasExplicitCodecWithPrefix(HlsUrl variant, String prefix) { - String codecs = variant.format.codecs; - if (TextUtils.isEmpty(codecs)) { - return false; - } - String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)"); - for (String codec : codecArray) { - if (codec.startsWith(prefix)) { - return true; - } - } - return false; + String sampleMimeType = MimeTypes.getMediaMimeType(codecs); + int bitrate = isPrimaryTrackInVariant ? variantFormat.bitrate : Format.NO_VALUE; + return Format.createAudioContainerFormat( + variantFormat.id, + label, + variantFormat.containerMimeType, + sampleMimeType, + codecs, + metadata, + bitrate, + channelCount, + /* sampleRate= */ Format.NO_VALUE, + /* initializationData= */ null, + selectionFlags, + roleFlags, + language); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 5c27830d4d3d..2fa49e13f0fa 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -15,83 +15,433 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls; +import static java.lang.annotation.RetentionPolicy.SOURCE; + import android.net.Uri; import android.os.Handler; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; -import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.BaseMediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaPeriod; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SequenceableLoader; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SinglePeriodTimeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.FilteringHlsPlaylistParserFactory; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParserFactory; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; import java.util.List; -/** - * An HLS {@link MediaSource}. - */ -public final class HlsMediaSource implements MediaSource, - HlsPlaylistTracker.PrimaryPlaylistListener { +/** An HLS {@link MediaSource}. */ +public final class HlsMediaSource extends BaseMediaSource + implements HlsPlaylistTracker.PrimaryPlaylistListener { + + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.hls"); + } /** - * The default minimum number of times to retry loading data prior to failing. + * The types of metadata that can be extracted from HLS streams. + * + *

Allowed values: + * + *

    + *
  • {@link #METADATA_TYPE_ID3} + *
  • {@link #METADATA_TYPE_EMSG} + *
+ * + *

See {@link Factory#setMetadataType(int)}. */ - public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; + @Documented + @Retention(SOURCE) + @IntDef({METADATA_TYPE_ID3, METADATA_TYPE_EMSG}) + public @interface MetadataType {} + /** Type for ID3 metadata in HLS streams. */ + public static final int METADATA_TYPE_ID3 = 1; + /** Type for ESMG metadata in HLS streams. */ + public static final int METADATA_TYPE_EMSG = 3; + + /** Factory for {@link HlsMediaSource}s. */ + public static final class Factory implements MediaSourceFactory { + + private final HlsDataSourceFactory hlsDataSourceFactory; + + private HlsExtractorFactory extractorFactory; + private HlsPlaylistParserFactory playlistParserFactory; + @Nullable private List streamKeys; + private HlsPlaylistTracker.Factory playlistTrackerFactory; + private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private DrmSessionManager drmSessionManager; + private LoadErrorHandlingPolicy loadErrorHandlingPolicy; + private boolean allowChunklessPreparation; + @MetadataType private int metadataType; + private boolean useSessionKeys; + private boolean isCreateCalled; + @Nullable private Object tag; + + /** + * Creates a new factory for {@link HlsMediaSource}s. + * + * @param dataSourceFactory A data source factory that will be wrapped by a {@link + * DefaultHlsDataSourceFactory} to create {@link DataSource}s for manifests, segments and + * keys. + */ + public Factory(DataSource.Factory dataSourceFactory) { + this(new DefaultHlsDataSourceFactory(dataSourceFactory)); + } + + /** + * Creates a new factory for {@link HlsMediaSource}s. + * + * @param hlsDataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for + * manifests, segments and keys. + */ + public Factory(HlsDataSourceFactory hlsDataSourceFactory) { + this.hlsDataSourceFactory = Assertions.checkNotNull(hlsDataSourceFactory); + playlistParserFactory = new DefaultHlsPlaylistParserFactory(); + playlistTrackerFactory = DefaultHlsPlaylistTracker.FACTORY; + extractorFactory = HlsExtractorFactory.DEFAULT; + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); + compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); + metadataType = METADATA_TYPE_ID3; + } + + /** + * Sets a tag for the media source which will be published in the {@link + * org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline} of the source as {@link + * org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline.Window#tag}. + * + * @param tag A tag for the media source. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setTag(@Nullable Object tag) { + Assertions.checkState(!isCreateCalled); + this.tag = tag; + return this; + } + + /** + * Sets the factory for {@link Extractor}s for the segments. The default value is {@link + * HlsExtractorFactory#DEFAULT}. + * + * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the + * segments. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setExtractorFactory(HlsExtractorFactory extractorFactory) { + Assertions.checkState(!isCreateCalled); + this.extractorFactory = Assertions.checkNotNull(extractorFactory); + return this; + } + + /** + * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link + * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. + * + *

Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}. + * + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + Assertions.checkState(!isCreateCalled); + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + return this; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. The default value is + * {@link DefaultLoadErrorHandlingPolicy#DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * + *

Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with + * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int) + * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)} + * + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead. + */ + @Deprecated + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); + this.loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount); + return this; + } + + /** + * Sets the factory from which playlist parsers will be obtained. The default value is a {@link + * DefaultHlsPlaylistParserFactory}. + * + * @param playlistParserFactory An {@link HlsPlaylistParserFactory}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setPlaylistParserFactory(HlsPlaylistParserFactory playlistParserFactory) { + Assertions.checkState(!isCreateCalled); + this.playlistParserFactory = Assertions.checkNotNull(playlistParserFactory); + return this; + } + + /** + * Sets the {@link HlsPlaylistTracker} factory. The default value is {@link + * DefaultHlsPlaylistTracker#FACTORY}. + * + * @param playlistTrackerFactory A factory for {@link HlsPlaylistTracker} instances. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setPlaylistTrackerFactory(HlsPlaylistTracker.Factory playlistTrackerFactory) { + Assertions.checkState(!isCreateCalled); + this.playlistTrackerFactory = Assertions.checkNotNull(playlistTrackerFactory); + return this; + } + + /** + * Sets the factory to create composite {@link SequenceableLoader}s for when this media source + * loads data from multiple streams (video, audio etc...). The default is an instance of {@link + * DefaultCompositeSequenceableLoaderFactory}. + * + * @param compositeSequenceableLoaderFactory A factory to create composite {@link + * SequenceableLoader}s for when this media source loads data from multiple streams (video, + * audio etc...). + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setCompositeSequenceableLoaderFactory( + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { + Assertions.checkState(!isCreateCalled); + this.compositeSequenceableLoaderFactory = + Assertions.checkNotNull(compositeSequenceableLoaderFactory); + return this; + } + + /** + * Sets whether chunkless preparation is allowed. If true, preparation without chunk downloads + * will be enabled for streams that provide sufficient information in their master playlist. + * + * @param allowChunklessPreparation Whether chunkless preparation is allowed. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setAllowChunklessPreparation(boolean allowChunklessPreparation) { + Assertions.checkState(!isCreateCalled); + this.allowChunklessPreparation = allowChunklessPreparation; + return this; + } + + /** + * Sets the type of metadata to extract from the HLS source (defaults to {@link + * #METADATA_TYPE_ID3}). + * + *

HLS supports in-band ID3 in both TS and fMP4 streams, but in the fMP4 case the data is + * wrapped in an EMSG box [spec]. + * + *

If this is set to {@link #METADATA_TYPE_ID3} then raw ID3 metadata of will be extracted + * from TS sources. From fMP4 streams EMSGs containing metadata of this type (in the variant + * stream only) will be unwrapped to expose the inner data. All other in-band metadata will be + * dropped. + * + *

If this is set to {@link #METADATA_TYPE_EMSG} then all EMSG data from the fMP4 variant + * stream will be extracted. No metadata will be extracted from TS streams, since they don't + * support EMSG. + * + * @param metadataType The type of metadata to extract. + * @return This factory, for convenience. + */ + public Factory setMetadataType(@MetadataType int metadataType) { + Assertions.checkState(!isCreateCalled); + this.metadataType = metadataType; + return this; + } + + /** + * Sets whether to use #EXT-X-SESSION-KEY tags provided in the master playlist. If enabled, it's + * assumed that any single session key declared in the master playlist can be used to obtain all + * of the keys required for playback. For media where this is not true, this option should not + * be enabled. + * + * @param useSessionKeys Whether to use #EXT-X-SESSION-KEY tags. + * @return This factory, for convenience. + */ + public Factory setUseSessionKeys(boolean useSessionKeys) { + this.useSessionKeys = useSessionKeys; + return this; + } + + /** + * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, + * MediaSourceEventListener)} instead. + */ + @Deprecated + public HlsMediaSource createMediaSource( + Uri playlistUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + HlsMediaSource mediaSource = createMediaSource(playlistUri); + if (eventHandler != null && eventListener != null) { + mediaSource.addEventListener(eventHandler, eventListener); + } + return mediaSource; + } + + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + @Override + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = drmSessionManager; + return this; + } + + /** + * Returns a new {@link HlsMediaSource} using the current parameters. + * + * @return The new {@link HlsMediaSource}. + */ + @Override + public HlsMediaSource createMediaSource(Uri playlistUri) { + isCreateCalled = true; + if (streamKeys != null) { + playlistParserFactory = + new FilteringHlsPlaylistParserFactory(playlistParserFactory, streamKeys); + } + return new HlsMediaSource( + playlistUri, + hlsDataSourceFactory, + extractorFactory, + compositeSequenceableLoaderFactory, + drmSessionManager, + loadErrorHandlingPolicy, + playlistTrackerFactory.createTracker( + hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory), + allowChunklessPreparation, + metadataType, + useSessionKeys, + tag); + } + + @Override + public Factory setStreamKeys(List streamKeys) { + Assertions.checkState(!isCreateCalled); + this.streamKeys = streamKeys; + return this; + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_HLS}; + } + + } + + private final HlsExtractorFactory extractorFactory; private final Uri manifestUri; private final HlsDataSourceFactory dataSourceFactory; - private final int minLoadableRetryCount; - private final EventDispatcher eventDispatcher; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final DrmSessionManager drmSessionManager; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; + private final boolean allowChunklessPreparation; + private final @MetadataType int metadataType; + private final boolean useSessionKeys; + private final HlsPlaylistTracker playlistTracker; + @Nullable private final Object tag; - private HlsPlaylistTracker playlistTracker; - private Listener sourceListener; + @Nullable private TransferListener mediaTransferListener; - public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { - this(manifestUri, dataSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, - eventListener); - } - - public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, - int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { - this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), minLoadableRetryCount, - eventHandler, eventListener); - } - - public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, - int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + private HlsMediaSource( + Uri manifestUri, + HlsDataSourceFactory dataSourceFactory, + HlsExtractorFactory extractorFactory, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, + HlsPlaylistTracker playlistTracker, + boolean allowChunklessPreparation, + @MetadataType int metadataType, + boolean useSessionKeys, + @Nullable Object tag) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; - this.minLoadableRetryCount = minLoadableRetryCount; - eventDispatcher = new EventDispatcher(eventHandler, eventListener); + this.extractorFactory = extractorFactory; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + this.drmSessionManager = drmSessionManager; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + this.playlistTracker = playlistTracker; + this.allowChunklessPreparation = allowChunklessPreparation; + this.metadataType = metadataType; + this.useSessionKeys = useSessionKeys; + this.tag = tag; } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - Assertions.checkState(playlistTracker == null); - playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, - minLoadableRetryCount, this); - sourceListener = listener; - playlistTracker.start(); + @Nullable + public Object getTag() { + return tag; + } + + @Override + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + this.mediaTransferListener = mediaTransferListener; + drmSessionManager.prepare(); + EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); + playlistTracker.start(manifestUri, eventDispatcher, /* listener= */ this); } @Override public void maybeThrowSourceInfoRefreshError() throws IOException { - playlistTracker.maybeThrowPlaylistRefreshError(); + playlistTracker.maybeThrowPrimaryPlaylistRefreshError(); } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - Assertions.checkArgument(index == 0); - return new HlsMediaPeriod(playlistTracker, dataSourceFactory, minLoadableRetryCount, - eventDispatcher, allocator, positionUs); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + EventDispatcher eventDispatcher = createEventDispatcher(id); + return new HlsMediaPeriod( + extractorFactory, + playlistTracker, + dataSourceFactory, + mediaTransferListener, + drmSessionManager, + loadErrorHandlingPolicy, + eventDispatcher, + allocator, + compositeSequenceableLoaderFactory, + allowChunklessPreparation, + metadataType, + useSessionKeys); } @Override @@ -100,37 +450,79 @@ public final class HlsMediaSource implements MediaSource, } @Override - public void releaseSource() { - if (playlistTracker != null) { - playlistTracker.release(); - playlistTracker = null; - } - sourceListener = null; + protected void releaseSourceInternal() { + playlistTracker.stop(); + drmSessionManager.release(); } @Override public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) { SinglePeriodTimeline timeline; + long windowStartTimeMs = playlist.hasProgramDateTime ? C.usToMs(playlist.startTimeUs) + : C.TIME_UNSET; + // For playlist types EVENT and VOD we know segments are never removed, so the presentation + // started at the same time as the window. Otherwise, we don't know the presentation start time. + long presentationStartTimeMs = + playlist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT + || playlist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD + ? windowStartTimeMs + : C.TIME_UNSET; long windowDefaultStartPositionUs = playlist.startOffsetUs; + // masterPlaylist is non-null because the first playlist has been fetched by now. + HlsManifest manifest = + new HlsManifest(Assertions.checkNotNull(playlistTracker.getMasterPlaylist()), playlist); if (playlistTracker.isLive()) { - long periodDurationUs = playlist.hasEndTag ? (playlist.startTimeUs + playlist.durationUs) - : C.TIME_UNSET; + long offsetFromInitialStartTimeUs = + playlist.startTimeUs - playlistTracker.getInitialStartTimeUs(); + long periodDurationUs = + playlist.hasEndTag ? offsetFromInitialStartTimeUs + playlist.durationUs : C.TIME_UNSET; List segments = playlist.segments; if (windowDefaultStartPositionUs == C.TIME_UNSET) { - windowDefaultStartPositionUs = segments.isEmpty() ? 0 - : segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs; + windowDefaultStartPositionUs = 0; + if (!segments.isEmpty()) { + int defaultStartSegmentIndex = Math.max(0, segments.size() - 3); + // We attempt to set the default start position to be at least twice the target duration + // behind the live edge. + long minStartPositionUs = playlist.durationUs - playlist.targetDurationUs * 2; + while (defaultStartSegmentIndex > 0 + && segments.get(defaultStartSegmentIndex).relativeStartTimeUs > minStartPositionUs) { + defaultStartSegmentIndex--; + } + windowDefaultStartPositionUs = segments.get(defaultStartSegmentIndex).relativeStartTimeUs; + } } - timeline = new SinglePeriodTimeline(periodDurationUs, playlist.durationUs, - playlist.startTimeUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag); + timeline = + new SinglePeriodTimeline( + presentationStartTimeMs, + windowStartTimeMs, + periodDurationUs, + /* windowDurationUs= */ playlist.durationUs, + /* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs, + windowDefaultStartPositionUs, + /* isSeekable= */ true, + /* isDynamic= */ !playlist.hasEndTag, + /* isLive= */ true, + manifest, + tag); } else /* not live */ { if (windowDefaultStartPositionUs == C.TIME_UNSET) { windowDefaultStartPositionUs = 0; } - timeline = new SinglePeriodTimeline(playlist.startTimeUs + playlist.durationUs, - playlist.durationUs, playlist.startTimeUs, windowDefaultStartPositionUs, true, false); + timeline = + new SinglePeriodTimeline( + presentationStartTimeMs, + windowStartTimeMs, + /* periodDurationUs= */ playlist.durationUs, + /* windowDurationUs= */ playlist.durationUs, + /* windowPositionInPeriodUs= */ 0, + windowDefaultStartPositionUs, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + manifest, + tag); } - sourceListener.onSourceInfoRefreshed(timeline, - new HlsManifest(playlistTracker.getMasterPlaylist(), playlist)); + refreshSourceInfo(timeline); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index 807027efffe0..5f44810af56a 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -15,43 +15,83 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleStream; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.IOException; /** - * {@link SampleStream} for a particular track group in HLS. + * {@link SampleStream} for a particular sample queue in HLS. */ /* package */ final class HlsSampleStream implements SampleStream { - public final int group; - + private final int trackGroupIndex; private final HlsSampleStreamWrapper sampleStreamWrapper; + private int sampleQueueIndex; - public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int group) { + public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int trackGroupIndex) { this.sampleStreamWrapper = sampleStreamWrapper; - this.group = group; + this.trackGroupIndex = trackGroupIndex; + sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING; } + public void bindSampleQueue() { + Assertions.checkArgument(sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING); + sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex); + } + + public void unbindSampleQueue() { + if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) { + sampleStreamWrapper.unbindSampleQueue(trackGroupIndex); + sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING; + } + } + + // SampleStream implementation. + @Override public boolean isReady() { - return sampleStreamWrapper.isReady(group); + return sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL + || (hasValidSampleQueueIndex() && sampleStreamWrapper.isReady(sampleQueueIndex)); } @Override public void maybeThrowError() throws IOException { - sampleStreamWrapper.maybeThrowError(); + if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) { + throw new SampleQueueMappingException( + sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType); + } else if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) { + sampleStreamWrapper.maybeThrowError(); + } else if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL) { + sampleStreamWrapper.maybeThrowError(sampleQueueIndex); + } } @Override public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { - return sampleStreamWrapper.readData(group, formatHolder, buffer, requireFormat); + if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL) { + buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } + return hasValidSampleQueueIndex() + ? sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat) + : C.RESULT_NOTHING_READ; } @Override - public void skipData(long positionUs) { - sampleStreamWrapper.skipData(group, positionUs); + public int skipData(long positionUs) { + return hasValidSampleQueueIndex() + ? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs) + : 0; } + // Internal methods. + + private boolean hasValidSampleQueueIndex() { + return sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING + && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL + && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL; + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 2e410cf0bbdc..833abbc29ffc 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -15,39 +15,67 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls; +import android.net.Uri; import android.os.Handler; -import android.text.TextUtils; -import android.util.SparseArray; +import android.util.SparseIntArray; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultTrackOutput; -import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DefaultTrackOutput.UpstreamFormatChangedListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DummyTrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg.EventMessage; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.PrivFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleStream; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SequenceableLoader; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.Chunk; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.EOFException; import java.io.IOException; -import java.util.LinkedList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides * {@link SampleStream}s from which the loaded media can be consumed. */ /* package */ final class HlsSampleStreamWrapper implements Loader.Callback, - SequenceableLoader, ExtractorOutput, UpstreamFormatChangedListener { + Loader.ReleaseCallback, SequenceableLoader, ExtractorOutput, UpstreamFormatChangedListener { /** * A callback to be notified of events. @@ -56,6 +84,9 @@ import java.util.LinkedList; /** * Called when the wrapper has been prepared. + * + *

Note: This method will be called on a later handler loop than the one on which either + * {@link #prepareWithMasterPlaylistInfo} or {@link #continuePreparing} are invoked. */ void onPrepared(); @@ -63,79 +94,133 @@ import java.util.LinkedList; * Called to schedule a {@link #continueLoading(long)} call when the playlist referred by the * given url changes. */ - void onPlaylistRefreshRequired(HlsMasterPlaylist.HlsUrl playlistUrl); - + void onPlaylistRefreshRequired(Uri playlistUrl); } - private static final int PRIMARY_TYPE_NONE = 0; - private static final int PRIMARY_TYPE_TEXT = 1; - private static final int PRIMARY_TYPE_AUDIO = 2; - private static final int PRIMARY_TYPE_VIDEO = 3; + private static final String TAG = "HlsSampleStreamWrapper"; + + public static final int SAMPLE_QUEUE_INDEX_PENDING = -1; + public static final int SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL = -2; + public static final int SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL = -3; + + private static final Set MAPPABLE_TYPES = + Collections.unmodifiableSet( + new HashSet<>( + Arrays.asList(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_METADATA))); private final int trackType; private final Callback callback; private final HlsChunkSource chunkSource; private final Allocator allocator; - private final Format muxedAudioFormat; - private final int minLoadableRetryCount; + @Nullable private final Format muxedAudioFormat; + private final DrmSessionManager drmSessionManager; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final Loader loader; private final EventDispatcher eventDispatcher; + private final @HlsMediaSource.MetadataType int metadataType; private final HlsChunkSource.HlsChunkHolder nextChunkHolder; - private final SparseArray sampleQueues; - private final LinkedList mediaChunks; + private final ArrayList mediaChunks; + private final List readOnlyMediaChunks; + // Using runnables rather than in-line method references to avoid repeated allocations. private final Runnable maybeFinishPrepareRunnable; + private final Runnable onTracksEndedRunnable; private final Handler handler; + private final ArrayList hlsSampleStreams; + private final Map overridingDrmInitData; + private FormatAdjustingSampleQueue[] sampleQueues; + private int[] sampleQueueTrackIds; + private Set sampleQueueMappingDoneByType; + private SparseIntArray sampleQueueIndicesByType; + @MonotonicNonNull private TrackOutput emsgUnwrappingTrackOutput; + private int primarySampleQueueType; + private int primarySampleQueueIndex; private boolean sampleQueuesBuilt; private boolean prepared; - private int enabledTrackCount; - private Format downstreamTrackFormat; - private int upstreamChunkUid; + private int enabledTrackGroupCount; + @MonotonicNonNull private Format upstreamTrackFormat; + @Nullable private Format downstreamTrackFormat; private boolean released; - // Tracks are complicated in HLS. See documentation of buildTracks for details. + // Tracks are complicated in HLS. See documentation of buildTracksFromSampleStreams for details. // Indexed by track (as exposed by this source). - private TrackGroupArray trackGroups; + @MonotonicNonNull private TrackGroupArray trackGroups; + @MonotonicNonNull private Set optionalTrackGroups; + // Indexed by track group. + private int @MonotonicNonNull [] trackGroupToSampleQueueIndex; private int primaryTrackGroupIndex; - // Indexed by group. - private boolean[] groupEnabledStates; + private boolean haveAudioVideoSampleQueues; + private boolean[] sampleQueuesEnabledStates; + private boolean[] sampleQueueIsAudioVideoFlags; private long lastSeekPositionUs; private long pendingResetPositionUs; - + private boolean pendingResetUpstreamFormats; + private boolean seenFirstTrackSelection; private boolean loadingFinished; + // Accessed only by the loading thread. + private boolean tracksEnded; + private long sampleOffsetUs; + @Nullable private DrmInitData drmInitData; + private int chunkUid; + /** * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants. * @param callback A callback for the wrapper. * @param chunkSource A {@link HlsChunkSource} from which chunks to load are obtained. + * @param overridingDrmInitData Overriding {@link DrmInitData}, keyed by protection scheme type + * (i.e. {@link DrmInitData#schemeType}). If the stream has {@link DrmInitData} and uses a + * protection scheme type for which overriding {@link DrmInitData} is provided, then the + * stream's {@link DrmInitData} will be overridden. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param positionUs The position from which to start loading media. * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist. - * @param minLoadableRetryCount The minimum number of times that the source should retry a load - * before propagating an error. + * @param drmSessionManager The {@link DrmSessionManager} to acquire {@link DrmSession + * DrmSessions} with. + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. * @param eventDispatcher A dispatcher to notify of events. */ - public HlsSampleStreamWrapper(int trackType, Callback callback, HlsChunkSource chunkSource, - Allocator allocator, long positionUs, Format muxedAudioFormat, int minLoadableRetryCount, - EventDispatcher eventDispatcher) { + public HlsSampleStreamWrapper( + int trackType, + Callback callback, + HlsChunkSource chunkSource, + Map overridingDrmInitData, + Allocator allocator, + long positionUs, + @Nullable Format muxedAudioFormat, + DrmSessionManager drmSessionManager, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, + EventDispatcher eventDispatcher, + @HlsMediaSource.MetadataType int metadataType) { this.trackType = trackType; this.callback = callback; this.chunkSource = chunkSource; + this.overridingDrmInitData = overridingDrmInitData; this.allocator = allocator; this.muxedAudioFormat = muxedAudioFormat; - this.minLoadableRetryCount = minLoadableRetryCount; + this.drmSessionManager = drmSessionManager; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; + this.metadataType = metadataType; loader = new Loader("Loader:HlsSampleStreamWrapper"); nextChunkHolder = new HlsChunkSource.HlsChunkHolder(); - sampleQueues = new SparseArray<>(); - mediaChunks = new LinkedList<>(); - maybeFinishPrepareRunnable = new Runnable() { - @Override - public void run() { - maybeFinishPrepare(); - } - }; + sampleQueueTrackIds = new int[0]; + sampleQueueMappingDoneByType = new HashSet<>(MAPPABLE_TYPES.size()); + sampleQueueIndicesByType = new SparseIntArray(MAPPABLE_TYPES.size()); + sampleQueues = new FormatAdjustingSampleQueue[0]; + sampleQueueIsAudioVideoFlags = new boolean[0]; + sampleQueuesEnabledStates = new boolean[0]; + mediaChunks = new ArrayList<>(); + readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); + hlsSampleStreams = new ArrayList<>(); + // Suppressions are needed because `this` is not initialized here. + @SuppressWarnings("nullness:methodref.receiver.bound.invalid") + Runnable maybeFinishPrepareRunnable = this::maybeFinishPrepare; + this.maybeFinishPrepareRunnable = maybeFinishPrepareRunnable; + @SuppressWarnings("nullness:methodref.receiver.bound.invalid") + Runnable onTracksEndedRunnable = this::onTracksEnded; + this.onTracksEndedRunnable = onTracksEndedRunnable; handler = new Handler(); lastSeekPositionUs = positionUs; pendingResetPositionUs = positionUs; @@ -148,97 +233,364 @@ import java.util.LinkedList; } /** - * Prepares a sample stream wrapper for which the master playlist provides enough information to - * prepare. + * Prepares the sample stream wrapper with master playlist information. + * + * @param trackGroups The {@link TrackGroup TrackGroups} to expose through {@link + * #getTrackGroups()}. + * @param primaryTrackGroupIndex The index of the adaptive track group. + * @param optionalTrackGroupsIndices The indices of any {@code trackGroups} that should not + * trigger a failure if not found in the media playlist's segments. */ - public void prepareSingleTrack(Format format) { - track(0, C.TRACK_TYPE_UNKNOWN).format(format); - sampleQueuesBuilt = true; - maybeFinishPrepare(); + public void prepareWithMasterPlaylistInfo( + TrackGroup[] trackGroups, int primaryTrackGroupIndex, int... optionalTrackGroupsIndices) { + this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups); + optionalTrackGroups = new HashSet<>(); + for (int optionalTrackGroupIndex : optionalTrackGroupsIndices) { + optionalTrackGroups.add(this.trackGroups.get(optionalTrackGroupIndex)); + } + this.primaryTrackGroupIndex = primaryTrackGroupIndex; + handler.post(callback::onPrepared); + setIsPrepared(); } public void maybeThrowPrepareError() throws IOException { maybeThrowError(); + if (loadingFinished && !prepared) { + throw new ParserException("Loading finished before preparation is complete."); + } } public TrackGroupArray getTrackGroups() { + assertIsPrepared(); return trackGroups; } - public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, boolean isFirstTrackSelection) { - Assertions.checkState(prepared); - // Disable old tracks. + public int getPrimaryTrackGroupIndex() { + return primaryTrackGroupIndex; + } + + public int bindSampleQueueToSampleStream(int trackGroupIndex) { + assertIsPrepared(); + Assertions.checkNotNull(trackGroupToSampleQueueIndex); + + int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; + if (sampleQueueIndex == C.INDEX_UNSET) { + return optionalTrackGroups.contains(trackGroups.get(trackGroupIndex)) + ? SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL + : SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL; + } + if (sampleQueuesEnabledStates[sampleQueueIndex]) { + // This sample queue is already bound to a different sample stream. + return SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL; + } + sampleQueuesEnabledStates[sampleQueueIndex] = true; + return sampleQueueIndex; + } + + public void unbindSampleQueue(int trackGroupIndex) { + assertIsPrepared(); + Assertions.checkNotNull(trackGroupToSampleQueueIndex); + int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; + Assertions.checkState(sampleQueuesEnabledStates[sampleQueueIndex]); + sampleQueuesEnabledStates[sampleQueueIndex] = false; + } + + /** + * Called by the parent {@link HlsMediaPeriod} when a track selection occurs. + * + * @param selections The renderer track selections. + * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained + * for each selection. A {@code true} value indicates that the selection is unchanged, and + * that the caller does not require that the sample stream be recreated. + * @param streams The existing sample streams, which will be updated to reflect the provided + * selections. + * @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that + * have been retained but with the requirement that the consuming renderer be reset. + * @param positionUs The current playback position in microseconds. + * @param forceReset If true then a reset is forced (i.e. a seek will be performed with in-buffer + * seeking disabled). + * @return Whether this wrapper requires the parent {@link HlsMediaPeriod} to perform a seek as + * part of the track selection. + */ + public boolean selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs, + boolean forceReset) { + assertIsPrepared(); + int oldEnabledTrackGroupCount = enabledTrackGroupCount; + // Deselect old tracks. for (int i = 0; i < selections.length; i++) { - if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { - int group = ((HlsSampleStream) streams[i]).group; - setTrackGroupEnabledState(group, false); - sampleQueues.valueAt(group).disable(); + HlsSampleStream stream = (HlsSampleStream) streams[i]; + if (stream != null && (selections[i] == null || !mayRetainStreamFlags[i])) { + enabledTrackGroupCount--; + stream.unbindSampleQueue(); streams[i] = null; } } - // Enable new tracks. - TrackSelection primaryTrackSelection = null; - boolean selectedNewTracks = false; + // We'll always need to seek if we're being forced to reset, or if this is a first selection to + // a position other than the one we started preparing with, or if we're making a selection + // having previously disabled all tracks. + boolean seekRequired = + forceReset + || (seenFirstTrackSelection + ? oldEnabledTrackGroupCount == 0 + : positionUs != lastSeekPositionUs); + // Get the old (i.e. current before the loop below executes) primary track selection. The new + // primary selection will equal the old one unless it's changed in the loop. + TrackSelection oldPrimaryTrackSelection = chunkSource.getTrackSelection(); + TrackSelection primaryTrackSelection = oldPrimaryTrackSelection; + // Select new tracks. for (int i = 0; i < selections.length; i++) { - if (streams[i] == null && selections[i] != null) { - TrackSelection selection = selections[i]; - int group = trackGroups.indexOf(selection.getTrackGroup()); - setTrackGroupEnabledState(group, true); - if (group == primaryTrackGroupIndex) { - primaryTrackSelection = selection; - chunkSource.selectTracks(selection); - } - streams[i] = new HlsSampleStream(this, group); + TrackSelection selection = selections[i]; + if (selection == null) { + continue; + } + int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); + if (trackGroupIndex == primaryTrackGroupIndex) { + primaryTrackSelection = selection; + chunkSource.setTrackSelection(selection); + } + if (streams[i] == null) { + enabledTrackGroupCount++; + streams[i] = new HlsSampleStream(this, trackGroupIndex); streamResetFlags[i] = true; - selectedNewTracks = true; - } - } - if (isFirstTrackSelection) { - // At the time of the first track selection all queues will be enabled, so we need to disable - // any that are no longer required. - int sampleQueueCount = sampleQueues.size(); - for (int i = 0; i < sampleQueueCount; i++) { - if (!groupEnabledStates[i]) { - sampleQueues.valueAt(i).disable(); - } - } - if (primaryTrackSelection != null && !mediaChunks.isEmpty()) { - primaryTrackSelection.updateSelectedTrack(0); - int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat); - if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) { - // The loaded preparation chunk does match the selection. We discard it. - seekTo(lastSeekPositionUs); + if (trackGroupToSampleQueueIndex != null) { + ((HlsSampleStream) streams[i]).bindSampleQueue(); + // If there's still a chance of avoiding a seek, try and seek within the sample queue. + if (!seekRequired) { + SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]]; + // A seek can be avoided if we're able to seek to the current playback position in + // the sample queue, or if we haven't read anything from the queue since the previous + // seek (this case is common for sparse tracks such as metadata tracks). In all other + // cases a seek is required. + seekRequired = + !sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ true) + && sampleQueue.getReadIndex() != 0; + } } } } - // Cancel requests if necessary. - if (enabledTrackCount == 0) { + + if (enabledTrackGroupCount == 0) { chunkSource.reset(); downstreamTrackFormat = null; + pendingResetUpstreamFormats = true; mediaChunks.clear(); if (loader.isLoading()) { + if (sampleQueuesBuilt) { + // Discard as much as we can synchronously. + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.discardToEnd(); + } + } loader.cancelLoading(); + } else { + resetSampleQueues(); + } + } else { + if (!mediaChunks.isEmpty() + && !Util.areEqual(primaryTrackSelection, oldPrimaryTrackSelection)) { + // The primary track selection has changed and we have buffered media. The buffered media + // may need to be discarded. + boolean primarySampleQueueDirty = false; + if (!seenFirstTrackSelection) { + long bufferedDurationUs = positionUs < 0 ? -positionUs : 0; + HlsMediaChunk lastMediaChunk = getLastMediaChunk(); + MediaChunkIterator[] mediaChunkIterators = + chunkSource.createMediaChunkIterators(lastMediaChunk, positionUs); + primaryTrackSelection.updateSelectedTrack( + positionUs, + bufferedDurationUs, + C.TIME_UNSET, + readOnlyMediaChunks, + mediaChunkIterators); + int chunkIndex = chunkSource.getTrackGroup().indexOf(lastMediaChunk.trackFormat); + if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) { + // This is the first selection and the chunk loaded during preparation does not match + // the initially selected format. + primarySampleQueueDirty = true; + } + } else { + // The primary sample queue contains media buffered for the old primary track selection. + primarySampleQueueDirty = true; + } + if (primarySampleQueueDirty) { + forceReset = true; + seekRequired = true; + pendingResetUpstreamFormats = true; + } + } + if (seekRequired) { + seekToUs(positionUs, forceReset); + // We'll need to reset renderers consuming from all streams due to the seek. + for (int i = 0; i < streams.length; i++) { + if (streams[i] != null) { + streamResetFlags[i] = true; + } + } } } - return selectedNewTracks; + + updateSampleStreams(streams); + seenFirstTrackSelection = true; + return seekRequired; } - public void seekTo(long positionUs) { + public void discardBuffer(long positionUs, boolean toKeyframe) { + if (!sampleQueuesBuilt || isPendingReset()) { + return; + } + int sampleQueueCount = sampleQueues.length; + for (int i = 0; i < sampleQueueCount; i++) { + sampleQueues[i].discardTo(positionUs, toKeyframe, sampleQueuesEnabledStates[i]); + } + } + + /** + * Attempts to seek to the specified position in microseconds. + * + * @param positionUs The seek position in microseconds. + * @param forceReset If true then a reset is forced (i.e. in-buffer seeking is disabled). + * @return Whether the wrapper was reset, meaning the wrapped sample queues were reset. If false, + * an in-buffer seek was performed. + */ + public boolean seekToUs(long positionUs, boolean forceReset) { lastSeekPositionUs = positionUs; + if (isPendingReset()) { + // A reset is already pending. We only need to update its position. + pendingResetPositionUs = positionUs; + return true; + } + + // If we're not forced to reset, try and seek within the buffer. + if (sampleQueuesBuilt && !forceReset && seekInsideBufferUs(positionUs)) { + return false; + } + + // We can't seek inside the buffer, and so need to reset. pendingResetPositionUs = positionUs; loadingFinished = false; mediaChunks.clear(); if (loader.isLoading()) { loader.cancelLoading(); } else { - int sampleQueueCount = sampleQueues.size(); - for (int i = 0; i < sampleQueueCount; i++) { - sampleQueues.valueAt(i).reset(groupEnabledStates[i]); + loader.clearFatalError(); + resetSampleQueues(); + } + return true; + } + + public void release() { + if (prepared) { + // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise + // sampleQueues may still be being modified by the loading thread. + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.preRelease(); } } + loader.release(this); + handler.removeCallbacksAndMessages(null); + released = true; + hlsSampleStreams.clear(); + } + + @Override + public void onLoaderReleased() { + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.release(); + } } + public void setIsTimestampMaster(boolean isTimestampMaster) { + chunkSource.setIsTimestampMaster(isTimestampMaster); + } + + public boolean onPlaylistError(Uri playlistUrl, long blacklistDurationMs) { + return chunkSource.onPlaylistError(playlistUrl, blacklistDurationMs); + } + + // SampleStream implementation. + + public boolean isReady(int sampleQueueIndex) { + return !isPendingReset() && sampleQueues[sampleQueueIndex].isReady(loadingFinished); + } + + public void maybeThrowError(int sampleQueueIndex) throws IOException { + maybeThrowError(); + sampleQueues[sampleQueueIndex].maybeThrowError(); + } + + public void maybeThrowError() throws IOException { + loader.maybeThrowError(); + chunkSource.maybeThrowError(); + } + + public int readData(int sampleQueueIndex, FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean requireFormat) { + if (isPendingReset()) { + return C.RESULT_NOTHING_READ; + } + + // TODO: Split into discard (in discardBuffer) and format change (here and in skipData) steps. + if (!mediaChunks.isEmpty()) { + int discardToMediaChunkIndex = 0; + while (discardToMediaChunkIndex < mediaChunks.size() - 1 + && finishedReadingChunk(mediaChunks.get(discardToMediaChunkIndex))) { + discardToMediaChunkIndex++; + } + Util.removeRange(mediaChunks, 0, discardToMediaChunkIndex); + HlsMediaChunk currentChunk = mediaChunks.get(0); + Format trackFormat = currentChunk.trackFormat; + if (!trackFormat.equals(downstreamTrackFormat)) { + eventDispatcher.downstreamFormatChanged(trackType, trackFormat, + currentChunk.trackSelectionReason, currentChunk.trackSelectionData, + currentChunk.startTimeUs); + } + downstreamTrackFormat = trackFormat; + } + + int result = + sampleQueues[sampleQueueIndex].read( + formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs); + if (result == C.RESULT_FORMAT_READ) { + Format format = Assertions.checkNotNull(formatHolder.format); + if (sampleQueueIndex == primarySampleQueueIndex) { + // Fill in primary sample format with information from the track format. + int chunkUid = sampleQueues[sampleQueueIndex].peekSourceId(); + int chunkIndex = 0; + while (chunkIndex < mediaChunks.size() && mediaChunks.get(chunkIndex).uid != chunkUid) { + chunkIndex++; + } + Format trackFormat = + chunkIndex < mediaChunks.size() + ? mediaChunks.get(chunkIndex).trackFormat + : Assertions.checkNotNull(upstreamTrackFormat); + format = format.copyWithManifestFormatInfo(trackFormat); + } + formatHolder.format = format; + } + return result; + } + + public int skipData(int sampleQueueIndex, long positionUs) { + if (isPendingReset()) { + return 0; + } + + SampleQueue sampleQueue = sampleQueues[sampleQueueIndex]; + if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { + return sampleQueue.advanceToEnd(); + } else { + return sampleQueue.advanceTo(positionUs); + } + } + + // SequenceableLoader implementation + + @Override public long getBufferedPositionUs() { if (loadingFinished) { return C.TIME_END_OF_SOURCE; @@ -246,115 +598,70 @@ import java.util.LinkedList; return pendingResetPositionUs; } else { long bufferedPositionUs = lastSeekPositionUs; - HlsMediaChunk lastMediaChunk = mediaChunks.getLast(); + HlsMediaChunk lastMediaChunk = getLastMediaChunk(); HlsMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null; if (lastCompletedMediaChunk != null) { bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs); } - int sampleQueueCount = sampleQueues.size(); - for (int i = 0; i < sampleQueueCount; i++) { - bufferedPositionUs = Math.max(bufferedPositionUs, - sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); + if (sampleQueuesBuilt) { + for (SampleQueue sampleQueue : sampleQueues) { + bufferedPositionUs = + Math.max(bufferedPositionUs, sampleQueue.getLargestQueuedTimestampUs()); + } } return bufferedPositionUs; } } - public void release() { - int sampleQueueCount = sampleQueues.size(); - for (int i = 0; i < sampleQueueCount; i++) { - sampleQueues.valueAt(i).disable(); - } - loader.release(); - handler.removeCallbacksAndMessages(null); - released = true; - } - - public void setIsTimestampMaster(boolean isTimestampMaster) { - chunkSource.setIsTimestampMaster(isTimestampMaster); - } - - public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { - chunkSource.onPlaylistBlacklisted(url, blacklistMs); - } - - // SampleStream implementation. - - /* package */ boolean isReady(int group) { - return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(group).isEmpty()); - } - - /* package */ void maybeThrowError() throws IOException { - loader.maybeThrowError(); - chunkSource.maybeThrowError(); - } - - /* package */ int readData(int group, FormatHolder formatHolder, DecoderInputBuffer buffer, - boolean requireFormat) { + @Override + public long getNextLoadPositionUs() { if (isPendingReset()) { - return C.RESULT_NOTHING_READ; - } - - while (mediaChunks.size() > 1 && finishedReadingChunk(mediaChunks.getFirst())) { - mediaChunks.removeFirst(); - } - HlsMediaChunk currentChunk = mediaChunks.getFirst(); - Format trackFormat = currentChunk.trackFormat; - if (!trackFormat.equals(downstreamTrackFormat)) { - eventDispatcher.downstreamFormatChanged(trackType, trackFormat, - currentChunk.trackSelectionReason, currentChunk.trackSelectionData, - currentChunk.startTimeUs); - } - downstreamTrackFormat = trackFormat; - - return sampleQueues.valueAt(group).readData(formatHolder, buffer, requireFormat, - loadingFinished, lastSeekPositionUs); - } - - /* package */ void skipData(int group, long positionUs) { - DefaultTrackOutput sampleQueue = sampleQueues.valueAt(group); - if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - sampleQueue.skipAll(); + return pendingResetPositionUs; } else { - sampleQueue.skipToKeyframeBefore(positionUs, true); + return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs; } } - private boolean finishedReadingChunk(HlsMediaChunk chunk) { - int chunkUid = chunk.uid; - for (int i = 0; i < sampleQueues.size(); i++) { - if (groupEnabledStates[i] && sampleQueues.valueAt(i).peekSourceId() == chunkUid) { - return false; - } - } - return true; - } - - // SequenceableLoader implementation - @Override public boolean continueLoading(long positionUs) { - if (loadingFinished || loader.isLoading()) { + if (loadingFinished || loader.isLoading() || loader.hasFatalError()) { return false; } - chunkSource.getNextChunk(mediaChunks.isEmpty() ? null : mediaChunks.getLast(), - pendingResetPositionUs != C.TIME_UNSET ? pendingResetPositionUs : positionUs, + List chunkQueue; + long loadPositionUs; + if (isPendingReset()) { + chunkQueue = Collections.emptyList(); + loadPositionUs = pendingResetPositionUs; + } else { + chunkQueue = readOnlyMediaChunks; + HlsMediaChunk lastMediaChunk = getLastMediaChunk(); + loadPositionUs = + lastMediaChunk.isLoadCompleted() + ? lastMediaChunk.endTimeUs + : Math.max(lastSeekPositionUs, lastMediaChunk.startTimeUs); + } + chunkSource.getNextChunk( + positionUs, + loadPositionUs, + chunkQueue, + /* allowEndOfStream= */ prepared || !chunkQueue.isEmpty(), nextChunkHolder); boolean endOfStream = nextChunkHolder.endOfStream; Chunk loadable = nextChunkHolder.chunk; - HlsMasterPlaylist.HlsUrl playlistToLoad = nextChunkHolder.playlist; + Uri playlistUrlToLoad = nextChunkHolder.playlistUrl; nextChunkHolder.clear(); if (endOfStream) { + pendingResetPositionUs = C.TIME_UNSET; loadingFinished = true; return true; } if (loadable == null) { - if (playlistToLoad != null) { - callback.onPlaylistRefreshRequired(playlistToLoad); + if (playlistUrlToLoad != null) { + callback.onPlaylistRefreshRequired(playlistUrlToLoad); } return false; } @@ -364,21 +671,32 @@ import java.util.LinkedList; HlsMediaChunk mediaChunk = (HlsMediaChunk) loadable; mediaChunk.init(this); mediaChunks.add(mediaChunk); + upstreamTrackFormat = mediaChunk.trackFormat; } - long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount); - eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs); + long elapsedRealtimeMs = + loader.startLoading( + loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)); + eventDispatcher.loadStarted( + loadable.dataSpec, + loadable.type, + trackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs); return true; } @Override - public long getNextLoadPositionUs() { - if (isPendingReset()) { - return pendingResetPositionUs; - } else { - return loadingFinished ? C.TIME_END_OF_SOURCE : mediaChunks.getLast().endTimeUs; - } + public boolean isLoading() { + return loader.isLoading(); + } + + @Override + public void reevaluateBuffer(long positionUs) { + // Do nothing. } // Loader.Callback implementation. @@ -386,9 +704,20 @@ import java.util.LinkedList; @Override public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) { chunkSource.onChunkLoadCompleted(loadable); - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); + eventDispatcher.loadCompleted( + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + loadable.type, + trackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); if (!prepared) { continueLoading(lastSeekPositionUs); } else { @@ -399,49 +728,91 @@ import java.util.LinkedList; @Override public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); + eventDispatcher.loadCanceled( + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + loadable.type, + trackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); if (!released) { - int sampleQueueCount = sampleQueues.size(); - for (int i = 0; i < sampleQueueCount; i++) { - sampleQueues.valueAt(i).reset(groupEnabledStates[i]); + resetSampleQueues(); + if (enabledTrackGroupCount > 0) { + callback.onContinueLoadingRequested(this); } - callback.onContinueLoadingRequested(this); } } @Override - public int onLoadError(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, - IOException error) { + public LoadErrorAction onLoadError( + Chunk loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error, + int errorCount) { long bytesLoaded = loadable.bytesLoaded(); boolean isMediaChunk = isMediaChunk(loadable); - boolean cancelable = !isMediaChunk || bytesLoaded == 0; - boolean canceled = false; - if (chunkSource.onChunkLoadError(loadable, cancelable, error)) { - if (isMediaChunk) { - HlsMediaChunk removed = mediaChunks.removeLast(); + boolean blacklistSucceeded = false; + LoadErrorAction loadErrorAction; + + long blacklistDurationMs = + loadErrorHandlingPolicy.getBlacklistDurationMsFor( + loadable.type, loadDurationMs, error, errorCount); + if (blacklistDurationMs != C.TIME_UNSET) { + blacklistSucceeded = chunkSource.maybeBlacklistTrack(loadable, blacklistDurationMs); + } + + if (blacklistSucceeded) { + if (isMediaChunk && bytesLoaded == 0) { + HlsMediaChunk removed = mediaChunks.remove(mediaChunks.size() - 1); Assertions.checkState(removed == loadable); if (mediaChunks.isEmpty()) { pendingResetPositionUs = lastSeekPositionUs; } } - canceled = true; + loadErrorAction = Loader.DONT_RETRY; + } else /* did not blacklist */ { + long retryDelayMs = + loadErrorHandlingPolicy.getRetryDelayMsFor( + loadable.type, loadDurationMs, error, errorCount); + loadErrorAction = + retryDelayMs != C.TIME_UNSET + ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs) + : Loader.DONT_RETRY_FATAL; } - eventDispatcher.loadError(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat, - loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded(), error, - canceled); - if (canceled) { + + eventDispatcher.loadError( + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + loadable.type, + trackType, + loadable.trackFormat, + loadable.trackSelectionReason, + loadable.trackSelectionData, + loadable.startTimeUs, + loadable.endTimeUs, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded, + error, + /* wasCanceled= */ !loadErrorAction.isRetry()); + + if (blacklistSucceeded) { if (!prepared) { continueLoading(lastSeekPositionUs); } else { callback.onContinueLoadingRequested(this); } - return Loader.DONT_RETRY; - } else { - return Loader.RETRY; } + return loadErrorAction; } // Called by the consuming thread, but only when there is no loading thread. @@ -454,13 +825,13 @@ import java.util.LinkedList; * samples already queued to the wrapper. */ public void init(int chunkUid, boolean shouldSpliceIn) { - upstreamChunkUid = chunkUid; - for (int i = 0; i < sampleQueues.size(); i++) { - sampleQueues.valueAt(i).sourceId(chunkUid); + this.chunkUid = chunkUid; + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.sourceId(chunkUid); } if (shouldSpliceIn) { - for (int i = 0; i < sampleQueues.size(); i++) { - sampleQueues.valueAt(i).splice(); + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.splice(); } } } @@ -468,21 +839,101 @@ import java.util.LinkedList; // ExtractorOutput implementation. Called by the loading thread. @Override - public DefaultTrackOutput track(int id, int type) { - if (sampleQueues.indexOfKey(id) >= 0) { - return sampleQueues.get(id); + public TrackOutput track(int id, int type) { + @Nullable TrackOutput trackOutput = null; + if (MAPPABLE_TYPES.contains(type)) { + // Track types in MAPPABLE_TYPES are handled manually to ignore IDs. + trackOutput = getMappedTrackOutput(id, type); + } else /* non-mappable type track */ { + for (int i = 0; i < sampleQueues.length; i++) { + if (sampleQueueTrackIds[i] == id) { + trackOutput = sampleQueues[i]; + break; + } + } } - DefaultTrackOutput trackOutput = new DefaultTrackOutput(allocator); + + if (trackOutput == null) { + if (tracksEnded) { + return createDummyTrackOutput(id, type); + } else { + // The relevant SampleQueue hasn't been constructed yet - so construct it. + trackOutput = createSampleQueue(id, type); + } + } + + if (type == C.TRACK_TYPE_METADATA) { + if (emsgUnwrappingTrackOutput == null) { + emsgUnwrappingTrackOutput = new EmsgUnwrappingTrackOutput(trackOutput, metadataType); + } + return emsgUnwrappingTrackOutput; + } + return trackOutput; + } + + /** + * Returns the {@link TrackOutput} for the provided {@code type} and {@code id}, or null if none + * has been created yet. + * + *

If a {@link SampleQueue} for {@code type} has been created and is mapped, but it has a + * different ID, then return a {@link DummyTrackOutput} that does nothing. + * + *

If a {@link SampleQueue} for {@code type} has been created but is not mapped, then map it to + * this {@code id} and return it. This situation can happen after a call to {@link + * #onNewExtractor}. + * + * @param id The ID of the track. + * @param type The type of the track, must be one of {@link #MAPPABLE_TYPES}. + * @return The the mapped {@link TrackOutput}, or null if it's not been created yet. + */ + @Nullable + private TrackOutput getMappedTrackOutput(int id, int type) { + Assertions.checkArgument(MAPPABLE_TYPES.contains(type)); + int sampleQueueIndex = sampleQueueIndicesByType.get(type, C.INDEX_UNSET); + if (sampleQueueIndex == C.INDEX_UNSET) { + return null; + } + + if (sampleQueueMappingDoneByType.add(type)) { + sampleQueueTrackIds[sampleQueueIndex] = id; + } + return sampleQueueTrackIds[sampleQueueIndex] == id + ? sampleQueues[sampleQueueIndex] + : createDummyTrackOutput(id, type); + } + + private SampleQueue createSampleQueue(int id, int type) { + int trackCount = sampleQueues.length; + + boolean isAudioVideo = type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO; + FormatAdjustingSampleQueue trackOutput = + new FormatAdjustingSampleQueue(allocator, drmSessionManager, overridingDrmInitData); + if (isAudioVideo) { + trackOutput.setDrmInitData(drmInitData); + } + trackOutput.setSampleOffsetUs(sampleOffsetUs); + trackOutput.sourceId(chunkUid); trackOutput.setUpstreamFormatChangeListener(this); - trackOutput.sourceId(upstreamChunkUid); - sampleQueues.put(id, trackOutput); + sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1); + sampleQueueTrackIds[trackCount] = id; + sampleQueues = Util.nullSafeArrayAppend(sampleQueues, trackOutput); + sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1); + sampleQueueIsAudioVideoFlags[trackCount] = isAudioVideo; + haveAudioVideoSampleQueues |= sampleQueueIsAudioVideoFlags[trackCount]; + sampleQueueMappingDoneByType.add(type); + sampleQueueIndicesByType.append(type, trackCount); + if (getTrackTypeScore(type) > getTrackTypeScore(primarySampleQueueType)) { + primarySampleQueueIndex = trackCount; + primarySampleQueueType = type; + } + sampleQueuesEnabledStates = Arrays.copyOf(sampleQueuesEnabledStates, trackCount + 1); return trackOutput; } @Override public void endTracks() { - sampleQueuesBuilt = true; - handler.post(maybeFinishPrepareRunnable); + tracksEnded = true; + handler.post(onTracksEndedRunnable); } @Override @@ -497,71 +948,188 @@ import java.util.LinkedList; handler.post(maybeFinishPrepareRunnable); } + // Called by the loading thread. + + /** Called when an {@link HlsMediaChunk} starts extracting media with a new {@link Extractor}. */ + public void onNewExtractor() { + sampleQueueMappingDoneByType.clear(); + } + + /** + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples that + * are subsequently loaded by this wrapper. + * + * @param sampleOffsetUs The timestamp offset in microseconds. + */ + public void setSampleOffsetUs(long sampleOffsetUs) { + if (this.sampleOffsetUs != sampleOffsetUs) { + this.sampleOffsetUs = sampleOffsetUs; + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.setSampleOffsetUs(sampleOffsetUs); + } + } + } + + /** + * Sets default {@link DrmInitData} for samples that are subsequently loaded by this wrapper. + * + *

This method should be called prior to loading each {@link HlsMediaChunk}. The {@link + * DrmInitData} passed should be that of an EXT-X-KEY tag that applies to the chunk, or {@code + * null} otherwise. + * + *

The final {@link DrmInitData} for subsequently queued samples is determined as followed: + * + *

    + *
  1. It is initially set to {@code drmInitData}, unless {@code drmInitData} is null in which + * case it's set to {@link Format#drmInitData} of the upstream {@link Format}. + *
  2. If the initial {@link DrmInitData} is non-null and {@link #overridingDrmInitData} + * contains an entry whose key matches the {@link DrmInitData#schemeType}, then the sample's + * {@link DrmInitData} is overridden to be this entry's value. + *
+ * + *

+ * + * @param drmInitData The default {@link DrmInitData} for samples that are subsequently queued. If + * non-null then it takes precedence over {@link Format#drmInitData} of the upstream {@link + * Format}, but will still be overridden by a matching override in {@link + * #overridingDrmInitData}. + */ + public void setDrmInitData(@Nullable DrmInitData drmInitData) { + if (!Util.areEqual(this.drmInitData, drmInitData)) { + this.drmInitData = drmInitData; + for (int i = 0; i < sampleQueues.length; i++) { + if (sampleQueueIsAudioVideoFlags[i]) { + sampleQueues[i].setDrmInitData(drmInitData); + } + } + } + } + // Internal methods. + private void updateSampleStreams(@NullableType SampleStream[] streams) { + hlsSampleStreams.clear(); + for (SampleStream stream : streams) { + if (stream != null) { + hlsSampleStreams.add((HlsSampleStream) stream); + } + } + } + + private boolean finishedReadingChunk(HlsMediaChunk chunk) { + int chunkUid = chunk.uid; + int sampleQueueCount = sampleQueues.length; + for (int i = 0; i < sampleQueueCount; i++) { + if (sampleQueuesEnabledStates[i] && sampleQueues[i].peekSourceId() == chunkUid) { + return false; + } + } + return true; + } + + private void resetSampleQueues() { + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(pendingResetUpstreamFormats); + } + pendingResetUpstreamFormats = false; + } + + private void onTracksEnded() { + sampleQueuesBuilt = true; + maybeFinishPrepare(); + } + private void maybeFinishPrepare() { - if (released || prepared || !sampleQueuesBuilt) { + if (released || trackGroupToSampleQueueIndex != null || !sampleQueuesBuilt) { return; } - int sampleQueueCount = sampleQueues.size(); - for (int i = 0; i < sampleQueueCount; i++) { - if (sampleQueues.valueAt(i).getUpstreamFormat() == null) { + for (SampleQueue sampleQueue : sampleQueues) { + if (sampleQueue.getUpstreamFormat() == null) { return; } } - buildTracks(); - prepared = true; - callback.onPrepared(); + if (trackGroups != null) { + // The track groups were created with master playlist information. They only need to be mapped + // to a sample queue. + mapSampleQueuesToMatchTrackGroups(); + } else { + // Tracks are created using media segment information. + buildTracksFromSampleStreams(); + setIsPrepared(); + callback.onPrepared(); + } + } + + @RequiresNonNull("trackGroups") + @EnsuresNonNull("trackGroupToSampleQueueIndex") + private void mapSampleQueuesToMatchTrackGroups() { + int trackGroupCount = trackGroups.length; + trackGroupToSampleQueueIndex = new int[trackGroupCount]; + Arrays.fill(trackGroupToSampleQueueIndex, C.INDEX_UNSET); + for (int i = 0; i < trackGroupCount; i++) { + for (int queueIndex = 0; queueIndex < sampleQueues.length; queueIndex++) { + SampleQueue sampleQueue = sampleQueues[queueIndex]; + if (formatsMatch(sampleQueue.getUpstreamFormat(), trackGroups.get(i).getFormat(0))) { + trackGroupToSampleQueueIndex[i] = queueIndex; + break; + } + } + } + for (HlsSampleStream sampleStream : hlsSampleStreams) { + sampleStream.bindSampleQueue(); + } } /** * Builds tracks that are exposed by this {@link HlsSampleStreamWrapper} instance, as well as * internal data-structures required for operation. - *

- * Tracks in HLS are complicated. A HLS master playlist contains a number of "variants". Each + * + *

Tracks in HLS are complicated. A HLS master playlist contains a number of "variants". Each * variant stream typically contains muxed video, audio and (possibly) additional audio, metadata * and caption tracks. We wish to allow the user to select between an adaptive track that spans * all variants, as well as each individual variant. If multiple audio tracks are present within * each variant then we wish to allow the user to select between those also. - *

- * To do this, tracks are constructed as follows. The {@link HlsChunkSource} exposes (N+1) tracks, - * where N is the number of variants defined in the HLS master playlist. These consist of one - * adaptive track defined to span all variants and a track for each individual variant. The + * + *

To do this, tracks are constructed as follows. The {@link HlsChunkSource} exposes (N+1) + * tracks, where N is the number of variants defined in the HLS master playlist. These consist of + * one adaptive track defined to span all variants and a track for each individual variant. The * adaptive track is initially selected. The extractor is then prepared to discover the tracks * inside of each variant stream. The two sets of tracks are then combined by this method to * create a third set, which is the set exposed by this {@link HlsSampleStreamWrapper}: + * *

    - *
  • The extractor tracks are inspected to infer a "primary" track type. If a video track is - * present then it is always the primary type. If not, audio is the primary type if present. - * Else text is the primary type if present. Else there is no primary type.
  • - *
  • If there is exactly one extractor track of the primary type, it's expanded into (N+1) - * exposed tracks, all of which correspond to the primary extractor track and each of which - * corresponds to a different chunk source track. Selecting one of these tracks has the effect - * of switching the selected track on the chunk source.
  • - *
  • All other extractor tracks are exposed directly. Selecting one of these tracks has the - * effect of selecting an extractor track, leaving the selected track on the chunk source - * unchanged.
  • + *
  • The extractor tracks are inspected to infer a "primary" track type. If a video track is + * present then it is always the primary type. If not, audio is the primary type if present. + * Else text is the primary type if present. Else there is no primary type. + *
  • If there is exactly one extractor track of the primary type, it's expanded into (N+1) + * exposed tracks, all of which correspond to the primary extractor track and each of which + * corresponds to a different chunk source track. Selecting one of these tracks has the + * effect of switching the selected track on the chunk source. + *
  • All other extractor tracks are exposed directly. Selecting one of these tracks has the + * effect of selecting an extractor track, leaving the selected track on the chunk source + * unchanged. *
*/ - private void buildTracks() { + @EnsuresNonNull({"trackGroups", "optionalTrackGroups", "trackGroupToSampleQueueIndex"}) + private void buildTracksFromSampleStreams() { // Iterate through the extractor tracks to discover the "primary" track type, and the index // of the single track of this type. - int primaryExtractorTrackType = PRIMARY_TYPE_NONE; + int primaryExtractorTrackType = C.TRACK_TYPE_NONE; int primaryExtractorTrackIndex = C.INDEX_UNSET; - int extractorTrackCount = sampleQueues.size(); + int extractorTrackCount = sampleQueues.length; for (int i = 0; i < extractorTrackCount; i++) { - String sampleMimeType = sampleQueues.valueAt(i).getUpstreamFormat().sampleMimeType; + String sampleMimeType = sampleQueues[i].getUpstreamFormat().sampleMimeType; int trackType; if (MimeTypes.isVideo(sampleMimeType)) { - trackType = PRIMARY_TYPE_VIDEO; + trackType = C.TRACK_TYPE_VIDEO; } else if (MimeTypes.isAudio(sampleMimeType)) { - trackType = PRIMARY_TYPE_AUDIO; + trackType = C.TRACK_TYPE_AUDIO; } else if (MimeTypes.isText(sampleMimeType)) { - trackType = PRIMARY_TYPE_TEXT; + trackType = C.TRACK_TYPE_TEXT; } else { - trackType = PRIMARY_TYPE_NONE; + trackType = C.TRACK_TYPE_NONE; } - if (trackType > primaryExtractorTrackType) { + if (getTrackTypeScore(trackType) > getTrackTypeScore(primaryExtractorTrackType)) { primaryExtractorTrackType = trackType; primaryExtractorTrackIndex = i; } else if (trackType == primaryExtractorTrackType @@ -577,95 +1145,391 @@ import java.util.LinkedList; // Instantiate the necessary internal data-structures. primaryTrackGroupIndex = C.INDEX_UNSET; - groupEnabledStates = new boolean[extractorTrackCount]; + trackGroupToSampleQueueIndex = new int[extractorTrackCount]; + for (int i = 0; i < extractorTrackCount; i++) { + trackGroupToSampleQueueIndex[i] = i; + } // Construct the set of exposed track groups. TrackGroup[] trackGroups = new TrackGroup[extractorTrackCount]; for (int i = 0; i < extractorTrackCount; i++) { - Format sampleFormat = sampleQueues.valueAt(i).getUpstreamFormat(); + Format sampleFormat = sampleQueues[i].getUpstreamFormat(); if (i == primaryExtractorTrackIndex) { Format[] formats = new Format[chunkSourceTrackCount]; - for (int j = 0; j < chunkSourceTrackCount; j++) { - formats[j] = deriveFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat); + if (chunkSourceTrackCount == 1) { + formats[0] = sampleFormat.copyWithManifestFormatInfo(chunkSourceTrackGroup.getFormat(0)); + } else { + for (int j = 0; j < chunkSourceTrackCount; j++) { + formats[j] = deriveFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat, true); + } } trackGroups[i] = new TrackGroup(formats); primaryTrackGroupIndex = i; } else { - Format trackFormat = primaryExtractorTrackType == PRIMARY_TYPE_VIDEO - && MimeTypes.isAudio(sampleFormat.sampleMimeType) ? muxedAudioFormat : null; - trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat)); + Format trackFormat = + primaryExtractorTrackType == C.TRACK_TYPE_VIDEO + && MimeTypes.isAudio(sampleFormat.sampleMimeType) + ? muxedAudioFormat + : null; + trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat, false)); } } - this.trackGroups = new TrackGroupArray(trackGroups); + this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups); + Assertions.checkState(optionalTrackGroups == null); + optionalTrackGroups = Collections.emptySet(); } - /** - * Enables or disables a specified track group. - * - * @param group The index of the track group. - * @param enabledState True if the group is being enabled, or false if it's being disabled. - */ - private void setTrackGroupEnabledState(int group, boolean enabledState) { - Assertions.checkState(groupEnabledStates[group] != enabledState); - groupEnabledStates[group] = enabledState; - enabledTrackCount = enabledTrackCount + (enabledState ? 1 : -1); - } - - /** - * Derives a track format corresponding to a given container format, by combining it with sample - * level information obtained from the samples. - * - * @param containerFormat The container format for which the track format should be derived. - * @param sampleFormat A sample format from which to obtain sample level information. - * @return The derived track format. - */ - private static Format deriveFormat(Format containerFormat, Format sampleFormat) { - if (containerFormat == null) { - return sampleFormat; + private TrackGroupArray createTrackGroupArrayWithDrmInfo(TrackGroup[] trackGroups) { + for (int i = 0; i < trackGroups.length; i++) { + TrackGroup trackGroup = trackGroups[i]; + Format[] exposedFormats = new Format[trackGroup.length]; + for (int j = 0; j < trackGroup.length; j++) { + Format format = trackGroup.getFormat(j); + if (format.drmInitData != null) { + format = + format.copyWithExoMediaCryptoType( + drmSessionManager.getExoMediaCryptoType(format.drmInitData)); + } + exposedFormats[j] = format; + } + trackGroups[i] = new TrackGroup(exposedFormats); } - String codecs = null; - int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType); - if (sampleTrackType == C.TRACK_TYPE_AUDIO) { - codecs = getAudioCodecs(containerFormat.codecs); - } else if (sampleTrackType == C.TRACK_TYPE_VIDEO) { - codecs = getVideoCodecs(containerFormat.codecs); - } - return sampleFormat.copyWithContainerInfo(containerFormat.id, codecs, containerFormat.bitrate, - containerFormat.width, containerFormat.height, containerFormat.selectionFlags, - containerFormat.language); + return new TrackGroupArray(trackGroups); } - private boolean isMediaChunk(Chunk chunk) { - return chunk instanceof HlsMediaChunk; + private HlsMediaChunk getLastMediaChunk() { + return mediaChunks.get(mediaChunks.size() - 1); } private boolean isPendingReset() { return pendingResetPositionUs != C.TIME_UNSET; } - private static String getAudioCodecs(String codecs) { - return getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO); - } - - private static String getVideoCodecs(String codecs) { - return getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO); - } - - private static String getCodecsOfType(String codecs, int trackType) { - if (TextUtils.isEmpty(codecs)) { - return null; - } - String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)"); - StringBuilder builder = new StringBuilder(); - for (String codec : codecArray) { - if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) { - if (builder.length() > 0) { - builder.append(","); - } - builder.append(codec); + /** + * Attempts to seek to the specified position within the sample queues. + * + * @param positionUs The seek position in microseconds. + * @return Whether the in-buffer seek was successful. + */ + private boolean seekInsideBufferUs(long positionUs) { + int sampleQueueCount = sampleQueues.length; + for (int i = 0; i < sampleQueueCount; i++) { + SampleQueue sampleQueue = sampleQueues[i]; + boolean seekInsideQueue = sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ false); + // If we have AV tracks then an in-queue seek is successful if the seek into every AV queue + // is successful. We ignore whether seeks within non-AV queues are successful in this case, as + // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is + // successful only if the seek into every queue succeeds. + if (!seekInsideQueue && (sampleQueueIsAudioVideoFlags[i] || !haveAudioVideoSampleQueues)) { + return false; } } - return builder.length() > 0 ? builder.toString() : null; + return true; } + @RequiresNonNull({"trackGroups", "optionalTrackGroups"}) + private void setIsPrepared() { + prepared = true; + } + + @EnsuresNonNull({"trackGroups", "optionalTrackGroups"}) + private void assertIsPrepared() { + Assertions.checkState(prepared); + Assertions.checkNotNull(trackGroups); + Assertions.checkNotNull(optionalTrackGroups); + } + + /** + * Scores a track type. Where multiple tracks are muxed into a container, the track with the + * highest score is the primary track. + * + * @param trackType The track type. + * @return The score. + */ + private static int getTrackTypeScore(int trackType) { + switch (trackType) { + case C.TRACK_TYPE_VIDEO: + return 3; + case C.TRACK_TYPE_AUDIO: + return 2; + case C.TRACK_TYPE_TEXT: + return 1; + default: + return 0; + } + } + + /** + * Derives a track sample format from the corresponding format in the master playlist, and a + * sample format that may have been obtained from a chunk belonging to a different track. + * + * @param playlistFormat The format information obtained from the master playlist. + * @param sampleFormat The format information obtained from the samples. + * @param propagateBitrate Whether the bitrate from the playlist format should be included in the + * derived format. + * @return The derived track format. + */ + private static Format deriveFormat( + @Nullable Format playlistFormat, Format sampleFormat, boolean propagateBitrate) { + if (playlistFormat == null) { + return sampleFormat; + } + int bitrate = propagateBitrate ? playlistFormat.bitrate : Format.NO_VALUE; + int channelCount = + playlistFormat.channelCount != Format.NO_VALUE + ? playlistFormat.channelCount + : sampleFormat.channelCount; + int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType); + String codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType); + String mimeType = MimeTypes.getMediaMimeType(codecs); + if (mimeType == null) { + mimeType = sampleFormat.sampleMimeType; + } + return sampleFormat.copyWithContainerInfo( + playlistFormat.id, + playlistFormat.label, + mimeType, + codecs, + playlistFormat.metadata, + bitrate, + playlistFormat.width, + playlistFormat.height, + channelCount, + playlistFormat.selectionFlags, + playlistFormat.language); + } + + private static boolean isMediaChunk(Chunk chunk) { + return chunk instanceof HlsMediaChunk; + } + + private static boolean formatsMatch(Format manifestFormat, Format sampleFormat) { + String manifestFormatMimeType = manifestFormat.sampleMimeType; + String sampleFormatMimeType = sampleFormat.sampleMimeType; + int manifestFormatTrackType = MimeTypes.getTrackType(manifestFormatMimeType); + if (manifestFormatTrackType != C.TRACK_TYPE_TEXT) { + return manifestFormatTrackType == MimeTypes.getTrackType(sampleFormatMimeType); + } else if (!Util.areEqual(manifestFormatMimeType, sampleFormatMimeType)) { + return false; + } + if (MimeTypes.APPLICATION_CEA608.equals(manifestFormatMimeType) + || MimeTypes.APPLICATION_CEA708.equals(manifestFormatMimeType)) { + return manifestFormat.accessibilityChannel == sampleFormat.accessibilityChannel; + } + return true; + } + + private static DummyTrackOutput createDummyTrackOutput(int id, int type) { + Log.w(TAG, "Unmapped track with id " + id + " of type " + type); + return new DummyTrackOutput(); + } + + private static final class FormatAdjustingSampleQueue extends SampleQueue { + + private final Map overridingDrmInitData; + @Nullable private DrmInitData drmInitData; + + public FormatAdjustingSampleQueue( + Allocator allocator, + DrmSessionManager drmSessionManager, + Map overridingDrmInitData) { + super(allocator, drmSessionManager); + this.overridingDrmInitData = overridingDrmInitData; + } + + public void setDrmInitData(@Nullable DrmInitData drmInitData) { + this.drmInitData = drmInitData; + invalidateUpstreamFormatAdjustment(); + } + + @Override + public Format getAdjustedUpstreamFormat(Format format) { + @Nullable + DrmInitData drmInitData = this.drmInitData != null ? this.drmInitData : format.drmInitData; + if (drmInitData != null) { + @Nullable + DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType); + if (overridingDrmInitData != null) { + drmInitData = overridingDrmInitData; + } + } + return super.getAdjustedUpstreamFormat( + format.copyWithAdjustments(drmInitData, getAdjustedMetadata(format.metadata))); + } + + /** + * Strips the private timestamp frame from metadata, if present. See: + * https://github.com/google/ExoPlayer/issues/5063 + */ + @Nullable + private Metadata getAdjustedMetadata(@Nullable Metadata metadata) { + if (metadata == null) { + return null; + } + int length = metadata.length(); + int transportStreamTimestampMetadataIndex = C.INDEX_UNSET; + for (int i = 0; i < length; i++) { + Metadata.Entry metadataEntry = metadata.get(i); + if (metadataEntry instanceof PrivFrame) { + PrivFrame privFrame = (PrivFrame) metadataEntry; + if (HlsMediaChunk.PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { + transportStreamTimestampMetadataIndex = i; + break; + } + } + } + if (transportStreamTimestampMetadataIndex == C.INDEX_UNSET) { + return metadata; + } + if (length == 1) { + return null; + } + Metadata.Entry[] newMetadataEntries = new Metadata.Entry[length - 1]; + for (int i = 0; i < length; i++) { + if (i != transportStreamTimestampMetadataIndex) { + int newIndex = i < transportStreamTimestampMetadataIndex ? i : i - 1; + newMetadataEntries[newIndex] = metadata.get(i); + } + } + return new Metadata(newMetadataEntries); + } + } + + private static class EmsgUnwrappingTrackOutput implements TrackOutput { + + private static final String TAG = "EmsgUnwrappingTrackOutput"; + + // TODO(ibaker): Create a Formats util class with common constants like this. + private static final Format ID3_FORMAT = + Format.createSampleFormat( + /* id= */ null, MimeTypes.APPLICATION_ID3, Format.OFFSET_SAMPLE_RELATIVE); + private static final Format EMSG_FORMAT = + Format.createSampleFormat( + /* id= */ null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE); + + private final EventMessageDecoder emsgDecoder; + private final TrackOutput delegate; + private final Format delegateFormat; + @MonotonicNonNull private Format format; + + private byte[] buffer; + private int bufferPosition; + + public EmsgUnwrappingTrackOutput( + TrackOutput delegate, @HlsMediaSource.MetadataType int metadataType) { + this.emsgDecoder = new EventMessageDecoder(); + this.delegate = delegate; + switch (metadataType) { + case HlsMediaSource.METADATA_TYPE_ID3: + delegateFormat = ID3_FORMAT; + break; + case HlsMediaSource.METADATA_TYPE_EMSG: + delegateFormat = EMSG_FORMAT; + break; + default: + throw new IllegalArgumentException("Unknown metadataType: " + metadataType); + } + + this.buffer = new byte[0]; + this.bufferPosition = 0; + } + + @Override + public void format(Format format) { + this.format = format; + delegate.format(delegateFormat); + } + + @Override + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + ensureBufferCapacity(bufferPosition + length); + int numBytesRead = input.read(buffer, bufferPosition, length); + if (numBytesRead == C.RESULT_END_OF_INPUT) { + if (allowEndOfInput) { + return C.RESULT_END_OF_INPUT; + } else { + throw new EOFException(); + } + } + bufferPosition += numBytesRead; + return numBytesRead; + } + + @Override + public void sampleData(ParsableByteArray buffer, int length) { + ensureBufferCapacity(bufferPosition + length); + buffer.readBytes(this.buffer, bufferPosition, length); + bufferPosition += length; + } + + @Override + public void sampleMetadata( + long timeUs, + @C.BufferFlags int flags, + int size, + int offset, + @Nullable CryptoData cryptoData) { + Assertions.checkNotNull(format); + ParsableByteArray sample = getSampleAndTrimBuffer(size, offset); + ParsableByteArray sampleForDelegate; + if (Util.areEqual(format.sampleMimeType, delegateFormat.sampleMimeType)) { + // Incoming format matches delegate track's format, so pass straight through. + sampleForDelegate = sample; + } else if (MimeTypes.APPLICATION_EMSG.equals(format.sampleMimeType)) { + // Incoming sample is EMSG, and delegate track is not expecting EMSG, so try unwrapping. + EventMessage emsg = emsgDecoder.decode(sample); + if (!emsgContainsExpectedWrappedFormat(emsg)) { + Log.w( + TAG, + String.format( + "Ignoring EMSG. Expected it to contain wrapped %s but actual wrapped format: %s", + delegateFormat.sampleMimeType, emsg.getWrappedMetadataFormat())); + return; + } + sampleForDelegate = + new ParsableByteArray(Assertions.checkNotNull(emsg.getWrappedMetadataBytes())); + } else { + Log.w(TAG, "Ignoring sample for unsupported format: " + format.sampleMimeType); + return; + } + + int sampleSize = sampleForDelegate.bytesLeft(); + + delegate.sampleData(sampleForDelegate, sampleSize); + delegate.sampleMetadata(timeUs, flags, sampleSize, offset, cryptoData); + } + + private boolean emsgContainsExpectedWrappedFormat(EventMessage emsg) { + @Nullable Format wrappedMetadataFormat = emsg.getWrappedMetadataFormat(); + return wrappedMetadataFormat != null + && Util.areEqual(delegateFormat.sampleMimeType, wrappedMetadataFormat.sampleMimeType); + } + + private void ensureBufferCapacity(int requiredLength) { + if (buffer.length < requiredLength) { + buffer = Arrays.copyOf(buffer, requiredLength + requiredLength / 2); + } + } + + /** + * Removes a complete sample from the {@link #buffer} field & reshuffles the tail data skipped + * by {@code offset} to the head of the array. + * + * @param size see {@code size} param of {@link #sampleMetadata}. + * @param offset see {@code offset} param of {@link #sampleMetadata}. + * @return A {@link ParsableByteArray} containing the sample removed from {@link #buffer}. + */ + private ParsableByteArray getSampleAndTrimBuffer(int size, int offset) { + int sampleEnd = bufferPosition - offset; + int sampleStart = sampleEnd - size; + + byte[] sampleBytes = Arrays.copyOfRange(buffer, sampleStart, sampleEnd); + ParsableByteArray sample = new ParsableByteArray(sampleBytes); + + System.arraycopy(buffer, sampleEnd, buffer, 0, offset); + bufferPosition = offset; + return sample; + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java new file mode 100644 index 000000000000..681fe57240ae --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java @@ -0,0 +1,245 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Holds metadata associated to an HLS media track. */ +public final class HlsTrackMetadataEntry implements Metadata.Entry { + + /** Holds attributes defined in an EXT-X-STREAM-INF tag. */ + public static final class VariantInfo implements Parcelable { + + /** The bitrate as declared by the EXT-X-STREAM-INF tag. */ + public final long bitrate; + + /** + * The VIDEO value as defined in the EXT-X-STREAM-INF tag, or null if the VIDEO attribute is not + * present. + */ + @Nullable public final String videoGroupId; + + /** + * The AUDIO value as defined in the EXT-X-STREAM-INF tag, or null if the AUDIO attribute is not + * present. + */ + @Nullable public final String audioGroupId; + + /** + * The SUBTITLES value as defined in the EXT-X-STREAM-INF tag, or null if the SUBTITLES + * attribute is not present. + */ + @Nullable public final String subtitleGroupId; + + /** + * The CLOSED-CAPTIONS value as defined in the EXT-X-STREAM-INF tag, or null if the + * CLOSED-CAPTIONS attribute is not present. + */ + @Nullable public final String captionGroupId; + + /** + * Creates an instance. + * + * @param bitrate See {@link #bitrate}. + * @param videoGroupId See {@link #videoGroupId}. + * @param audioGroupId See {@link #audioGroupId}. + * @param subtitleGroupId See {@link #subtitleGroupId}. + * @param captionGroupId See {@link #captionGroupId}. + */ + public VariantInfo( + long bitrate, + @Nullable String videoGroupId, + @Nullable String audioGroupId, + @Nullable String subtitleGroupId, + @Nullable String captionGroupId) { + this.bitrate = bitrate; + this.videoGroupId = videoGroupId; + this.audioGroupId = audioGroupId; + this.subtitleGroupId = subtitleGroupId; + this.captionGroupId = captionGroupId; + } + + /* package */ VariantInfo(Parcel in) { + bitrate = in.readLong(); + videoGroupId = in.readString(); + audioGroupId = in.readString(); + subtitleGroupId = in.readString(); + captionGroupId = in.readString(); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + VariantInfo that = (VariantInfo) other; + return bitrate == that.bitrate + && TextUtils.equals(videoGroupId, that.videoGroupId) + && TextUtils.equals(audioGroupId, that.audioGroupId) + && TextUtils.equals(subtitleGroupId, that.subtitleGroupId) + && TextUtils.equals(captionGroupId, that.captionGroupId); + } + + @Override + public int hashCode() { + int result = (int) (bitrate ^ (bitrate >>> 32)); + result = 31 * result + (videoGroupId != null ? videoGroupId.hashCode() : 0); + result = 31 * result + (audioGroupId != null ? audioGroupId.hashCode() : 0); + result = 31 * result + (subtitleGroupId != null ? subtitleGroupId.hashCode() : 0); + result = 31 * result + (captionGroupId != null ? captionGroupId.hashCode() : 0); + return result; + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(bitrate); + dest.writeString(videoGroupId); + dest.writeString(audioGroupId); + dest.writeString(subtitleGroupId); + dest.writeString(captionGroupId); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public VariantInfo createFromParcel(Parcel in) { + return new VariantInfo(in); + } + + @Override + public VariantInfo[] newArray(int size) { + return new VariantInfo[size]; + } + }; + } + + /** + * The GROUP-ID value of this track, if the track is derived from an EXT-X-MEDIA tag. Null if the + * track is not derived from an EXT-X-MEDIA TAG. + */ + @Nullable public final String groupId; + /** + * The NAME value of this track, if the track is derived from an EXT-X-MEDIA tag. Null if the + * track is not derived from an EXT-X-MEDIA TAG. + */ + @Nullable public final String name; + /** + * The EXT-X-STREAM-INF tags attributes associated with this track. This field is non-applicable + * (and therefore empty) if this track is derived from an EXT-X-MEDIA tag. + */ + public final List variantInfos; + + /** + * Creates an instance. + * + * @param groupId See {@link #groupId}. + * @param name See {@link #name}. + * @param variantInfos See {@link #variantInfos}. + */ + public HlsTrackMetadataEntry( + @Nullable String groupId, @Nullable String name, List variantInfos) { + this.groupId = groupId; + this.name = name; + this.variantInfos = Collections.unmodifiableList(new ArrayList<>(variantInfos)); + } + + /* package */ HlsTrackMetadataEntry(Parcel in) { + groupId = in.readString(); + name = in.readString(); + int variantInfoSize = in.readInt(); + ArrayList variantInfos = new ArrayList<>(variantInfoSize); + for (int i = 0; i < variantInfoSize; i++) { + variantInfos.add(in.readParcelable(VariantInfo.class.getClassLoader())); + } + this.variantInfos = Collections.unmodifiableList(variantInfos); + } + + @Override + public String toString() { + return "HlsTrackMetadataEntry" + (groupId != null ? (" [" + groupId + ", " + name + "]") : ""); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + + HlsTrackMetadataEntry that = (HlsTrackMetadataEntry) other; + return TextUtils.equals(groupId, that.groupId) + && TextUtils.equals(name, that.name) + && variantInfos.equals(that.variantInfos); + } + + @Override + public int hashCode() { + int result = groupId != null ? groupId.hashCode() : 0; + result = 31 * result + (name != null ? name.hashCode() : 0); + result = 31 * result + variantInfos.hashCode(); + return result; + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(groupId); + dest.writeString(name); + int variantInfosSize = variantInfos.size(); + dest.writeInt(variantInfosSize); + for (int i = 0; i < variantInfosSize; i++) { + dest.writeParcelable(variantInfos.get(i), /* parcelableFlags= */ 0); + } + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public HlsTrackMetadataEntry createFromParcel(Parcel in) { + return new HlsTrackMetadataEntry(in); + } + + @Override + public HlsTrackMetadataEntry[] newArray(int size) { + return new HlsTrackMetadataEntry[size]; + } + }; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java new file mode 100644 index 000000000000..a67a92b4b70a --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleQueue; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup; +import java.io.IOException; + +/** Thrown when it is not possible to map a {@link TrackGroup} to a {@link SampleQueue}. */ +public final class SampleQueueMappingException extends IOException { + + /** @param mimeType The mime type of the track group whose mapping failed. */ + public SampleQueueMappingException(@Nullable String mimeType) { + super("Unable to bind a sample queue to TrackGroup with mime type " + mimeType + "."); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/WebvttExtractor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/WebvttExtractor.java index a2445f90b07b..1d5e669a037a 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/WebvttExtractor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/WebvttExtractor.java @@ -16,6 +16,7 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls; import android.text.TextUtils; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; @@ -25,8 +26,8 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorO import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; -import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleDecoderException; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt.WebvttParserUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster; @@ -34,30 +35,34 @@ import java.io.IOException; import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * A special purpose extractor for WebVTT content in HLS. - *

- * This extractor passes through non-empty WebVTT files untouched, however derives the correct + * + *

This extractor passes through non-empty WebVTT files untouched, however derives the correct * sample timestamp for each by sniffing the X-TIMESTAMP-MAP header along with the start timestamp * of the first cue header. Empty WebVTT files are not passed through, since it's not possible to * derive a sample timestamp in this case. */ -/* package */ final class WebvttExtractor implements Extractor { +public final class WebvttExtractor implements Extractor { private static final Pattern LOCAL_TIMESTAMP = Pattern.compile("LOCAL:([^,]+)"); - private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:(\\d+)"); + private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:(-?\\d+)"); + private static final int HEADER_MIN_LENGTH = 6 /* "WEBVTT" */; + private static final int HEADER_MAX_LENGTH = 3 /* optional Byte Order Mark */ + HEADER_MIN_LENGTH; - private final String language; + @Nullable private final String language; private final TimestampAdjuster timestampAdjuster; private final ParsableByteArray sampleDataWrapper; - private ExtractorOutput output; + private @MonotonicNonNull ExtractorOutput output; private byte[] sampleData; private int sampleSize; - public WebvttExtractor(String language, TimestampAdjuster timestampAdjuster) { + public WebvttExtractor(@Nullable String language, TimestampAdjuster timestampAdjuster) { this.language = language; this.timestampAdjuster = timestampAdjuster; this.sampleDataWrapper = new ParsableByteArray(); @@ -68,8 +73,21 @@ import java.util.regex.Pattern; @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { - // This extractor is only used for the HLS use case, which should not call this method. - throw new IllegalStateException(); + // Check whether there is a header without BOM. + input.peekFully( + sampleData, /* offset= */ 0, /* length= */ HEADER_MIN_LENGTH, /* allowEndOfInput= */ false); + sampleDataWrapper.reset(sampleData, HEADER_MIN_LENGTH); + if (WebvttParserUtil.isWebvttHeaderLine(sampleDataWrapper)) { + return true; + } + // The header did not match, try including the BOM. + input.peekFully( + sampleData, + /* offset= */ HEADER_MIN_LENGTH, + HEADER_MAX_LENGTH - HEADER_MIN_LENGTH, + /* allowEndOfInput= */ false); + sampleDataWrapper.reset(sampleData, HEADER_MAX_LENGTH); + return WebvttParserUtil.isWebvttHeaderLine(sampleDataWrapper); } @Override @@ -92,6 +110,8 @@ import java.util.regex.Pattern; @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + // output == null suggests init() hasn't been called + Assertions.checkNotNull(output); int currentFileSize = (int) input.getLength(); // Increase the size of sampleData if necessary. @@ -114,23 +134,21 @@ import java.util.regex.Pattern; return Extractor.RESULT_END_OF_INPUT; } + @RequiresNonNull("output") private void processSample() throws ParserException { ParsableByteArray webvttData = new ParsableByteArray(sampleData); // Validate the first line of the header. - try { - WebvttParserUtil.validateWebvttHeaderLine(webvttData); - } catch (SubtitleDecoderException e) { - throw new ParserException(e); - } + WebvttParserUtil.validateWebvttHeaderLine(webvttData); // Defaults to use if the header doesn't contain an X-TIMESTAMP-MAP header. long vttTimestampUs = 0; long tsTimestampUs = 0; // Parse the remainder of the header looking for X-TIMESTAMP-MAP. - String line; - while (!TextUtils.isEmpty(line = webvttData.readLine())) { + for (String line = webvttData.readLine(); + !TextUtils.isEmpty(line); + line = webvttData.readLine()) { if (line.startsWith("X-TIMESTAMP-MAP")) { Matcher localTimestampMatcher = LOCAL_TIMESTAMP.matcher(line); if (!localTimestampMatcher.find()) { @@ -141,8 +159,7 @@ import java.util.regex.Pattern; throw new ParserException("X-TIMESTAMP-MAP doesn't contain media timestamp: " + line); } vttTimestampUs = WebvttParserUtil.parseTimestampUs(localTimestampMatcher.group(1)); - tsTimestampUs = TimestampAdjuster.ptsToUs( - Long.parseLong(mediaTimestampMatcher.group(1))); + tsTimestampUs = TimestampAdjuster.ptsToUs(Long.parseLong(mediaTimestampMatcher.group(1))); } } @@ -155,8 +172,8 @@ import java.util.regex.Pattern; } long firstCueTimeUs = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)); - long sampleTimeUs = timestampAdjuster.adjustSampleTimestamp( - firstCueTimeUs + tsTimestampUs - vttTimestampUs); + long sampleTimeUs = timestampAdjuster.adjustTsTimestamp( + TimestampAdjuster.usToPts(firstCueTimeUs + tsTimestampUs - vttTimestampUs)); long subsampleOffsetUs = sampleTimeUs - firstCueTimeUs; // Output the track. TrackOutput trackOutput = buildTrackOutput(subsampleOffsetUs); @@ -166,6 +183,7 @@ import java.util.regex.Pattern; trackOutput.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); } + @RequiresNonNull("output") private TrackOutput buildTrackOutput(long subsampleOffsetUs) { TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_TEXT); trackOutput.format(Format.createTextSampleFormat(null, MimeTypes.TEXT_VTT, null, diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java new file mode 100644 index 000000000000..636100a8a958 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.offline; + +import android.net.Uri; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.SegmentDownloader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.ParsingLoadable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.UriUtil; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +/** + * A downloader for HLS streams. + * + *

Example usage: + * + *

{@code
+ * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
+ * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
+ * DownloaderConstructorHelper constructorHelper =
+ *     new DownloaderConstructorHelper(cache, factory);
+ * // Create a downloader for the first variant in a master playlist.
+ * HlsDownloader hlsDownloader =
+ *     new HlsDownloader(
+ *         playlistUri,
+ *         Collections.singletonList(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, 0)),
+ *         constructorHelper);
+ * // Perform the download.
+ * hlsDownloader.download(progressListener);
+ * // Access downloaded data using CacheDataSource
+ * CacheDataSource cacheDataSource =
+ *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);
+ * }
+ */ +public final class HlsDownloader extends SegmentDownloader { + + /** + * @param playlistUri The {@link Uri} of the playlist to be downloaded. + * @param streamKeys Keys defining which renditions in the playlist should be selected for + * download. If empty, all renditions are downloaded. + * @param constructorHelper A {@link DownloaderConstructorHelper} instance. + */ + public HlsDownloader( + Uri playlistUri, List streamKeys, DownloaderConstructorHelper constructorHelper) { + super(playlistUri, streamKeys, constructorHelper); + } + + @Override + protected HlsPlaylist getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException { + return loadManifest(dataSource, dataSpec); + } + + @Override + protected List getSegments( + DataSource dataSource, HlsPlaylist playlist, boolean allowIncompleteList) throws IOException { + ArrayList mediaPlaylistDataSpecs = new ArrayList<>(); + if (playlist instanceof HlsMasterPlaylist) { + HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; + addMediaPlaylistDataSpecs(masterPlaylist.mediaPlaylistUrls, mediaPlaylistDataSpecs); + } else { + mediaPlaylistDataSpecs.add( + SegmentDownloader.getCompressibleDataSpec(Uri.parse(playlist.baseUri))); + } + + ArrayList segments = new ArrayList<>(); + HashSet seenEncryptionKeyUris = new HashSet<>(); + for (DataSpec mediaPlaylistDataSpec : mediaPlaylistDataSpecs) { + segments.add(new Segment(/* startTimeUs= */ 0, mediaPlaylistDataSpec)); + HlsMediaPlaylist mediaPlaylist; + try { + mediaPlaylist = (HlsMediaPlaylist) loadManifest(dataSource, mediaPlaylistDataSpec); + } catch (IOException e) { + if (!allowIncompleteList) { + throw e; + } + // Generating an incomplete segment list is allowed. Advance to the next media playlist. + continue; + } + HlsMediaPlaylist.Segment lastInitSegment = null; + List hlsSegments = mediaPlaylist.segments; + for (int i = 0; i < hlsSegments.size(); i++) { + HlsMediaPlaylist.Segment segment = hlsSegments.get(i); + HlsMediaPlaylist.Segment initSegment = segment.initializationSegment; + if (initSegment != null && initSegment != lastInitSegment) { + lastInitSegment = initSegment; + addSegment(mediaPlaylist, initSegment, seenEncryptionKeyUris, segments); + } + addSegment(mediaPlaylist, segment, seenEncryptionKeyUris, segments); + } + } + return segments; + } + + private void addMediaPlaylistDataSpecs(List mediaPlaylistUrls, List out) { + for (int i = 0; i < mediaPlaylistUrls.size(); i++) { + out.add(SegmentDownloader.getCompressibleDataSpec(mediaPlaylistUrls.get(i))); + } + } + + private static HlsPlaylist loadManifest(DataSource dataSource, DataSpec dataSpec) + throws IOException { + return ParsingLoadable.load( + dataSource, new HlsPlaylistParser(), dataSpec, C.DATA_TYPE_MANIFEST); + } + + private void addSegment( + HlsMediaPlaylist mediaPlaylist, + HlsMediaPlaylist.Segment segment, + HashSet seenEncryptionKeyUris, + ArrayList out) { + String baseUri = mediaPlaylist.baseUri; + long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs; + if (segment.fullSegmentEncryptionKeyUri != null) { + Uri keyUri = UriUtil.resolveToUri(baseUri, segment.fullSegmentEncryptionKeyUri); + if (seenEncryptionKeyUris.add(keyUri)) { + out.add(new Segment(startTimeUs, SegmentDownloader.getCompressibleDataSpec(keyUri))); + } + } + Uri segmentUri = UriUtil.resolveToUri(baseUri, segment.url); + DataSpec dataSpec = + new DataSpec(segmentUri, segment.byterangeOffset, segment.byterangeLength, /* key= */ null); + out.add(new Segment(startTimeUs, dataSpec)); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/package-info.java new file mode 100644 index 000000000000..669bd44c896e --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.offline; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/package-info.java new file mode 100644 index 000000000000..89882bb59676 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java new file mode 100644 index 000000000000..394a97a56ae2 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.ParsingLoadable; + +/** Default implementation for {@link HlsPlaylistParserFactory}. */ +public final class DefaultHlsPlaylistParserFactory implements HlsPlaylistParserFactory { + + @Override + public ParsingLoadable.Parser createPlaylistParser() { + return new HlsPlaylistParser(); + } + + @Override + public ParsingLoadable.Parser createPlaylistParser( + HlsMasterPlaylist masterPlaylist) { + return new HlsPlaylistParser(masterPlaylist); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java new file mode 100644 index 000000000000..b7f6a069756e --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java @@ -0,0 +1,678 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist; + +import android.net.Uri; +import android.os.Handler; +import android.os.SystemClock; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.ParsingLoadable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** Default implementation for {@link HlsPlaylistTracker}. */ +public final class DefaultHlsPlaylistTracker + implements HlsPlaylistTracker, Loader.Callback> { + + /** Factory for {@link DefaultHlsPlaylistTracker} instances. */ + public static final Factory FACTORY = DefaultHlsPlaylistTracker::new; + + /** + * Default coefficient applied on the target duration of a playlist to determine the amount of + * time after which an unchanging playlist is considered stuck. + */ + public static final double DEFAULT_PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 3.5; + + private final HlsDataSourceFactory dataSourceFactory; + private final HlsPlaylistParserFactory playlistParserFactory; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; + private final HashMap playlistBundles; + private final List listeners; + private final double playlistStuckTargetDurationCoefficient; + + @Nullable private ParsingLoadable.Parser mediaPlaylistParser; + @Nullable private EventDispatcher eventDispatcher; + @Nullable private Loader initialPlaylistLoader; + @Nullable private Handler playlistRefreshHandler; + @Nullable private PrimaryPlaylistListener primaryPlaylistListener; + @Nullable private HlsMasterPlaylist masterPlaylist; + @Nullable private Uri primaryMediaPlaylistUrl; + @Nullable private HlsMediaPlaylist primaryMediaPlaylistSnapshot; + private boolean isLive; + private long initialStartTimeUs; + + /** + * Creates an instance. + * + * @param dataSourceFactory A factory for {@link DataSource} instances. + * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. + * @param playlistParserFactory An {@link HlsPlaylistParserFactory}. + */ + public DefaultHlsPlaylistTracker( + HlsDataSourceFactory dataSourceFactory, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, + HlsPlaylistParserFactory playlistParserFactory) { + this( + dataSourceFactory, + loadErrorHandlingPolicy, + playlistParserFactory, + DEFAULT_PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT); + } + + /** + * Creates an instance. + * + * @param dataSourceFactory A factory for {@link DataSource} instances. + * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. + * @param playlistParserFactory An {@link HlsPlaylistParserFactory}. + * @param playlistStuckTargetDurationCoefficient A coefficient to apply to the target duration of + * media playlists in order to determine that a non-changing playlist is stuck. Once a + * playlist is deemed stuck, a {@link PlaylistStuckException} is thrown via {@link + * #maybeThrowPlaylistRefreshError(Uri)}. + */ + public DefaultHlsPlaylistTracker( + HlsDataSourceFactory dataSourceFactory, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, + HlsPlaylistParserFactory playlistParserFactory, + double playlistStuckTargetDurationCoefficient) { + this.dataSourceFactory = dataSourceFactory; + this.playlistParserFactory = playlistParserFactory; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + this.playlistStuckTargetDurationCoefficient = playlistStuckTargetDurationCoefficient; + listeners = new ArrayList<>(); + playlistBundles = new HashMap<>(); + initialStartTimeUs = C.TIME_UNSET; + } + + // HlsPlaylistTracker implementation. + + @Override + public void start( + Uri initialPlaylistUri, + EventDispatcher eventDispatcher, + PrimaryPlaylistListener primaryPlaylistListener) { + this.playlistRefreshHandler = new Handler(); + this.eventDispatcher = eventDispatcher; + this.primaryPlaylistListener = primaryPlaylistListener; + ParsingLoadable masterPlaylistLoadable = + new ParsingLoadable<>( + dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), + initialPlaylistUri, + C.DATA_TYPE_MANIFEST, + playlistParserFactory.createPlaylistParser()); + Assertions.checkState(initialPlaylistLoader == null); + initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MasterPlaylist"); + long elapsedRealtime = + initialPlaylistLoader.startLoading( + masterPlaylistLoadable, + this, + loadErrorHandlingPolicy.getMinimumLoadableRetryCount(masterPlaylistLoadable.type)); + eventDispatcher.loadStarted( + masterPlaylistLoadable.dataSpec, + masterPlaylistLoadable.type, + elapsedRealtime); + } + + @Override + public void stop() { + primaryMediaPlaylistUrl = null; + primaryMediaPlaylistSnapshot = null; + masterPlaylist = null; + initialStartTimeUs = C.TIME_UNSET; + initialPlaylistLoader.release(); + initialPlaylistLoader = null; + for (MediaPlaylistBundle bundle : playlistBundles.values()) { + bundle.release(); + } + playlistRefreshHandler.removeCallbacksAndMessages(null); + playlistRefreshHandler = null; + playlistBundles.clear(); + } + + @Override + public void addListener(PlaylistEventListener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(PlaylistEventListener listener) { + listeners.remove(listener); + } + + @Override + @Nullable + public HlsMasterPlaylist getMasterPlaylist() { + return masterPlaylist; + } + + @Override + @Nullable + public HlsMediaPlaylist getPlaylistSnapshot(Uri url, boolean isForPlayback) { + HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot(); + if (snapshot != null && isForPlayback) { + maybeSetPrimaryUrl(url); + } + return snapshot; + } + + @Override + public long getInitialStartTimeUs() { + return initialStartTimeUs; + } + + @Override + public boolean isSnapshotValid(Uri url) { + return playlistBundles.get(url).isSnapshotValid(); + } + + @Override + public void maybeThrowPrimaryPlaylistRefreshError() throws IOException { + if (initialPlaylistLoader != null) { + initialPlaylistLoader.maybeThrowError(); + } + if (primaryMediaPlaylistUrl != null) { + maybeThrowPlaylistRefreshError(primaryMediaPlaylistUrl); + } + } + + @Override + public void maybeThrowPlaylistRefreshError(Uri url) throws IOException { + playlistBundles.get(url).maybeThrowPlaylistRefreshError(); + } + + @Override + public void refreshPlaylist(Uri url) { + playlistBundles.get(url).loadPlaylist(); + } + + @Override + public boolean isLive() { + return isLive; + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted( + ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { + HlsPlaylist result = loadable.getResult(); + HlsMasterPlaylist masterPlaylist; + boolean isMediaPlaylist = result instanceof HlsMediaPlaylist; + if (isMediaPlaylist) { + masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri); + } else /* result instanceof HlsMasterPlaylist */ { + masterPlaylist = (HlsMasterPlaylist) result; + } + this.masterPlaylist = masterPlaylist; + mediaPlaylistParser = playlistParserFactory.createPlaylistParser(masterPlaylist); + primaryMediaPlaylistUrl = masterPlaylist.variants.get(0).url; + createBundles(masterPlaylist.mediaPlaylistUrls); + MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryMediaPlaylistUrl); + if (isMediaPlaylist) { + // We don't need to load the playlist again. We can use the same result. + primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result, loadDurationMs); + } else { + primaryBundle.loadPlaylist(); + } + eventDispatcher.loadCompleted( + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + } + + @Override + public void onLoadCanceled( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + boolean released) { + eventDispatcher.loadCanceled( + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + } + + @Override + public LoadErrorAction onLoadError( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error, + int errorCount) { + long retryDelayMs = + loadErrorHandlingPolicy.getRetryDelayMsFor( + loadable.type, loadDurationMs, error, errorCount); + boolean isFatal = retryDelayMs == C.TIME_UNSET; + eventDispatcher.loadError( + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded(), + error, + isFatal); + return isFatal + ? Loader.DONT_RETRY_FATAL + : Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs); + } + + // Internal methods. + + private boolean maybeSelectNewPrimaryUrl() { + List variants = masterPlaylist.variants; + int variantsSize = variants.size(); + long currentTimeMs = SystemClock.elapsedRealtime(); + for (int i = 0; i < variantsSize; i++) { + MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i).url); + if (currentTimeMs > bundle.blacklistUntilMs) { + primaryMediaPlaylistUrl = bundle.playlistUrl; + bundle.loadPlaylist(); + return true; + } + } + return false; + } + + private void maybeSetPrimaryUrl(Uri url) { + if (url.equals(primaryMediaPlaylistUrl) + || !isVariantUrl(url) + || (primaryMediaPlaylistSnapshot != null && primaryMediaPlaylistSnapshot.hasEndTag)) { + // Ignore if the primary media playlist URL is unchanged, if the media playlist is not + // referenced directly by a variant, or it the last primary snapshot contains an end tag. + return; + } + primaryMediaPlaylistUrl = url; + playlistBundles.get(primaryMediaPlaylistUrl).loadPlaylist(); + } + + /** Returns whether any of the variants in the master playlist have the specified playlist URL. */ + private boolean isVariantUrl(Uri playlistUrl) { + List variants = masterPlaylist.variants; + for (int i = 0; i < variants.size(); i++) { + if (playlistUrl.equals(variants.get(i).url)) { + return true; + } + } + return false; + } + + private void createBundles(List urls) { + int listSize = urls.size(); + for (int i = 0; i < listSize; i++) { + Uri url = urls.get(i); + MediaPlaylistBundle bundle = new MediaPlaylistBundle(url); + playlistBundles.put(url, bundle); + } + } + + /** + * Called by the bundles when a snapshot changes. + * + * @param url The url of the playlist. + * @param newSnapshot The new snapshot. + */ + private void onPlaylistUpdated(Uri url, HlsMediaPlaylist newSnapshot) { + if (url.equals(primaryMediaPlaylistUrl)) { + if (primaryMediaPlaylistSnapshot == null) { + // This is the first primary url snapshot. + isLive = !newSnapshot.hasEndTag; + initialStartTimeUs = newSnapshot.startTimeUs; + } + primaryMediaPlaylistSnapshot = newSnapshot; + primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot); + } + int listenersSize = listeners.size(); + for (int i = 0; i < listenersSize; i++) { + listeners.get(i).onPlaylistChanged(); + } + } + + private boolean notifyPlaylistError(Uri playlistUrl, long blacklistDurationMs) { + int listenersSize = listeners.size(); + boolean anyBlacklistingFailed = false; + for (int i = 0; i < listenersSize; i++) { + anyBlacklistingFailed |= !listeners.get(i).onPlaylistError(playlistUrl, blacklistDurationMs); + } + return anyBlacklistingFailed; + } + + private HlsMediaPlaylist getLatestPlaylistSnapshot( + HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + if (!loadedPlaylist.isNewerThan(oldPlaylist)) { + if (loadedPlaylist.hasEndTag) { + // If the loaded playlist has an end tag but is not newer than the old playlist then we have + // an inconsistent state. This is typically caused by the server incorrectly resetting the + // media sequence when appending the end tag. We resolve this case as best we can by + // returning the old playlist with the end tag appended. + return oldPlaylist.copyWithEndTag(); + } else { + return oldPlaylist; + } + } + long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist); + int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist); + return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence); + } + + private long getLoadedPlaylistStartTimeUs( + HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + if (loadedPlaylist.hasProgramDateTime) { + return loadedPlaylist.startTimeUs; + } + long primarySnapshotStartTimeUs = + primaryMediaPlaylistSnapshot != null ? primaryMediaPlaylistSnapshot.startTimeUs : 0; + if (oldPlaylist == null) { + return primarySnapshotStartTimeUs; + } + int oldPlaylistSize = oldPlaylist.segments.size(); + Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); + if (firstOldOverlappingSegment != null) { + return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs; + } else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) { + return oldPlaylist.getEndTimeUs(); + } else { + // No segments overlap, we assume the new playlist start coincides with the primary playlist. + return primarySnapshotStartTimeUs; + } + } + + private int getLoadedPlaylistDiscontinuitySequence( + HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + if (loadedPlaylist.hasDiscontinuitySequence) { + return loadedPlaylist.discontinuitySequence; + } + // TODO: Improve cross-playlist discontinuity adjustment. + int primaryUrlDiscontinuitySequence = + primaryMediaPlaylistSnapshot != null + ? primaryMediaPlaylistSnapshot.discontinuitySequence + : 0; + if (oldPlaylist == null) { + return primaryUrlDiscontinuitySequence; + } + Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); + if (firstOldOverlappingSegment != null) { + return oldPlaylist.discontinuitySequence + + firstOldOverlappingSegment.relativeDiscontinuitySequence + - loadedPlaylist.segments.get(0).relativeDiscontinuitySequence; + } + return primaryUrlDiscontinuitySequence; + } + + private static Segment getFirstOldOverlappingSegment( + HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + int mediaSequenceOffset = (int) (loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence); + List oldSegments = oldPlaylist.segments; + return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null; + } + + /** Holds all information related to a specific Media Playlist. */ + private final class MediaPlaylistBundle + implements Loader.Callback>, Runnable { + + private final Uri playlistUrl; + private final Loader mediaPlaylistLoader; + private final ParsingLoadable mediaPlaylistLoadable; + + @Nullable private HlsMediaPlaylist playlistSnapshot; + private long lastSnapshotLoadMs; + private long lastSnapshotChangeMs; + private long earliestNextLoadTimeMs; + private long blacklistUntilMs; + private boolean loadPending; + private IOException playlistError; + + public MediaPlaylistBundle(Uri playlistUrl) { + this.playlistUrl = playlistUrl; + mediaPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MediaPlaylist"); + mediaPlaylistLoadable = + new ParsingLoadable<>( + dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), + playlistUrl, + C.DATA_TYPE_MANIFEST, + mediaPlaylistParser); + } + + @Nullable + public HlsMediaPlaylist getPlaylistSnapshot() { + return playlistSnapshot; + } + + public boolean isSnapshotValid() { + if (playlistSnapshot == null) { + return false; + } + long currentTimeMs = SystemClock.elapsedRealtime(); + long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs)); + return playlistSnapshot.hasEndTag + || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT + || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD + || lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs; + } + + public void release() { + mediaPlaylistLoader.release(); + } + + public void loadPlaylist() { + blacklistUntilMs = 0; + if (loadPending || mediaPlaylistLoader.isLoading() || mediaPlaylistLoader.hasFatalError()) { + // Load already pending, in progress, or a fatal error has been encountered. Do nothing. + return; + } + long currentTimeMs = SystemClock.elapsedRealtime(); + if (currentTimeMs < earliestNextLoadTimeMs) { + loadPending = true; + playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs); + } else { + loadPlaylistImmediately(); + } + } + + public void maybeThrowPlaylistRefreshError() throws IOException { + mediaPlaylistLoader.maybeThrowError(); + if (playlistError != null) { + throw playlistError; + } + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted( + ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { + HlsPlaylist result = loadable.getResult(); + if (result instanceof HlsMediaPlaylist) { + processLoadedPlaylist((HlsMediaPlaylist) result, loadDurationMs); + eventDispatcher.loadCompleted( + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + } else { + playlistError = new ParserException("Loaded playlist has unexpected type."); + } + } + + @Override + public void onLoadCanceled( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + boolean released) { + eventDispatcher.loadCanceled( + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + } + + @Override + public LoadErrorAction onLoadError( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error, + int errorCount) { + LoadErrorAction loadErrorAction; + + long blacklistDurationMs = + loadErrorHandlingPolicy.getBlacklistDurationMsFor( + loadable.type, loadDurationMs, error, errorCount); + boolean shouldBlacklist = blacklistDurationMs != C.TIME_UNSET; + + boolean blacklistingFailed = + notifyPlaylistError(playlistUrl, blacklistDurationMs) || !shouldBlacklist; + if (shouldBlacklist) { + blacklistingFailed |= blacklistPlaylist(blacklistDurationMs); + } + + if (blacklistingFailed) { + long retryDelay = + loadErrorHandlingPolicy.getRetryDelayMsFor( + loadable.type, loadDurationMs, error, errorCount); + loadErrorAction = + retryDelay != C.TIME_UNSET + ? Loader.createRetryAction(false, retryDelay) + : Loader.DONT_RETRY_FATAL; + } else { + loadErrorAction = Loader.DONT_RETRY; + } + + eventDispatcher.loadError( + loadable.dataSpec, + loadable.getUri(), + loadable.getResponseHeaders(), + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded(), + error, + /* wasCanceled= */ !loadErrorAction.isRetry()); + + return loadErrorAction; + } + + // Runnable implementation. + + @Override + public void run() { + loadPending = false; + loadPlaylistImmediately(); + } + + // Internal methods. + + private void loadPlaylistImmediately() { + long elapsedRealtime = + mediaPlaylistLoader.startLoading( + mediaPlaylistLoadable, + this, + loadErrorHandlingPolicy.getMinimumLoadableRetryCount(mediaPlaylistLoadable.type)); + eventDispatcher.loadStarted( + mediaPlaylistLoadable.dataSpec, + mediaPlaylistLoadable.type, + elapsedRealtime); + } + + private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist, long loadDurationMs) { + HlsMediaPlaylist oldPlaylist = playlistSnapshot; + long currentTimeMs = SystemClock.elapsedRealtime(); + lastSnapshotLoadMs = currentTimeMs; + playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist); + if (playlistSnapshot != oldPlaylist) { + playlistError = null; + lastSnapshotChangeMs = currentTimeMs; + onPlaylistUpdated(playlistUrl, playlistSnapshot); + } else if (!playlistSnapshot.hasEndTag) { + if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size() + < playlistSnapshot.mediaSequence) { + // TODO: Allow customization of playlist resets handling. + // The media sequence jumped backwards. The server has probably reset. We do not try + // blacklisting in this case. + playlistError = new PlaylistResetException(playlistUrl); + notifyPlaylistError(playlistUrl, C.TIME_UNSET); + } else if (currentTimeMs - lastSnapshotChangeMs + > C.usToMs(playlistSnapshot.targetDurationUs) + * playlistStuckTargetDurationCoefficient) { + // TODO: Allow customization of stuck playlists handling. + playlistError = new PlaylistStuckException(playlistUrl); + long blacklistDurationMs = + loadErrorHandlingPolicy.getBlacklistDurationMsFor( + C.DATA_TYPE_MANIFEST, loadDurationMs, playlistError, /* errorCount= */ 1); + notifyPlaylistError(playlistUrl, blacklistDurationMs); + if (blacklistDurationMs != C.TIME_UNSET) { + blacklistPlaylist(blacklistDurationMs); + } + } + } + // Do not allow the playlist to load again within the target duration if we obtained a new + // snapshot, or half the target duration otherwise. + earliestNextLoadTimeMs = + currentTimeMs + + C.usToMs( + playlistSnapshot != oldPlaylist + ? playlistSnapshot.targetDurationUs + : (playlistSnapshot.targetDurationUs / 2)); + // Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the + // next load will be scheduled when refreshPlaylist is called, or when this playlist becomes + // the primary. + if (playlistUrl.equals(primaryMediaPlaylistUrl) && !playlistSnapshot.hasEndTag) { + loadPlaylist(); + } + } + + /** + * Blacklists the playlist. + * + * @param blacklistDurationMs The number of milliseconds for which the playlist should be + * blacklisted. + * @return Whether the playlist is the primary, despite being blacklisted. + */ + private boolean blacklistPlaylist(long blacklistDurationMs) { + blacklistUntilMs = SystemClock.elapsedRealtime() + blacklistDurationMs; + return playlistUrl.equals(primaryMediaPlaylistUrl) && !maybeSelectNewPrimaryUrl(); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java new file mode 100644 index 000000000000..a8c9ea1756b6 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.FilteringManifestParser; +import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.ParsingLoadable; +import java.util.List; + +/** + * A {@link HlsPlaylistParserFactory} that includes only the streams identified by the given stream + * keys. + */ +public final class FilteringHlsPlaylistParserFactory implements HlsPlaylistParserFactory { + + private final HlsPlaylistParserFactory hlsPlaylistParserFactory; + private final List streamKeys; + + /** + * @param hlsPlaylistParserFactory A factory for the parsers of the playlists which will be + * filtered. + * @param streamKeys The stream keys. If null or empty then filtering will not occur. + */ + public FilteringHlsPlaylistParserFactory( + HlsPlaylistParserFactory hlsPlaylistParserFactory, List streamKeys) { + this.hlsPlaylistParserFactory = hlsPlaylistParserFactory; + this.streamKeys = streamKeys; + } + + @Override + public ParsingLoadable.Parser createPlaylistParser() { + return new FilteringManifestParser<>( + hlsPlaylistParserFactory.createPlaylistParser(), streamKeys); + } + + @Override + public ParsingLoadable.Parser createPlaylistParser( + HlsMasterPlaylist masterPlaylist) { + return new FilteringManifestParser<>( + hlsPlaylistParserFactory.createPlaylistParser(masterPlaylist), streamKeys); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index 7b9c98b6ded0..376f2b4301ec 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -15,59 +15,316 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist; +import android.net.Uri; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; -/** - * Represents an HLS master playlist. - */ +/** Represents an HLS master playlist. */ public final class HlsMasterPlaylist extends HlsPlaylist { - /** - * Represents a url in an HLS master playlist. - */ - public static final class HlsUrl { + /** Represents an empty master playlist, from which no attributes can be inherited. */ + public static final HlsMasterPlaylist EMPTY = + new HlsMasterPlaylist( + /* baseUri= */ "", + /* tags= */ Collections.emptyList(), + /* variants= */ Collections.emptyList(), + /* videos= */ Collections.emptyList(), + /* audios= */ Collections.emptyList(), + /* subtitles= */ Collections.emptyList(), + /* closedCaptions= */ Collections.emptyList(), + /* muxedAudioFormat= */ null, + /* muxedCaptionFormats= */ Collections.emptyList(), + /* hasIndependentSegments= */ false, + /* variableDefinitions= */ Collections.emptyMap(), + /* sessionKeyDrmInitData= */ Collections.emptyList()); - public final String url; + // These constants must not be changed because they are persisted in offline stream keys. + public static final int GROUP_INDEX_VARIANT = 0; + public static final int GROUP_INDEX_AUDIO = 1; + public static final int GROUP_INDEX_SUBTITLE = 2; + + /** A variant (i.e. an #EXT-X-STREAM-INF tag) in a master playlist. */ + public static final class Variant { + + /** The variant's url. */ + public final Uri url; + + /** Format information associated with this variant. */ public final Format format; - public static HlsUrl createMediaPlaylistHlsUrl(String baseUri) { - Format format = Format.createContainerFormat("0", MimeTypes.APPLICATION_M3U8, null, null, - Format.NO_VALUE, 0, null); - return new HlsUrl(baseUri, format); - } + /** The video rendition group referenced by this variant, or {@code null}. */ + @Nullable public final String videoGroupId; - public HlsUrl(String url, Format format) { + /** The audio rendition group referenced by this variant, or {@code null}. */ + @Nullable public final String audioGroupId; + + /** The subtitle rendition group referenced by this variant, or {@code null}. */ + @Nullable public final String subtitleGroupId; + + /** The caption rendition group referenced by this variant, or {@code null}. */ + @Nullable public final String captionGroupId; + + /** + * @param url See {@link #url}. + * @param format See {@link #format}. + * @param videoGroupId See {@link #videoGroupId}. + * @param audioGroupId See {@link #audioGroupId}. + * @param subtitleGroupId See {@link #subtitleGroupId}. + * @param captionGroupId See {@link #captionGroupId}. + */ + public Variant( + Uri url, + Format format, + @Nullable String videoGroupId, + @Nullable String audioGroupId, + @Nullable String subtitleGroupId, + @Nullable String captionGroupId) { this.url = url; this.format = format; + this.videoGroupId = videoGroupId; + this.audioGroupId = audioGroupId; + this.subtitleGroupId = subtitleGroupId; + this.captionGroupId = captionGroupId; + } + + /** + * Creates a variant for a given media playlist url. + * + * @param url The media playlist url. + * @return The variant instance. + */ + public static Variant createMediaPlaylistVariantUrl(Uri url) { + Format format = + Format.createContainerFormat( + "0", + /* label= */ null, + MimeTypes.APPLICATION_M3U8, + /* sampleMimeType= */ null, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* selectionFlags= */ 0, + /* roleFlags= */ 0, + /* language= */ null); + return new Variant( + url, + format, + /* videoGroupId= */ null, + /* audioGroupId= */ null, + /* subtitleGroupId= */ null, + /* captionGroupId= */ null); + } + + /** Returns a copy of this instance with the given {@link Format}. */ + public Variant copyWithFormat(Format format) { + return new Variant(url, format, videoGroupId, audioGroupId, subtitleGroupId, captionGroupId); + } + } + + /** A rendition (i.e. an #EXT-X-MEDIA tag) in a master playlist. */ + public static final class Rendition { + + /** The rendition's url, or null if the tag does not have a URI attribute. */ + @Nullable public final Uri url; + + /** Format information associated with this rendition. */ + public final Format format; + + /** The group to which this rendition belongs. */ + public final String groupId; + + /** The name of the rendition. */ + public final String name; + + /** + * @param url See {@link #url}. + * @param format See {@link #format}. + * @param groupId See {@link #groupId}. + * @param name See {@link #name}. + */ + public Rendition(@Nullable Uri url, Format format, String groupId, String name) { + this.url = url; + this.format = format; + this.groupId = groupId; + this.name = name; } } - public final List variants; - public final List audios; - public final List subtitles; + /** All of the media playlist URLs referenced by the playlist. */ + public final List mediaPlaylistUrls; + /** The variants declared by the playlist. */ + public final List variants; + /** The video renditions declared by the playlist. */ + public final List videos; + /** The audio renditions declared by the playlist. */ + public final List audios; + /** The subtitle renditions declared by the playlist. */ + public final List subtitles; + /** The closed caption renditions declared by the playlist. */ + public final List closedCaptions; - public final Format muxedAudioFormat; - public final List muxedCaptionFormats; + /** + * The format of the audio muxed in the variants. May be null if the playlist does not declare any + * muxed audio. + */ + @Nullable public final Format muxedAudioFormat; + /** + * The format of the closed captions declared by the playlist. May be empty if the playlist + * explicitly declares no captions are available, or null if the playlist does not declare any + * captions information. + */ + @Nullable public final List muxedCaptionFormats; + /** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */ + public final Map variableDefinitions; + /** DRM initialization data derived from #EXT-X-SESSION-KEY tags. */ + public final List sessionKeyDrmInitData; - public HlsMasterPlaylist(String baseUri, List variants, List audios, - List subtitles, Format muxedAudioFormat, List muxedCaptionFormats) { - super(baseUri); + /** + * @param baseUri See {@link #baseUri}. + * @param tags See {@link #tags}. + * @param variants See {@link #variants}. + * @param videos See {@link #videos}. + * @param audios See {@link #audios}. + * @param subtitles See {@link #subtitles}. + * @param closedCaptions See {@link #closedCaptions}. + * @param muxedAudioFormat See {@link #muxedAudioFormat}. + * @param muxedCaptionFormats See {@link #muxedCaptionFormats}. + * @param hasIndependentSegments See {@link #hasIndependentSegments}. + * @param variableDefinitions See {@link #variableDefinitions}. + * @param sessionKeyDrmInitData See {@link #sessionKeyDrmInitData}. + */ + public HlsMasterPlaylist( + String baseUri, + List tags, + List variants, + List videos, + List audios, + List subtitles, + List closedCaptions, + @Nullable Format muxedAudioFormat, + @Nullable List muxedCaptionFormats, + boolean hasIndependentSegments, + Map variableDefinitions, + List sessionKeyDrmInitData) { + super(baseUri, tags, hasIndependentSegments); + this.mediaPlaylistUrls = + Collections.unmodifiableList( + getMediaPlaylistUrls(variants, videos, audios, subtitles, closedCaptions)); this.variants = Collections.unmodifiableList(variants); + this.videos = Collections.unmodifiableList(videos); this.audios = Collections.unmodifiableList(audios); this.subtitles = Collections.unmodifiableList(subtitles); + this.closedCaptions = Collections.unmodifiableList(closedCaptions); this.muxedAudioFormat = muxedAudioFormat; - this.muxedCaptionFormats = Collections.unmodifiableList(muxedCaptionFormats); + this.muxedCaptionFormats = muxedCaptionFormats != null + ? Collections.unmodifiableList(muxedCaptionFormats) : null; + this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions); + this.sessionKeyDrmInitData = Collections.unmodifiableList(sessionKeyDrmInitData); } - public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUri) { - List variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUri)); - List emptyList = Collections.emptyList(); - return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, - Collections.emptyList()); + @Override + public HlsMasterPlaylist copy(List streamKeys) { + return new HlsMasterPlaylist( + baseUri, + tags, + copyStreams(variants, GROUP_INDEX_VARIANT, streamKeys), + // TODO: Allow stream keys to specify video renditions to be retained. + /* videos= */ Collections.emptyList(), + copyStreams(audios, GROUP_INDEX_AUDIO, streamKeys), + copyStreams(subtitles, GROUP_INDEX_SUBTITLE, streamKeys), + // TODO: Update to retain all closed captions. + /* closedCaptions= */ Collections.emptyList(), + muxedAudioFormat, + muxedCaptionFormats, + hasIndependentSegments, + variableDefinitions, + sessionKeyDrmInitData); + } + + /** + * Creates a playlist with a single variant. + * + * @param variantUrl The url of the single variant. + * @return A master playlist with a single variant for the provided url. + */ + public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUrl) { + List variant = + Collections.singletonList(Variant.createMediaPlaylistVariantUrl(Uri.parse(variantUrl))); + return new HlsMasterPlaylist( + /* baseUri= */ "", + /* tags= */ Collections.emptyList(), + variant, + /* videos= */ Collections.emptyList(), + /* audios= */ Collections.emptyList(), + /* subtitles= */ Collections.emptyList(), + /* closedCaptions= */ Collections.emptyList(), + /* muxedAudioFormat= */ null, + /* muxedCaptionFormats= */ null, + /* hasIndependentSegments= */ false, + /* variableDefinitions= */ Collections.emptyMap(), + /* sessionKeyDrmInitData= */ Collections.emptyList()); + } + + private static List getMediaPlaylistUrls( + List variants, + List videos, + List audios, + List subtitles, + List closedCaptions) { + ArrayList mediaPlaylistUrls = new ArrayList<>(); + for (int i = 0; i < variants.size(); i++) { + Uri uri = variants.get(i).url; + if (!mediaPlaylistUrls.contains(uri)) { + mediaPlaylistUrls.add(uri); + } + } + addMediaPlaylistUrls(videos, mediaPlaylistUrls); + addMediaPlaylistUrls(audios, mediaPlaylistUrls); + addMediaPlaylistUrls(subtitles, mediaPlaylistUrls); + addMediaPlaylistUrls(closedCaptions, mediaPlaylistUrls); + return mediaPlaylistUrls; + } + + private static void addMediaPlaylistUrls(List renditions, List out) { + for (int i = 0; i < renditions.size(); i++) { + Uri uri = renditions.get(i).url; + if (uri != null && !out.contains(uri)) { + out.add(uri); + } + } + } + + private static List copyStreams( + List streams, int groupIndex, List streamKeys) { + List copiedStreams = new ArrayList<>(streamKeys.size()); + // TODO: + // 1. When variants with the same URL are not de-duplicated, duplicates must not increment + // trackIndex so as to avoid breaking stream keys that have been persisted for offline. All + // duplicates should be copied if the first variant is copied, or discarded otherwise. + // 2. When renditions with null URLs are permitted, they must not increment trackIndex so as to + // avoid breaking stream keys that have been persisted for offline. All renitions with null + // URLs should be copied. They may become unreachable if all variants that reference them are + // removed, but this is OK. + // 3. Renditions with URLs matching copied variants should always themselves be copied, even if + // the corresponding stream key is omitted. Else we're throwing away information for no gain. + for (int i = 0; i < streams.size(); i++) { + T stream = streams.get(i); + for (int j = 0; j < streamKeys.size(); j++) { + StreamKey streamKey = streamKeys.get(j); + if (streamKey.groupIndex == groupIndex && streamKey.trackIndex == i) { + copiedStreams.add(stream); + break; + } + } + } + return copiedStreams; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 7c82e2628733..c3250a5cc0f6 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -15,54 +15,145 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.StreamKey; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.List; -/** - * Represents an HLS media playlist. - */ +/** Represents an HLS media playlist. */ public final class HlsMediaPlaylist extends HlsPlaylist { - /** - * Media segment reference. - */ + /** Media segment reference. */ + @SuppressWarnings("ComparableType") public static final class Segment implements Comparable { + /** + * The url of the segment. + */ public final String url; + /** + * The media initialization section for this segment, as defined by #EXT-X-MAP. May be null if + * the media playlist does not define a media section for this segment. The same instance is + * used for all segments that share an EXT-X-MAP tag. + */ + @Nullable public final Segment initializationSegment; + /** The duration of the segment in microseconds, as defined by #EXTINF. */ public final long durationUs; + /** The human readable title of the segment. */ + public final String title; + /** + * The number of #EXT-X-DISCONTINUITY tags in the playlist before the segment. + */ public final int relativeDiscontinuitySequence; + /** + * The start time of the segment in microseconds, relative to the start of the playlist. + */ public final long relativeStartTimeUs; - public final boolean isEncrypted; - public final String encryptionKeyUri; - public final String encryptionIV; + /** + * DRM initialization data for sample decryption, or null if the segment does not use CDM-DRM + * protection. + */ + @Nullable public final DrmInitData drmInitData; + /** + * The encryption identity key uri as defined by #EXT-X-KEY, or null if the segment does not use + * full segment encryption with identity key. + */ + @Nullable public final String fullSegmentEncryptionKeyUri; + /** + * The encryption initialization vector as defined by #EXT-X-KEY, or null if the segment is not + * encrypted. + */ + @Nullable public final String encryptionIV; + /** + * The segment's byte range offset, as defined by #EXT-X-BYTERANGE. + */ public final long byterangeOffset; + /** + * The segment's byte range length, as defined by #EXT-X-BYTERANGE, or {@link C#LENGTH_UNSET} if + * no byte range is specified. + */ public final long byterangeLength; - public Segment(String uri, long byterangeOffset, long byterangeLength) { - this(uri, 0, -1, C.TIME_UNSET, false, null, null, byterangeOffset, byterangeLength); + /** Whether the segment is tagged with #EXT-X-GAP. */ + public final boolean hasGapTag; + + /** + * @param uri See {@link #url}. + * @param byterangeOffset See {@link #byterangeOffset}. + * @param byterangeLength See {@link #byterangeLength}. + * @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}. + * @param encryptionIV See {@link #encryptionIV}. + */ + public Segment( + String uri, + long byterangeOffset, + long byterangeLength, + @Nullable String fullSegmentEncryptionKeyUri, + @Nullable String encryptionIV) { + this( + uri, + /* initializationSegment= */ null, + /* title= */ "", + /* durationUs= */ 0, + /* relativeDiscontinuitySequence= */ -1, + /* relativeStartTimeUs= */ C.TIME_UNSET, + /* drmInitData= */ null, + fullSegmentEncryptionKeyUri, + encryptionIV, + byterangeOffset, + byterangeLength, + /* hasGapTag= */ false); } - public Segment(String uri, long durationUs, int relativeDiscontinuitySequence, - long relativeStartTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV, - long byterangeOffset, long byterangeLength) { - this.url = uri; + /** + * @param url See {@link #url}. + * @param initializationSegment See {@link #initializationSegment}. + * @param title See {@link #title}. + * @param durationUs See {@link #durationUs}. + * @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}. + * @param relativeStartTimeUs See {@link #relativeStartTimeUs}. + * @param drmInitData See {@link #drmInitData}. + * @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}. + * @param encryptionIV See {@link #encryptionIV}. + * @param byterangeOffset See {@link #byterangeOffset}. + * @param byterangeLength See {@link #byterangeLength}. + * @param hasGapTag See {@link #hasGapTag}. + */ + public Segment( + String url, + @Nullable Segment initializationSegment, + String title, + long durationUs, + int relativeDiscontinuitySequence, + long relativeStartTimeUs, + @Nullable DrmInitData drmInitData, + @Nullable String fullSegmentEncryptionKeyUri, + @Nullable String encryptionIV, + long byterangeOffset, + long byterangeLength, + boolean hasGapTag) { + this.url = url; + this.initializationSegment = initializationSegment; + this.title = title; this.durationUs = durationUs; this.relativeDiscontinuitySequence = relativeDiscontinuitySequence; this.relativeStartTimeUs = relativeStartTimeUs; - this.isEncrypted = isEncrypted; - this.encryptionKeyUri = encryptionKeyUri; + this.drmInitData = drmInitData; + this.fullSegmentEncryptionKeyUri = fullSegmentEncryptionKeyUri; this.encryptionIV = encryptionIV; this.byterangeOffset = byterangeOffset; this.byterangeLength = byterangeLength; + this.hasGapTag = hasGapTag; } @Override - public int compareTo(@NonNull Long relativeStartTimeUs) { + public int compareTo(Long relativeStartTimeUs) { return this.relativeStartTimeUs > relativeStartTimeUs ? 1 : (this.relativeStartTimeUs < relativeStartTimeUs ? -1 : 0); } @@ -70,34 +161,110 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } /** - * Type of the playlist as specified by #EXT-X-PLAYLIST-TYPE. + * Type of the playlist, as defined by #EXT-X-PLAYLIST-TYPE. One of {@link + * #PLAYLIST_TYPE_UNKNOWN}, {@link #PLAYLIST_TYPE_VOD} or {@link #PLAYLIST_TYPE_EVENT}. */ + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({PLAYLIST_TYPE_UNKNOWN, PLAYLIST_TYPE_VOD, PLAYLIST_TYPE_EVENT}) public @interface PlaylistType {} + public static final int PLAYLIST_TYPE_UNKNOWN = 0; public static final int PLAYLIST_TYPE_VOD = 1; public static final int PLAYLIST_TYPE_EVENT = 2; + /** + * The type of the playlist. See {@link PlaylistType}. + */ @PlaylistType public final int playlistType; + /** + * The start offset in microseconds, as defined by #EXT-X-START. + */ public final long startOffsetUs; + /** + * If {@link #hasProgramDateTime} is true, contains the datetime as microseconds since epoch. + * Otherwise, contains the aggregated duration of removed segments up to this snapshot of the + * playlist. + */ public final long startTimeUs; + /** + * Whether the playlist contains the #EXT-X-DISCONTINUITY-SEQUENCE tag. + */ public final boolean hasDiscontinuitySequence; + /** + * The discontinuity sequence number of the first media segment in the playlist, as defined by + * #EXT-X-DISCONTINUITY-SEQUENCE. + */ public final int discontinuitySequence; - public final int mediaSequence; + /** + * The media sequence number of the first media segment in the playlist, as defined by + * #EXT-X-MEDIA-SEQUENCE. + */ + public final long mediaSequence; + /** + * The compatibility version, as defined by #EXT-X-VERSION. + */ public final int version; + /** + * The target duration in microseconds, as defined by #EXT-X-TARGETDURATION. + */ public final long targetDurationUs; + /** + * Whether the playlist contains the #EXT-X-ENDLIST tag. + */ public final boolean hasEndTag; + /** + * Whether the playlist contains a #EXT-X-PROGRAM-DATE-TIME tag. + */ public final boolean hasProgramDateTime; - public final Segment initializationSegment; + /** + * Contains the CDM protection schemes used by segments in this playlist. Does not contain any key + * acquisition data. Null if none of the segments in the playlist is CDM-encrypted. + */ + @Nullable public final DrmInitData protectionSchemes; + /** + * The list of segments in the playlist. + */ public final List segments; + /** + * The total duration of the playlist in microseconds. + */ public final long durationUs; - public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, long startOffsetUs, - long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence, - int mediaSequence, int version, long targetDurationUs, boolean hasEndTag, - boolean hasProgramDateTime, Segment initializationSegment, List segments) { - super(baseUri); + /** + * @param playlistType See {@link #playlistType}. + * @param baseUri See {@link #baseUri}. + * @param tags See {@link #tags}. + * @param startOffsetUs See {@link #startOffsetUs}. + * @param startTimeUs See {@link #startTimeUs}. + * @param hasDiscontinuitySequence See {@link #hasDiscontinuitySequence}. + * @param discontinuitySequence See {@link #discontinuitySequence}. + * @param mediaSequence See {@link #mediaSequence}. + * @param version See {@link #version}. + * @param targetDurationUs See {@link #targetDurationUs}. + * @param hasIndependentSegments See {@link #hasIndependentSegments}. + * @param hasEndTag See {@link #hasEndTag}. + * @param protectionSchemes See {@link #protectionSchemes}. + * @param hasProgramDateTime See {@link #hasProgramDateTime}. + * @param segments See {@link #segments}. + */ + public HlsMediaPlaylist( + @PlaylistType int playlistType, + String baseUri, + List tags, + long startOffsetUs, + long startTimeUs, + boolean hasDiscontinuitySequence, + int discontinuitySequence, + long mediaSequence, + int version, + long targetDurationUs, + boolean hasIndependentSegments, + boolean hasEndTag, + boolean hasProgramDateTime, + @Nullable DrmInitData protectionSchemes, + List segments) { + super(baseUri, tags, hasIndependentSegments); this.playlistType = playlistType; this.startTimeUs = startTimeUs; this.hasDiscontinuitySequence = hasDiscontinuitySequence; @@ -107,7 +274,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { this.targetDurationUs = targetDurationUs; this.hasEndTag = hasEndTag; this.hasProgramDateTime = hasProgramDateTime; - this.initializationSegment = initializationSegment; + this.protectionSchemes = protectionSchemes; this.segments = Collections.unmodifiableList(segments); if (!segments.isEmpty()) { Segment last = segments.get(segments.size() - 1); @@ -119,6 +286,11 @@ public final class HlsMediaPlaylist extends HlsPlaylist { : startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs; } + @Override + public HlsMediaPlaylist copy(List streamKeys) { + return this; + } + /** * Returns whether this playlist is newer than {@code other}. * @@ -139,6 +311,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist { || (segmentCount == otherSegmentCount && hasEndTag && !other.hasEndTag); } + /** + * Returns the result of adding the duration of the playlist to its start time. + */ public long getEndTimeUs() { return startTimeUs + durationUs; } @@ -150,27 +325,51 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * * @param startTimeUs The start time for the returned playlist. * @param discontinuitySequence The discontinuity sequence for the returned playlist. - * @return The playlist. + * @return An identical playlist including the provided discontinuity and timing information. */ public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) { - return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, true, - discontinuitySequence, mediaSequence, version, targetDurationUs, hasEndTag, - hasProgramDateTime, initializationSegment, segments); + return new HlsMediaPlaylist( + playlistType, + baseUri, + tags, + startOffsetUs, + startTimeUs, + /* hasDiscontinuitySequence= */ true, + discontinuitySequence, + mediaSequence, + version, + targetDurationUs, + hasIndependentSegments, + hasEndTag, + hasProgramDateTime, + protectionSchemes, + segments); } /** * Returns a playlist identical to this one except that an end tag is added. If an end tag is * already present then the playlist will return itself. - * - * @return The playlist. */ public HlsMediaPlaylist copyWithEndTag() { if (this.hasEndTag) { return this; } - return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, - hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs, - true, hasProgramDateTime, initializationSegment, segments); + return new HlsMediaPlaylist( + playlistType, + baseUri, + tags, + startOffsetUs, + startTimeUs, + hasDiscontinuitySequence, + discontinuitySequence, + mediaSequence, + version, + targetDurationUs, + hasIndependentSegments, + /* hasEndTag= */ true, + hasProgramDateTime, + protectionSchemes, + segments); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java index 719170f58a24..28f9b0eeb0c0 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java @@ -15,15 +15,36 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist; -/** - * Represents an HLS playlist. - */ -public abstract class HlsPlaylist { +import org.mozilla.thirdparty.com.google.android.exoplayer2.offline.FilterableManifest; +import java.util.Collections; +import java.util.List; +/** Represents an HLS playlist. */ +public abstract class HlsPlaylist implements FilterableManifest { + + /** + * The base uri. Used to resolve relative paths. + */ public final String baseUri; + /** + * The list of tags in the playlist. + */ + public final List tags; + /** + * Whether the media is formed of independent segments, as defined by the + * #EXT-X-INDEPENDENT-SEGMENTS tag. + */ + public final boolean hasIndependentSegments; - protected HlsPlaylist(String baseUri) { + /** + * @param baseUri See {@link #baseUri}. + * @param tags See {@link #tags}. + * @param hasIndependentSegments See {@link #hasIndependentSegments}. + */ + protected HlsPlaylist(String baseUri, List tags, boolean hasIndependentSegments) { this.baseUri = baseUri; + this.tags = Collections.unmodifiableList(tags); + this.hasIndependentSegments = hasIndependentSegments; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 61b74390f0b6..5495d2852003 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -16,24 +16,45 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist; import android.net.Uri; +import android.text.TextUtils; +import android.util.Base64; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.UnrecognizedInputFormatException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry.VariantInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Rendition; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.ParsingLoadable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.UriUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.LinkedList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; import java.util.Queue; +import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.PolyNull; /** * HLS playlists parsing logic. @@ -42,8 +63,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser extraLines = new LinkedList<>(); + Queue extraLines = new ArrayDeque<>(); String line; try { if (!checkPlaylistHeader(reader)) { @@ -126,7 +215,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variants = new ArrayList<>(); - ArrayList audios = new ArrayList<>(); - ArrayList subtitles = new ArrayList<>(); + HashMap> urlToVariantInfos = new HashMap<>(); + HashMap variableDefinitions = new HashMap<>(); + ArrayList variants = new ArrayList<>(); + ArrayList videos = new ArrayList<>(); + ArrayList audios = new ArrayList<>(); + ArrayList subtitles = new ArrayList<>(); + ArrayList closedCaptions = new ArrayList<>(); + ArrayList mediaTags = new ArrayList<>(); + ArrayList sessionKeyDrmInitData = new ArrayList<>(); + ArrayList tags = new ArrayList<>(); Format muxedAudioFormat = null; - ArrayList muxedCaptionFormats = new ArrayList<>(); + List muxedCaptionFormats = null; + boolean noClosedCaptions = false; + boolean hasIndependentSegmentsTag = false; String line; while (iterator.hasNext()) { line = iterator.next(); - if (line.startsWith(TAG_MEDIA)) { - @C.SelectionFlags int selectionFlags = parseSelectionFlags(line); - String uri = parseOptionalStringAttr(line, REGEX_URI); - String id = parseStringAttr(line, REGEX_NAME); - String language = parseOptionalStringAttr(line, REGEX_LANGUAGE); - Format format; - switch (parseStringAttr(line, REGEX_TYPE)) { - case TYPE_AUDIO: - format = Format.createAudioContainerFormat(id, MimeTypes.APPLICATION_M3U8, null, null, - Format.NO_VALUE, Format.NO_VALUE, Format.NO_VALUE, null, selectionFlags, language); - if (uri == null) { - muxedAudioFormat = format; - } else { - audios.add(new HlsMasterPlaylist.HlsUrl(uri, format)); - } - break; - case TYPE_SUBTITLES: - format = Format.createTextContainerFormat(id, MimeTypes.APPLICATION_M3U8, - MimeTypes.TEXT_VTT, null, Format.NO_VALUE, selectionFlags, language); - subtitles.add(new HlsMasterPlaylist.HlsUrl(uri, format)); - break; - case TYPE_CLOSED_CAPTIONS: - String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID); - String mimeType; - int accessibilityChannel; - if (instreamId.startsWith("CC")) { - mimeType = MimeTypes.APPLICATION_CEA608; - accessibilityChannel = Integer.parseInt(instreamId.substring(2)); - } else /* starts with SERVICE */ { - mimeType = MimeTypes.APPLICATION_CEA708; - accessibilityChannel = Integer.parseInt(instreamId.substring(7)); - } - muxedCaptionFormats.add(Format.createTextContainerFormat(id, null, mimeType, null, - Format.NO_VALUE, selectionFlags, language, accessibilityChannel)); - break; - default: - // Do nothing. - break; + + if (line.startsWith(TAG_PREFIX)) { + // We expose all tags through the playlist. + tags.add(line); + } + + if (line.startsWith(TAG_DEFINE)) { + variableDefinitions.put( + /* key= */ parseStringAttr(line, REGEX_NAME, variableDefinitions), + /* value= */ parseStringAttr(line, REGEX_VALUE, variableDefinitions)); + } else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) { + hasIndependentSegmentsTag = true; + } else if (line.startsWith(TAG_MEDIA)) { + // Media tags are parsed at the end to include codec information from #EXT-X-STREAM-INF + // tags. + mediaTags.add(line); + } else if (line.startsWith(TAG_SESSION_KEY)) { + String keyFormat = + parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions); + SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions); + if (schemeData != null) { + String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions); + String scheme = parseEncryptionScheme(method); + sessionKeyDrmInitData.add(new DrmInitData(scheme, schemeData)); } } else if (line.startsWith(TAG_STREAM_INF)) { + noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE); int bitrate = parseIntAttr(line, REGEX_BANDWIDTH); - String codecs = parseOptionalStringAttr(line, REGEX_CODECS); - String resolutionString = parseOptionalStringAttr(line, REGEX_RESOLUTION); + // TODO: Plumb this into Format. + int averageBitrate = parseOptionalIntAttr(line, REGEX_AVERAGE_BANDWIDTH, -1); + String codecs = parseOptionalStringAttr(line, REGEX_CODECS, variableDefinitions); + String resolutionString = + parseOptionalStringAttr(line, REGEX_RESOLUTION, variableDefinitions); int width; int height; if (resolutionString != null) { @@ -235,36 +324,288 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variantInfosForUrl = urlToVariantInfos.get(uri); + if (variantInfosForUrl == null) { + variantInfosForUrl = new ArrayList<>(); + urlToVariantInfos.put(uri, variantInfosForUrl); + } + variantInfosForUrl.add( + new VariantInfo( + bitrate, videoGroupId, audioGroupId, subtitlesGroupId, closedCaptionsGroupId)); } } - return new HlsMasterPlaylist(baseUri, variants, audios, subtitles, muxedAudioFormat, - muxedCaptionFormats); + + // TODO: Don't deduplicate variants by URL. + ArrayList deduplicatedVariants = new ArrayList<>(); + HashSet urlsInDeduplicatedVariants = new HashSet<>(); + for (int i = 0; i < variants.size(); i++) { + Variant variant = variants.get(i); + if (urlsInDeduplicatedVariants.add(variant.url)) { + Assertions.checkState(variant.format.metadata == null); + HlsTrackMetadataEntry hlsMetadataEntry = + new HlsTrackMetadataEntry( + /* groupId= */ null, + /* name= */ null, + Assertions.checkNotNull(urlToVariantInfos.get(variant.url))); + deduplicatedVariants.add( + variant.copyWithFormat( + variant.format.copyWithMetadata(new Metadata(hlsMetadataEntry)))); + } + } + + for (int i = 0; i < mediaTags.size(); i++) { + line = mediaTags.get(i); + String groupId = parseStringAttr(line, REGEX_GROUP_ID, variableDefinitions); + String name = parseStringAttr(line, REGEX_NAME, variableDefinitions); + String referenceUri = parseOptionalStringAttr(line, REGEX_URI, variableDefinitions); + Uri uri = referenceUri == null ? null : UriUtil.resolveToUri(baseUri, referenceUri); + String language = parseOptionalStringAttr(line, REGEX_LANGUAGE, variableDefinitions); + @C.SelectionFlags int selectionFlags = parseSelectionFlags(line); + @C.RoleFlags int roleFlags = parseRoleFlags(line, variableDefinitions); + String formatId = groupId + ":" + name; + Format format; + Metadata metadata = + new Metadata(new HlsTrackMetadataEntry(groupId, name, Collections.emptyList())); + switch (parseStringAttr(line, REGEX_TYPE, variableDefinitions)) { + case TYPE_VIDEO: + Variant variant = getVariantWithVideoGroup(variants, groupId); + String codecs = null; + int width = Format.NO_VALUE; + int height = Format.NO_VALUE; + float frameRate = Format.NO_VALUE; + if (variant != null) { + Format variantFormat = variant.format; + codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO); + width = variantFormat.width; + height = variantFormat.height; + frameRate = variantFormat.frameRate; + } + String sampleMimeType = codecs != null ? MimeTypes.getMediaMimeType(codecs) : null; + format = + Format.createVideoContainerFormat( + /* id= */ formatId, + /* label= */ name, + /* containerMimeType= */ MimeTypes.APPLICATION_M3U8, + sampleMimeType, + codecs, + /* metadata= */ null, + /* bitrate= */ Format.NO_VALUE, + width, + height, + frameRate, + /* initializationData= */ null, + selectionFlags, + roleFlags) + .copyWithMetadata(metadata); + if (uri == null) { + // TODO: Remove this case and add a Rendition with a null uri to videos. + } else { + videos.add(new Rendition(uri, format, groupId, name)); + } + break; + case TYPE_AUDIO: + variant = getVariantWithAudioGroup(variants, groupId); + codecs = + variant != null + ? Util.getCodecsOfType(variant.format.codecs, C.TRACK_TYPE_AUDIO) + : null; + sampleMimeType = codecs != null ? MimeTypes.getMediaMimeType(codecs) : null; + String channelsString = + parseOptionalStringAttr(line, REGEX_CHANNELS, variableDefinitions); + int channelCount = Format.NO_VALUE; + if (channelsString != null) { + channelCount = Integer.parseInt(Util.splitAtFirst(channelsString, "/")[0]); + if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType) && channelsString.endsWith("/JOC")) { + sampleMimeType = MimeTypes.AUDIO_E_AC3_JOC; + } + } + format = + Format.createAudioContainerFormat( + /* id= */ formatId, + /* label= */ name, + /* containerMimeType= */ MimeTypes.APPLICATION_M3U8, + sampleMimeType, + codecs, + /* metadata= */ null, + /* bitrate= */ Format.NO_VALUE, + channelCount, + /* sampleRate= */ Format.NO_VALUE, + /* initializationData= */ null, + selectionFlags, + roleFlags, + language); + if (uri == null) { + // TODO: Remove muxedAudioFormat and add a Rendition with a null uri to audios. + muxedAudioFormat = format; + } else { + audios.add(new Rendition(uri, format.copyWithMetadata(metadata), groupId, name)); + } + break; + case TYPE_SUBTITLES: + codecs = null; + sampleMimeType = null; + variant = getVariantWithSubtitleGroup(variants, groupId); + if (variant != null) { + codecs = Util.getCodecsOfType(variant.format.codecs, C.TRACK_TYPE_TEXT); + sampleMimeType = MimeTypes.getMediaMimeType(codecs); + } + if (sampleMimeType == null) { + sampleMimeType = MimeTypes.TEXT_VTT; + } + format = + Format.createTextContainerFormat( + /* id= */ formatId, + /* label= */ name, + /* containerMimeType= */ MimeTypes.APPLICATION_M3U8, + sampleMimeType, + codecs, + /* bitrate= */ Format.NO_VALUE, + selectionFlags, + roleFlags, + language) + .copyWithMetadata(metadata); + subtitles.add(new Rendition(uri, format, groupId, name)); + break; + case TYPE_CLOSED_CAPTIONS: + String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID, variableDefinitions); + String mimeType; + int accessibilityChannel; + if (instreamId.startsWith("CC")) { + mimeType = MimeTypes.APPLICATION_CEA608; + accessibilityChannel = Integer.parseInt(instreamId.substring(2)); + } else /* starts with SERVICE */ { + mimeType = MimeTypes.APPLICATION_CEA708; + accessibilityChannel = Integer.parseInt(instreamId.substring(7)); + } + if (muxedCaptionFormats == null) { + muxedCaptionFormats = new ArrayList<>(); + } + muxedCaptionFormats.add( + Format.createTextContainerFormat( + /* id= */ formatId, + /* label= */ name, + /* containerMimeType= */ null, + /* sampleMimeType= */ mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + selectionFlags, + roleFlags, + language, + accessibilityChannel)); + // TODO: Remove muxedCaptionFormats and add a Rendition with a null uri to closedCaptions. + break; + default: + // Do nothing. + break; + } + } + + if (noClosedCaptions) { + muxedCaptionFormats = Collections.emptyList(); + } + + return new HlsMasterPlaylist( + baseUri, + tags, + deduplicatedVariants, + videos, + audios, + subtitles, + closedCaptions, + muxedAudioFormat, + muxedCaptionFormats, + hasIndependentSegmentsTag, + variableDefinitions, + sessionKeyDrmInitData); } - @C.SelectionFlags - private static int parseSelectionFlags(String line) { - return (parseBooleanAttribute(line, REGEX_DEFAULT, false) ? C.SELECTION_FLAG_DEFAULT : 0) - | (parseBooleanAttribute(line, REGEX_FORCED, false) ? C.SELECTION_FLAG_FORCED : 0) - | (parseBooleanAttribute(line, REGEX_AUTOSELECT, false) ? C.SELECTION_FLAG_AUTOSELECT : 0); + @Nullable + private static Variant getVariantWithAudioGroup(ArrayList variants, String groupId) { + for (int i = 0; i < variants.size(); i++) { + Variant variant = variants.get(i); + if (groupId.equals(variant.audioGroupId)) { + return variant; + } + } + return null; } - private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String baseUri) - throws IOException { + @Nullable + private static Variant getVariantWithVideoGroup(ArrayList variants, String groupId) { + for (int i = 0; i < variants.size(); i++) { + Variant variant = variants.get(i); + if (groupId.equals(variant.videoGroupId)) { + return variant; + } + } + return null; + } + + @Nullable + private static Variant getVariantWithSubtitleGroup(ArrayList variants, String groupId) { + for (int i = 0; i < variants.size(); i++) { + Variant variant = variants.get(i); + if (groupId.equals(variant.subtitleGroupId)) { + return variant; + } + } + return null; + } + + private static HlsMediaPlaylist parseMediaPlaylist( + HlsMasterPlaylist masterPlaylist, LineIterator iterator, String baseUri) throws IOException { @HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN; long startOffsetUs = C.TIME_UNSET; - int mediaSequence = 0; + long mediaSequence = 0; int version = 1; // Default version == 1. long targetDurationUs = C.TIME_UNSET; + boolean hasIndependentSegmentsTag = masterPlaylist.hasIndependentSegments; boolean hasEndTag = false; Segment initializationSegment = null; + HashMap variableDefinitions = new HashMap<>(); List segments = new ArrayList<>(); + List tags = new ArrayList<>(); long segmentDurationUs = 0; + String segmentTitle = ""; boolean hasDiscontinuitySequence = false; int playlistDiscontinuitySequence = 0; int relativeDiscontinuitySequence = 0; @@ -272,29 +613,37 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser currentSchemeDatas = new TreeMap<>(); + String encryptionScheme = null; + DrmInitData cachedDrmInitData = null; String line; while (iterator.hasNext()) { line = iterator.next(); + + if (line.startsWith(TAG_PREFIX)) { + // We expose all tags through the playlist. + tags.add(line); + } + if (line.startsWith(TAG_PLAYLIST_TYPE)) { - String playlistTypeString = parseStringAttr(line, REGEX_PLAYLIST_TYPE); + String playlistTypeString = parseStringAttr(line, REGEX_PLAYLIST_TYPE, variableDefinitions); if ("VOD".equals(playlistTypeString)) { playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_VOD; } else if ("EVENT".equals(playlistTypeString)) { playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_EVENT; - } else { - throw new ParserException("Illegal playlist type: " + playlistTypeString); } } else if (line.startsWith(TAG_START)) { startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND); } else if (line.startsWith(TAG_INIT_SEGMENT)) { - String uri = parseStringAttr(line, REGEX_URI); - String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE); + String uri = parseStringAttr(line, REGEX_URI, variableDefinitions); + String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE, variableDefinitions); if (byteRange != null) { String[] splitByteRange = byteRange.split("@"); segmentByteRangeLength = Long.parseLong(splitByteRange[0]); @@ -302,31 +651,78 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser 1) { @@ -343,62 +739,217 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variableDefinitions) { + String concatenatedCharacteristics = + parseOptionalStringAttr(line, REGEX_CHARACTERISTICS, variableDefinitions); + if (TextUtils.isEmpty(concatenatedCharacteristics)) { + return 0; + } + String[] characteristics = Util.split(concatenatedCharacteristics, ","); + @C.RoleFlags int roleFlags = 0; + if (Util.contains(characteristics, "public.accessibility.describes-video")) { + roleFlags |= C.ROLE_FLAG_DESCRIBES_VIDEO; + } + if (Util.contains(characteristics, "public.accessibility.transcribes-spoken-dialog")) { + roleFlags |= C.ROLE_FLAG_TRANSCRIBES_DIALOG; + } + if (Util.contains(characteristics, "public.accessibility.describes-music-and-sound")) { + roleFlags |= C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND; + } + if (Util.contains(characteristics, "public.easy-to-read")) { + roleFlags |= C.ROLE_FLAG_EASY_TO_READ; + } + return roleFlags; } - private static double parseDoubleAttr(String line, Pattern pattern) throws ParserException { - return Double.parseDouble(parseStringAttr(line, pattern)); - } - - private static String parseOptionalStringAttr(String line, Pattern pattern) { - Matcher matcher = pattern.matcher(line); - if (matcher.find()) { - return matcher.group(1); + @Nullable + private static SchemeData parseDrmSchemeData( + String line, String keyFormat, Map variableDefinitions) + throws ParserException { + String keyFormatVersions = + parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1", variableDefinitions); + if (KEYFORMAT_WIDEVINE_PSSH_BINARY.equals(keyFormat)) { + String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions); + return new SchemeData( + C.WIDEVINE_UUID, + MimeTypes.VIDEO_MP4, + Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT)); + } else if (KEYFORMAT_WIDEVINE_PSSH_JSON.equals(keyFormat)) { + return new SchemeData(C.WIDEVINE_UUID, "hls", Util.getUtf8Bytes(line)); + } else if (KEYFORMAT_PLAYREADY.equals(keyFormat) && "1".equals(keyFormatVersions)) { + String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions); + byte[] data = Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT); + byte[] psshData = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, data); + return new SchemeData(C.PLAYREADY_UUID, MimeTypes.VIDEO_MP4, psshData); } return null; } - private static boolean parseBooleanAttribute(String line, Pattern pattern, boolean defaultValue) { + private static String parseEncryptionScheme(String method) { + return METHOD_SAMPLE_AES_CENC.equals(method) || METHOD_SAMPLE_AES_CTR.equals(method) + ? C.CENC_TYPE_cenc + : C.CENC_TYPE_cbcs; + } + + private static int parseIntAttr(String line, Pattern pattern) throws ParserException { + return Integer.parseInt(parseStringAttr(line, pattern, Collections.emptyMap())); + } + + private static int parseOptionalIntAttr(String line, Pattern pattern, int defaultValue) { + Matcher matcher = pattern.matcher(line); + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)); + } + return defaultValue; + } + + private static long parseLongAttr(String line, Pattern pattern) throws ParserException { + return Long.parseLong(parseStringAttr(line, pattern, Collections.emptyMap())); + } + + private static double parseDoubleAttr(String line, Pattern pattern) throws ParserException { + return Double.parseDouble(parseStringAttr(line, pattern, Collections.emptyMap())); + } + + private static String parseStringAttr( + String line, Pattern pattern, Map variableDefinitions) + throws ParserException { + String value = parseOptionalStringAttr(line, pattern, variableDefinitions); + if (value != null) { + return value; + } else { + throw new ParserException("Couldn't match " + pattern.pattern() + " in " + line); + } + } + + private static @Nullable String parseOptionalStringAttr( + String line, Pattern pattern, Map variableDefinitions) { + return parseOptionalStringAttr(line, pattern, null, variableDefinitions); + } + + private static @PolyNull String parseOptionalStringAttr( + String line, + Pattern pattern, + @PolyNull String defaultValue, + Map variableDefinitions) { + Matcher matcher = pattern.matcher(line); + String value = matcher.find() ? matcher.group(1) : defaultValue; + return variableDefinitions.isEmpty() || value == null + ? value + : replaceVariableReferences(value, variableDefinitions); + } + + private static String replaceVariableReferences( + String string, Map variableDefinitions) { + Matcher matcher = REGEX_VARIABLE_REFERENCE.matcher(string); + // TODO: Replace StringBuffer with StringBuilder once Java 9 is available. + StringBuffer stringWithReplacements = new StringBuffer(); + while (matcher.find()) { + String groupName = matcher.group(1); + if (variableDefinitions.containsKey(groupName)) { + matcher.appendReplacement( + stringWithReplacements, Matcher.quoteReplacement(variableDefinitions.get(groupName))); + } else { + // The variable is not defined. The value is ignored. + } + } + matcher.appendTail(stringWithReplacements); + return stringWithReplacements.toString(); + } + + private static boolean parseOptionalBooleanAttribute( + String line, Pattern pattern, boolean defaultValue) { Matcher matcher = pattern.matcher(line); if (matcher.find()) { return matcher.group(1).equals(BOOLEAN_TRUE); @@ -415,19 +966,20 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser extraLines; - private String next; + @Nullable private String next; public LineIterator(Queue extraLines, BufferedReader reader) { this.extraLines = extraLines; this.reader = reader; } + @EnsuresNonNullIf(expression = "next", result = true) public boolean hasNext() throws IOException { if (next != null) { return true; } if (!extraLines.isEmpty()) { - next = extraLines.poll(); + next = Assertions.checkNotNull(extraLines.poll()); return true; } while ((next = reader.readLine()) != null) { @@ -439,13 +991,15 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser createPlaylistParser(); + + /** + * Returns a playlist parser for playlists that were referenced by the given {@link + * HlsMasterPlaylist}. Returned {@link HlsMediaPlaylist} instances may inherit attributes from + * {@code masterPlaylist}. + * + * @param masterPlaylist The master playlist that referenced any parsed media playlists. + * @return A parser for HLS playlists. + */ + ParsingLoadable.Parser createPlaylistParser(HlsMasterPlaylist masterPlaylist); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index efbc2657316d..69f8cb02c994 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2018 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. @@ -16,34 +16,45 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist; import android.net.Uri; -import android.os.Handler; -import android.os.SystemClock; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; -import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.ParsingLoadable; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.UriUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import java.io.IOException; -import java.util.ArrayList; -import java.util.IdentityHashMap; -import java.util.List; /** - * Tracks playlists linked to a provided playlist url. The provided url might reference an HLS - * master playlist or a media playlist. + * Tracks playlists associated to an HLS stream and provides snapshots. + * + *

The playlist tracker is responsible for exposing the seeking window, which is defined by the + * segments that one of the playlists exposes. This playlist is called primary and needs to be + * periodically refreshed in the case of live streams. Note that the primary playlist is one of the + * media playlists while the master playlist is an optional kind of playlist defined by the HLS + * specification (RFC 8216). + * + *

Playlist loads might encounter errors. The tracker may choose to blacklist them to ensure a + * primary playlist is always available. */ -public final class HlsPlaylistTracker implements Loader.Callback> { +public interface HlsPlaylistTracker { - /** - * Listener for primary playlist changes. - */ - public interface PrimaryPlaylistListener { + /** Factory for {@link HlsPlaylistTracker} instances. */ + interface Factory { + + /** + * Creates a new tracker instance. + * + * @param dataSourceFactory The {@link HlsDataSourceFactory} to use for playlist loading. + * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy} for playlist load errors. + * @param playlistParserFactory The {@link HlsPlaylistParserFactory} for playlist parsing. + */ + HlsPlaylistTracker createTracker( + HlsDataSourceFactory dataSourceFactory, + LoadErrorHandlingPolicy loadErrorHandlingPolicy, + HlsPlaylistParserFactory playlistParserFactory); + } + + /** Listener for primary playlist changes. */ + interface PrimaryPlaylistListener { /** * Called when the primary playlist changes. @@ -51,13 +62,10 @@ public final class HlsPlaylistTracker implements Loader.Callback playlistBundles; - private final Handler playlistRefreshHandler; - private final PrimaryPlaylistListener primaryPlaylistListener; - private final List listeners; - private final Loader initialPlaylistLoader; - private final EventDispatcher eventDispatcher; - - private HlsMasterPlaylist masterPlaylist; - private HlsUrl primaryHlsUrl; - private HlsMediaPlaylist primaryUrlSnapshot; - private boolean isLive; - - /** - * @param initialPlaylistUri Uri for the initial playlist of the stream. Can refer a media - * playlist or a master playlist. - * @param dataSourceFactory A factory for {@link DataSource} instances. - * @param eventDispatcher A dispatcher to notify of events. - * @param minRetryCount The minimum number of times the load must be retried before blacklisting a + * Starts the playlist tracker. + * + *

Must be called from the playback thread. A tracker may be restarted after a {@link #stop()} + * call. + * + * @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a master * playlist. - * @param primaryPlaylistListener A callback for the primary playlist change events. + * @param eventDispatcher A dispatcher to notify of events. + * @param listener A callback for the primary playlist change events. */ - public HlsPlaylistTracker(Uri initialPlaylistUri, HlsDataSourceFactory dataSourceFactory, - EventDispatcher eventDispatcher, int minRetryCount, - PrimaryPlaylistListener primaryPlaylistListener) { - this.initialPlaylistUri = initialPlaylistUri; - this.dataSourceFactory = dataSourceFactory; - this.eventDispatcher = eventDispatcher; - this.minRetryCount = minRetryCount; - this.primaryPlaylistListener = primaryPlaylistListener; - listeners = new ArrayList<>(); - initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist"); - playlistParser = new HlsPlaylistParser(); - playlistBundles = new IdentityHashMap<>(); - playlistRefreshHandler = new Handler(); - } + void start( + Uri initialPlaylistUri, EventDispatcher eventDispatcher, PrimaryPlaylistListener listener); + + /** + * Stops the playlist tracker and releases any acquired resources. + * + *

Must be called once per {@link #start} call. + */ + void stop(); /** * Registers a listener to receive events from the playlist tracker. * * @param listener The listener. */ - public void addListener(PlaylistEventListener listener) { - listeners.add(listener); - } + void addListener(PlaylistEventListener listener); /** * Unregisters a listener. * * @param listener The listener to unregister. */ - public void removeListener(PlaylistEventListener listener) { - listeners.remove(listener); - } - - /** - * Starts tracking all the playlists related to the provided Uri. - */ - public void start() { - ParsingLoadable masterPlaylistLoadable = new ParsingLoadable<>( - dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), initialPlaylistUri, - C.DATA_TYPE_MANIFEST, playlistParser); - initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount); - } + void removeListener(PlaylistEventListener listener); /** * Returns the master playlist. * + *

If the uri passed to {@link #start} points to a media playlist, an {@link HlsMasterPlaylist} + * with a single variant for said media playlist is returned. + * * @return The master playlist. Null if the initial playlist has yet to be loaded. */ - public HlsMasterPlaylist getMasterPlaylist() { - return masterPlaylist; - } + @Nullable + HlsMasterPlaylist getMasterPlaylist(); /** - * Returns the most recent snapshot available of the playlist referenced by the provided - * {@link HlsUrl}. + * Returns the most recent snapshot available of the playlist referenced by the provided {@link + * Uri}. * - * @param url The {@link HlsUrl} corresponding to the requested media playlist. - * @return The most recent snapshot of the playlist referenced by the provided {@link HlsUrl}. May - * be null if no snapshot has been loaded yet. + * @param url The {@link Uri} corresponding to the requested media playlist. + * @param isForPlayback Whether the caller might use the snapshot to request media segments for + * playback. If true, the primary playlist may be updated to the one requested. + * @return The most recent snapshot of the playlist referenced by the provided {@link Uri}. May be + * null if no snapshot has been loaded yet. */ - public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) { - HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot(); - if (snapshot != null) { - maybeSetPrimaryUrl(url); - } - return snapshot; - } + @Nullable + HlsMediaPlaylist getPlaylistSnapshot(Uri url, boolean isForPlayback); /** - * Returns whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is - * valid, meaning all the segments referenced by the playlist are expected to be available. If the + * Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no + * media playlist has been loaded. + */ + long getInitialStartTimeUs(); + + /** + * Returns whether the snapshot of the playlist referenced by the provided {@link Uri} is valid, + * meaning all the segments referenced by the playlist are expected to be available. If the * playlist is not valid then some of the segments may no longer be available. - - * @param url The {@link HlsUrl}. - * @return Whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is - * valid. + * + * @param url The {@link Uri}. + * @return Whether the snapshot of the playlist referenced by the provided {@link Uri} is valid. */ - public boolean isSnapshotValid(HlsUrl url) { - return playlistBundles.get(url).isSnapshotValid(); - } + boolean isSnapshotValid(Uri url); /** - * Releases the playlist tracker. - */ - public void release() { - initialPlaylistLoader.release(); - for (MediaPlaylistBundle bundle : playlistBundles.values()) { - bundle.release(); - } - playlistRefreshHandler.removeCallbacksAndMessages(null); - playlistBundles.clear(); - } - - /** - * If the tracker is having trouble refreshing the primary playlist or loading an irreplaceable - * playlist, this method throws the underlying error. Otherwise, does nothing. + * If the tracker is having trouble refreshing the master playlist or the primary playlist, this + * method throws the underlying error. Otherwise, does nothing. * * @throws IOException The underlying error. */ - public void maybeThrowPlaylistRefreshError() throws IOException { - initialPlaylistLoader.maybeThrowError(); - if (primaryHlsUrl != null) { - playlistBundles.get(primaryHlsUrl).mediaPlaylistLoader.maybeThrowError(); - } - } + void maybeThrowPrimaryPlaylistRefreshError() throws IOException; /** - * Triggers a playlist refresh and whitelists it. + * If the playlist is having trouble refreshing the playlist referenced by the given {@link Uri}, + * this method throws the underlying error. * - * @param url The {@link HlsUrl} of the playlist to be refreshed. + * @param url The {@link Uri}. + * @throws IOException The underyling error. */ - public void refreshPlaylist(HlsUrl url) { - playlistBundles.get(url).loadPlaylist(); - } + void maybeThrowPlaylistRefreshError(Uri url) throws IOException; /** - * Returns whether this is live content. + * Requests a playlist refresh and whitelists it. + * + *

The playlist tracker may choose the delay the playlist refresh. The request is discarded if + * a refresh was already pending. + * + * @param url The {@link Uri} of the playlist to be refreshed. + */ + void refreshPlaylist(Uri url); + + /** + * Returns whether the tracked playlists describe a live stream. * * @return True if the content is live. False otherwise. */ - public boolean isLive() { - return isLive; - } - - // Loader.Callback implementation. - - @Override - public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { - HlsPlaylist result = loadable.getResult(); - HlsMasterPlaylist masterPlaylist; - boolean isMediaPlaylist = result instanceof HlsMediaPlaylist; - if (isMediaPlaylist) { - masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri); - } else /* result instanceof HlsMasterPlaylist */ { - masterPlaylist = (HlsMasterPlaylist) result; - } - this.masterPlaylist = masterPlaylist; - primaryHlsUrl = masterPlaylist.variants.get(0); - ArrayList urls = new ArrayList<>(); - urls.addAll(masterPlaylist.variants); - urls.addAll(masterPlaylist.audios); - urls.addAll(masterPlaylist.subtitles); - createBundles(urls); - MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryHlsUrl); - if (isMediaPlaylist) { - // We don't need to load the playlist again. We can use the same result. - primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result); - } else { - primaryBundle.loadPlaylist(); - } - eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } - - @Override - public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } - - @Override - public int onLoadError(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, IOException error) { - boolean isFatal = error instanceof ParserException; - eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded(), error, isFatal); - return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; - } - - // Internal methods. - - private boolean maybeSelectNewPrimaryUrl() { - List variants = masterPlaylist.variants; - int variantsSize = variants.size(); - long currentTimeMs = SystemClock.elapsedRealtime(); - for (int i = 0; i < variantsSize; i++) { - MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i)); - if (currentTimeMs > bundle.blacklistUntilMs) { - primaryHlsUrl = bundle.playlistUrl; - bundle.loadPlaylist(); - return true; - } - } - return false; - } - - private void maybeSetPrimaryUrl(HlsUrl url) { - if (!masterPlaylist.variants.contains(url) - || (primaryUrlSnapshot != null && primaryUrlSnapshot.hasEndTag)) { - // Only allow variant urls to be chosen as primary. Also prevent changing the primary url if - // the last primary snapshot contains an end tag. - return; - } - MediaPlaylistBundle currentPrimaryBundle = playlistBundles.get(primaryHlsUrl); - long primarySnapshotAccessAgeMs = - currentPrimaryBundle.lastSnapshotAccessTimeMs - SystemClock.elapsedRealtime(); - if (primarySnapshotAccessAgeMs > PRIMARY_URL_KEEPALIVE_MS) { - primaryHlsUrl = url; - playlistBundles.get(primaryHlsUrl).loadPlaylist(); - } - } - - private void createBundles(List urls) { - int listSize = urls.size(); - long currentTimeMs = SystemClock.elapsedRealtime(); - for (int i = 0; i < listSize; i++) { - HlsUrl url = urls.get(i); - MediaPlaylistBundle bundle = new MediaPlaylistBundle(url, currentTimeMs); - playlistBundles.put(url, bundle); - } - } - - /** - * Called by the bundles when a snapshot changes. - * - * @param url The url of the playlist. - * @param newSnapshot The new snapshot. - * @return True if a refresh should be scheduled. - */ - private boolean onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) { - if (url == primaryHlsUrl) { - if (primaryUrlSnapshot == null) { - // This is the first primary url snapshot. - isLive = !newSnapshot.hasEndTag; - } - primaryUrlSnapshot = newSnapshot; - primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot); - } - int listenersSize = listeners.size(); - for (int i = 0; i < listenersSize; i++) { - listeners.get(i).onPlaylistChanged(); - } - // If the primary playlist is not the final one, we should schedule a refresh. - return url == primaryHlsUrl && !newSnapshot.hasEndTag; - } - - private void notifyPlaylistBlacklisting(HlsUrl url, long blacklistMs) { - int listenersSize = listeners.size(); - for (int i = 0; i < listenersSize; i++) { - listeners.get(i).onPlaylistBlacklisted(url, blacklistMs); - } - } - - private HlsMediaPlaylist getLatestPlaylistSnapshot(HlsMediaPlaylist oldPlaylist, - HlsMediaPlaylist loadedPlaylist) { - if (!loadedPlaylist.isNewerThan(oldPlaylist)) { - if (loadedPlaylist.hasEndTag) { - // If the loaded playlist has an end tag but is not newer than the old playlist then we have - // an inconsistent state. This is typically caused by the server incorrectly resetting the - // media sequence when appending the end tag. We resolve this case as best we can by - // returning the old playlist with the end tag appended. - return oldPlaylist.copyWithEndTag(); - } else { - return oldPlaylist; - } - } - long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist); - int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist); - return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence); - } - - private long getLoadedPlaylistStartTimeUs(HlsMediaPlaylist oldPlaylist, - HlsMediaPlaylist loadedPlaylist) { - if (loadedPlaylist.hasProgramDateTime) { - return loadedPlaylist.startTimeUs; - } - long primarySnapshotStartTimeUs = primaryUrlSnapshot != null - ? primaryUrlSnapshot.startTimeUs : 0; - if (oldPlaylist == null) { - return primarySnapshotStartTimeUs; - } - int oldPlaylistSize = oldPlaylist.segments.size(); - Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); - if (firstOldOverlappingSegment != null) { - return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs; - } else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) { - return oldPlaylist.getEndTimeUs(); - } else { - // No segments overlap, we assume the new playlist start coincides with the primary playlist. - return primarySnapshotStartTimeUs; - } - } - - private int getLoadedPlaylistDiscontinuitySequence(HlsMediaPlaylist oldPlaylist, - HlsMediaPlaylist loadedPlaylist) { - if (loadedPlaylist.hasDiscontinuitySequence) { - return loadedPlaylist.discontinuitySequence; - } - // TODO: Improve cross-playlist discontinuity adjustment. - int primaryUrlDiscontinuitySequence = primaryUrlSnapshot != null - ? primaryUrlSnapshot.discontinuitySequence : 0; - if (oldPlaylist == null) { - return primaryUrlDiscontinuitySequence; - } - Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); - if (firstOldOverlappingSegment != null) { - return oldPlaylist.discontinuitySequence - + firstOldOverlappingSegment.relativeDiscontinuitySequence - - loadedPlaylist.segments.get(0).relativeDiscontinuitySequence; - } - return primaryUrlDiscontinuitySequence; - } - - private static Segment getFirstOldOverlappingSegment(HlsMediaPlaylist oldPlaylist, - HlsMediaPlaylist loadedPlaylist) { - int mediaSequenceOffset = loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence; - List oldSegments = oldPlaylist.segments; - return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null; - } - - /** - * Holds all information related to a specific Media Playlist. - */ - private final class MediaPlaylistBundle implements Loader.Callback>, - Runnable { - - private final HlsUrl playlistUrl; - private final Loader mediaPlaylistLoader; - private final ParsingLoadable mediaPlaylistLoadable; - - private HlsMediaPlaylist playlistSnapshot; - private long lastSnapshotLoadMs; - private long lastSnapshotAccessTimeMs; - private long blacklistUntilMs; - private boolean pendingRefresh; - - public MediaPlaylistBundle(HlsUrl playlistUrl, long initialLastSnapshotAccessTimeMs) { - this.playlistUrl = playlistUrl; - lastSnapshotAccessTimeMs = initialLastSnapshotAccessTimeMs; - mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist"); - mediaPlaylistLoadable = new ParsingLoadable<>( - dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), - UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST, - playlistParser); - } - - public HlsMediaPlaylist getPlaylistSnapshot() { - lastSnapshotAccessTimeMs = SystemClock.elapsedRealtime(); - return playlistSnapshot; - } - - public boolean isSnapshotValid() { - if (playlistSnapshot == null) { - return false; - } - long currentTimeMs = SystemClock.elapsedRealtime(); - long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs)); - return playlistSnapshot.hasEndTag - || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT - || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD - || lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs; - } - - public void release() { - mediaPlaylistLoader.release(); - } - - public void loadPlaylist() { - blacklistUntilMs = 0; - if (!pendingRefresh && !mediaPlaylistLoader.isLoading()) { - mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount); - } - } - - // Loader.Callback implementation. - - @Override - public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { - HlsPlaylist result = loadable.getResult(); - if (result instanceof HlsMediaPlaylist) { - processLoadedPlaylist((HlsMediaPlaylist) result); - eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } else { - onLoadError(loadable, elapsedRealtimeMs, loadDurationMs, - new ParserException("Loaded playlist has unexpected type.")); - } - } - - @Override - public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } - - @Override - public int onLoadError(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, IOException error) { - boolean isFatal = error instanceof ParserException; - eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded(), error, isFatal); - if (isFatal) { - return Loader.DONT_RETRY_FATAL; - } - boolean shouldRetry = true; - if (ChunkedTrackBlacklistUtil.shouldBlacklist(error)) { - blacklistUntilMs = - SystemClock.elapsedRealtime() + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS; - notifyPlaylistBlacklisting(playlistUrl, - ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS); - shouldRetry = primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl(); - } - return shouldRetry ? Loader.RETRY : Loader.DONT_RETRY; - } - - // Runnable implementation. - - @Override - public void run() { - pendingRefresh = false; - loadPlaylist(); - } - - // Internal methods. - - private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) { - HlsMediaPlaylist oldPlaylist = playlistSnapshot; - lastSnapshotLoadMs = SystemClock.elapsedRealtime(); - playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist); - long refreshDelayUs = C.TIME_UNSET; - if (playlistSnapshot != oldPlaylist) { - if (onPlaylistUpdated(playlistUrl, playlistSnapshot)) { - refreshDelayUs = playlistSnapshot.targetDurationUs; - } - } else if (!playlistSnapshot.hasEndTag) { - refreshDelayUs = playlistSnapshot.targetDurationUs / 2; - } - if (refreshDelayUs != C.TIME_UNSET) { - // See HLS spec v20, section 6.3.4 for more information on media playlist refreshing. - pendingRefresh = playlistRefreshHandler.postDelayed(this, C.usToMs(refreshDelayUs)); - } - } - - } - + boolean isLive(); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/package-info.java new file mode 100644 index 000000000000..be9f862644da --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/CaptionStyleCompat.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/CaptionStyleCompat.java index b154d9ee64b8..c9acc1c8f558 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/CaptionStyleCompat.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/CaptionStyleCompat.java @@ -18,10 +18,12 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.text; import android.annotation.TargetApi; import android.graphics.Color; import android.graphics.Typeface; -import android.support.annotation.IntDef; import android.view.accessibility.CaptioningManager; import android.view.accessibility.CaptioningManager.CaptionStyle; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -31,11 +33,19 @@ import java.lang.annotation.RetentionPolicy; public final class CaptionStyleCompat { /** - * The type of edge, which may be none. + * The type of edge, which may be none. One of {@link #EDGE_TYPE_NONE}, {@link + * #EDGE_TYPE_OUTLINE}, {@link #EDGE_TYPE_DROP_SHADOW}, {@link #EDGE_TYPE_RAISED} or {@link + * #EDGE_TYPE_DEPRESSED}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({EDGE_TYPE_NONE, EDGE_TYPE_OUTLINE, EDGE_TYPE_DROP_SHADOW, EDGE_TYPE_RAISED, - EDGE_TYPE_DEPRESSED}) + @IntDef({ + EDGE_TYPE_NONE, + EDGE_TYPE_OUTLINE, + EDGE_TYPE_DROP_SHADOW, + EDGE_TYPE_RAISED, + EDGE_TYPE_DEPRESSED + }) public @interface EdgeType {} /** * Edge type value specifying no character edges. @@ -63,11 +73,15 @@ public final class CaptionStyleCompat { */ public static final int USE_TRACK_COLOR_SETTINGS = 1; - /** - * Default caption style. - */ - public static final CaptionStyleCompat DEFAULT = new CaptionStyleCompat( - Color.WHITE, Color.BLACK, Color.TRANSPARENT, EDGE_TYPE_NONE, Color.WHITE, null); + /** Default caption style. */ + public static final CaptionStyleCompat DEFAULT = + new CaptionStyleCompat( + Color.WHITE, + Color.BLACK, + Color.TRANSPARENT, + EDGE_TYPE_NONE, + Color.WHITE, + /* typeface= */ null); /** * The preferred foreground color. @@ -101,10 +115,8 @@ public final class CaptionStyleCompat { */ public final int edgeColor; - /** - * The preferred typeface. - */ - public final Typeface typeface; + /** The preferred typeface, or {@code null} if unspecified. */ + @Nullable public final Typeface typeface; /** * Creates a {@link CaptionStyleCompat} equivalent to a provided {@link CaptionStyle}. @@ -132,8 +144,13 @@ public final class CaptionStyleCompat { * @param edgeColor See {@link #edgeColor}. * @param typeface See {@link #typeface}. */ - public CaptionStyleCompat(int foregroundColor, int backgroundColor, int windowColor, - @EdgeType int edgeType, int edgeColor, Typeface typeface) { + public CaptionStyleCompat( + int foregroundColor, + int backgroundColor, + int windowColor, + @EdgeType int edgeType, + int edgeColor, + @Nullable Typeface typeface) { this.foregroundColor = foregroundColor; this.backgroundColor = backgroundColor; this.windowColor = windowColor; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/Cue.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/Cue.java index 1309d2b61b03..71627781c1a0 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/Cue.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/Cue.java @@ -17,8 +17,10 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.text; import android.graphics.Bitmap; import android.graphics.Color; -import android.support.annotation.IntDef; import android.text.Layout.Alignment; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -27,14 +29,18 @@ import java.lang.annotation.RetentionPolicy; */ public class Cue { - /** - * An unset position or width. - */ - public static final float DIMEN_UNSET = Float.MIN_VALUE; + /** The empty cue. */ + public static final Cue EMPTY = new Cue(""); + + /** An unset position, width or size. */ + // Note: We deliberately don't use Float.MIN_VALUE because it's positive & very close to zero. + public static final float DIMEN_UNSET = -Float.MAX_VALUE; /** - * The type of anchor, which may be unset. + * The type of anchor, which may be unset. One of {@link #TYPE_UNSET}, {@link #ANCHOR_TYPE_START}, + * {@link #ANCHOR_TYPE_MIDDLE} or {@link #ANCHOR_TYPE_END}. */ + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({TYPE_UNSET, ANCHOR_TYPE_START, ANCHOR_TYPE_MIDDLE, ANCHOR_TYPE_END}) public @interface AnchorType {} @@ -62,8 +68,10 @@ public class Cue { public static final int ANCHOR_TYPE_END = 2; /** - * The type of line, which may be unset. + * The type of line, which may be unset. One of {@link #TYPE_UNSET}, {@link #LINE_TYPE_FRACTION} + * or {@link #LINE_TYPE_NUMBER}. */ + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({TYPE_UNSET, LINE_TYPE_FRACTION, LINE_TYPE_NUMBER}) public @interface LineType {} @@ -78,21 +86,41 @@ public class Cue { */ public static final int LINE_TYPE_NUMBER = 1; + /** + * The type of default text size for this cue, which may be unset. One of {@link #TYPE_UNSET}, + * {@link #TEXT_SIZE_TYPE_FRACTIONAL}, {@link #TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING} or {@link + * #TEXT_SIZE_TYPE_ABSOLUTE}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + TYPE_UNSET, + TEXT_SIZE_TYPE_FRACTIONAL, + TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING, + TEXT_SIZE_TYPE_ABSOLUTE + }) + public @interface TextSizeType {} + + /** Text size is measured as a fraction of the viewport size minus the view padding. */ + public static final int TEXT_SIZE_TYPE_FRACTIONAL = 0; + + /** Text size is measured as a fraction of the viewport size, ignoring the view padding */ + public static final int TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING = 1; + + /** Text size is measured in number of pixels. */ + public static final int TEXT_SIZE_TYPE_ABSOLUTE = 2; + /** * The cue text, or null if this is an image cue. Note the {@link CharSequence} may be decorated * with styling spans. */ - public final CharSequence text; + @Nullable public final CharSequence text; - /** - * The alignment of the cue text within the cue box, or null if the alignment is undefined. - */ - public final Alignment textAlignment; + /** The alignment of the cue text within the cue box, or null if the alignment is undefined. */ + @Nullable public final Alignment textAlignment; - /** - * The cue image, or null if this is a text cue. - */ - public final Bitmap bitmap; + /** The cue image, or null if this is a text cue. */ + @Nullable public final Bitmap bitmap; /** * The position of the {@link #lineAnchor} of the cue box within the viewport in the direction @@ -106,40 +134,39 @@ public class Cue { /** * The type of the {@link #line} value. - *

- * {@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the + * + *

{@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the * viewport. - *

- * {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of each - * line is taken to be the size of the first line of the cue. When {@link #line} is greater than - * or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset from - * the start edge. When {@link #line} is negative lines count from the end of the viewport, with - * -1 indicating zero offset from the end edge. For horizontal text the line spacing is the height - * of the first line of the cue, and the start and end of the viewport are the top and bottom - * respectively. - *

- * Note that it's particularly important to consider the effect of {@link #lineAnchor} when using - * {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} positions a - * (potentially multi-line) cue at the very top of the viewport. - * {@code (line == -1 && lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue - * at the very bottom of the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} - * and {@code (line == -1 && lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of - * the viewport. {@code (line == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only - * the last line is visible at the top of the viewport. - * {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a cue so that only its first - * line is visible at the bottom of the viewport. + * + *

{@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of + * each line is taken to be the size of the first line of the cue. When {@link #line} is greater + * than or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset + * from the start edge. When {@link #line} is negative lines count from the end of the viewport, + * with -1 indicating zero offset from the end edge. For horizontal text the line spacing is the + * height of the first line of the cue, and the start and end of the viewport are the top and + * bottom respectively. + * + *

Note that it's particularly important to consider the effect of {@link #lineAnchor} when + * using {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} + * positions a (potentially multi-line) cue at the very top of the viewport. {@code (line == -1 && + * lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue at the very bottom of + * the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} and {@code (line == -1 && + * lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of the viewport. {@code (line + * == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only the last line is visible + * at the top of the viewport. {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a + * cue so that only its first line is visible at the bottom of the viewport. */ - @LineType public final int lineType; + public final @LineType int lineType; /** - * The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START}, - * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. - *

- * For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE} - * and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box - * respectively. + * The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + * + *

For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of + * the cue box respectively. */ - @AnchorType public final int lineAnchor; + public final @AnchorType int lineAnchor; /** * The fractional position of the {@link #positionAnchor} of the cue box within the viewport in @@ -152,14 +179,14 @@ public class Cue { public final float position; /** - * The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START}, - * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. - *

- * For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE} - * and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box - * respectively. + * The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + * + *

For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of + * the cue box respectively. */ - @AnchorType public final int positionAnchor; + public final @AnchorType int positionAnchor; /** * The size of the cue box in the writing direction specified as a fraction of the viewport size @@ -184,6 +211,18 @@ public class Cue { */ public final int windowColor; + /** + * The default text size type for this cue's text, or {@link #TYPE_UNSET} if this cue has no + * default text size. + */ + public final @TextSizeType int textSizeType; + + /** + * The default text size for this cue's text, or {@link #DIMEN_UNSET} if this cue has no default + * text size. + */ + public final float textSize; + /** * Creates an image cue. * @@ -194,17 +233,36 @@ public class Cue { * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. * @param verticalPosition The position of the vertical anchor within the viewport, expressed as a * fraction of the viewport height. - * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, - * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. * @param width The width of the cue as a fraction of the viewport width. - * @param height The height of the cue as a fraction of the viewport height, or - * {@link #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the - * specified {@code width}. + * @param height The height of the cue as a fraction of the viewport height, or {@link + * #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the specified + * {@code width}. */ - public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor, - float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float height) { - this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor, - horizontalPosition, horizontalPositionAnchor, width, height, false, Color.BLACK); + public Cue( + Bitmap bitmap, + float horizontalPosition, + @AnchorType int horizontalPositionAnchor, + float verticalPosition, + @AnchorType int verticalPositionAnchor, + float width, + float height) { + this( + /* text= */ null, + /* textAlignment= */ null, + bitmap, + verticalPosition, + /* lineType= */ LINE_TYPE_FRACTION, + verticalPositionAnchor, + horizontalPosition, + horizontalPositionAnchor, + /* textSizeType= */ TYPE_UNSET, + /* textSize= */ DIMEN_UNSET, + width, + height, + /* windowColorSet= */ false, + /* windowColor= */ Color.BLACK); } /** @@ -214,7 +272,15 @@ public class Cue { * @param text See {@link #text}. */ public Cue(CharSequence text) { - this(text, null, DIMEN_UNSET, TYPE_UNSET, TYPE_UNSET, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET); + this( + text, + /* textAlignment= */ null, + /* line= */ DIMEN_UNSET, + /* lineType= */ TYPE_UNSET, + /* lineAnchor= */ TYPE_UNSET, + /* position= */ DIMEN_UNSET, + /* positionAnchor= */ TYPE_UNSET, + /* size= */ DIMEN_UNSET); } /** @@ -229,10 +295,68 @@ public class Cue { * @param positionAnchor See {@link #positionAnchor}. * @param size See {@link #size}. */ - public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, - @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size) { - this(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, false, - Color.BLACK); + public Cue( + CharSequence text, + @Nullable Alignment textAlignment, + float line, + @LineType int lineType, + @AnchorType int lineAnchor, + float position, + @AnchorType int positionAnchor, + float size) { + this( + text, + textAlignment, + line, + lineType, + lineAnchor, + position, + positionAnchor, + size, + /* windowColorSet= */ false, + /* windowColor= */ Color.BLACK); + } + + /** + * Creates a text cue. + * + * @param text See {@link #text}. + * @param textAlignment See {@link #textAlignment}. + * @param line See {@link #line}. + * @param lineType See {@link #lineType}. + * @param lineAnchor See {@link #lineAnchor}. + * @param position See {@link #position}. + * @param positionAnchor See {@link #positionAnchor}. + * @param size See {@link #size}. + * @param textSizeType See {@link #textSizeType}. + * @param textSize See {@link #textSize}. + */ + public Cue( + CharSequence text, + @Nullable Alignment textAlignment, + float line, + @LineType int lineType, + @AnchorType int lineAnchor, + float position, + @AnchorType int positionAnchor, + float size, + @TextSizeType int textSizeType, + float textSize) { + this( + text, + textAlignment, + /* bitmap= */ null, + line, + lineType, + lineAnchor, + position, + positionAnchor, + textSizeType, + textSize, + size, + /* bitmapHeight= */ DIMEN_UNSET, + /* windowColorSet= */ false, + /* windowColor= */ Color.BLACK); } /** @@ -249,16 +373,48 @@ public class Cue { * @param windowColorSet See {@link #windowColorSet}. * @param windowColor See {@link #windowColor}. */ - public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, - @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, - boolean windowColorSet, int windowColor) { - this(text, textAlignment, null, line, lineType, lineAnchor, position, positionAnchor, size, - DIMEN_UNSET, windowColorSet, windowColor); + public Cue( + CharSequence text, + @Nullable Alignment textAlignment, + float line, + @LineType int lineType, + @AnchorType int lineAnchor, + float position, + @AnchorType int positionAnchor, + float size, + boolean windowColorSet, + int windowColor) { + this( + text, + textAlignment, + /* bitmap= */ null, + line, + lineType, + lineAnchor, + position, + positionAnchor, + /* textSizeType= */ TYPE_UNSET, + /* textSize= */ DIMEN_UNSET, + size, + /* bitmapHeight= */ DIMEN_UNSET, + windowColorSet, + windowColor); } - private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float line, - @LineType int lineType, @AnchorType int lineAnchor, float position, - @AnchorType int positionAnchor, float size, float bitmapHeight, boolean windowColorSet, + private Cue( + @Nullable CharSequence text, + @Nullable Alignment textAlignment, + @Nullable Bitmap bitmap, + float line, + @LineType int lineType, + @AnchorType int lineAnchor, + float position, + @AnchorType int positionAnchor, + @TextSizeType int textSizeType, + float textSize, + float size, + float bitmapHeight, + boolean windowColorSet, int windowColor) { this.text = text; this.textAlignment = textAlignment; @@ -272,6 +428,8 @@ public class Cue { this.bitmapHeight = bitmapHeight; this.windowColorSet = windowColorSet; this.windowColor = windowColor; + this.textSizeType = textSizeType; + this.textSize = textSize; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java index 1c5444f213e1..b58bb1daea7c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java @@ -15,8 +15,10 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.text; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.SimpleDecoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.nio.ByteBuffer; /** @@ -28,9 +30,8 @@ public abstract class SimpleSubtitleDecoder extends private final String name; - /** - * @param name The name of the decoder. - */ + /** @param name The name of the decoder. */ + @SuppressWarnings("initialization:method.invocation.invalid") protected SimpleSubtitleDecoder(String name) { super(new SubtitleInputBuffer[2], new SubtitleOutputBuffer[2]); this.name = name; @@ -57,16 +58,23 @@ public abstract class SimpleSubtitleDecoder extends return new SimpleSubtitleOutputBuffer(this); } + @Override + protected final SubtitleDecoderException createUnexpectedDecodeException(Throwable error) { + return new SubtitleDecoderException("Unexpected decode error", error); + } + @Override protected final void releaseOutputBuffer(SubtitleOutputBuffer buffer) { super.releaseOutputBuffer(buffer); } + @SuppressWarnings("ByteBufferBackingArray") @Override - protected final SubtitleDecoderException decode(SubtitleInputBuffer inputBuffer, - SubtitleOutputBuffer outputBuffer, boolean reset) { + @Nullable + protected final SubtitleDecoderException decode( + SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) { try { - ByteBuffer inputData = inputBuffer.data; + ByteBuffer inputData = Assertions.checkNotNull(inputBuffer.data); Subtitle subtitle = decode(inputData.array(), inputData.limit(), reset); outputBuffer.setContent(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs); // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]). diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderException.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderException.java index b91b020053a5..9ee15188b047 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderException.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderException.java @@ -27,6 +27,11 @@ public class SubtitleDecoderException extends Exception { super(message); } + /** @param cause The cause of this exception. */ + public SubtitleDecoderException(Exception cause) { + super(cause); + } + /** * @param message The detail message for this exception. * @param cause The cause of this exception. diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index fa6bef33ad55..2fb0200f0dc8 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -15,10 +15,13 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.text; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea.Cea608Decoder; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea.Cea708Decoder; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.dvb.DvbDecoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.pgs.PgsDecoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.ssa.SsaDecoder; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.subrip.SubripDecoder; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.ttml.TtmlDecoder; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.tx3g.Tx3gDecoder; @@ -51,60 +54,73 @@ public interface SubtitleDecoderFactory { /** * Default {@link SubtitleDecoderFactory} implementation. - *

- * The formats supported by this factory are: + * + *

The formats supported by this factory are: + * *

    - *
  • WebVTT ({@link WebvttDecoder})
  • - *
  • WebVTT (MP4) ({@link Mp4WebvttDecoder})
  • - *
  • TTML ({@link TtmlDecoder})
  • - *
  • SubRip ({@link SubripDecoder})
  • - *
  • TX3G ({@link Tx3gDecoder})
  • - *
  • Cea608 ({@link Cea608Decoder})
  • - *
  • Cea708 ({@link Cea708Decoder})
  • - *
  • DVB ({@link DvbDecoder})
  • + *
  • WebVTT ({@link WebvttDecoder}) + *
  • WebVTT (MP4) ({@link Mp4WebvttDecoder}) + *
  • TTML ({@link TtmlDecoder}) + *
  • SubRip ({@link SubripDecoder}) + *
  • SSA/ASS ({@link SsaDecoder}) + *
  • TX3G ({@link Tx3gDecoder}) + *
  • Cea608 ({@link Cea608Decoder}) + *
  • Cea708 ({@link Cea708Decoder}) + *
  • DVB ({@link DvbDecoder}) + *
  • PGS ({@link PgsDecoder}) *
*/ - SubtitleDecoderFactory DEFAULT = new SubtitleDecoderFactory() { + SubtitleDecoderFactory DEFAULT = + new SubtitleDecoderFactory() { - @Override - public boolean supportsFormat(Format format) { - String mimeType = format.sampleMimeType; - return MimeTypes.TEXT_VTT.equals(mimeType) - || MimeTypes.APPLICATION_TTML.equals(mimeType) - || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) - || MimeTypes.APPLICATION_SUBRIP.equals(mimeType) - || MimeTypes.APPLICATION_TX3G.equals(mimeType) - || MimeTypes.APPLICATION_CEA608.equals(mimeType) - || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) - || MimeTypes.APPLICATION_CEA708.equals(mimeType) - || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType); - } - - @Override - public SubtitleDecoder createDecoder(Format format) { - switch (format.sampleMimeType) { - case MimeTypes.TEXT_VTT: - return new WebvttDecoder(); - case MimeTypes.APPLICATION_MP4VTT: - return new Mp4WebvttDecoder(); - case MimeTypes.APPLICATION_TTML: - return new TtmlDecoder(); - case MimeTypes.APPLICATION_SUBRIP: - return new SubripDecoder(); - case MimeTypes.APPLICATION_TX3G: - return new Tx3gDecoder(format.initializationData); - case MimeTypes.APPLICATION_CEA608: - case MimeTypes.APPLICATION_MP4CEA608: - return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel); - case MimeTypes.APPLICATION_CEA708: - return new Cea708Decoder(format.accessibilityChannel); - case MimeTypes.APPLICATION_DVBSUBS: - return new DvbDecoder(format.initializationData); - default: - throw new IllegalArgumentException("Attempted to create decoder for unsupported format"); - } - } - - }; + @Override + public boolean supportsFormat(Format format) { + @Nullable String mimeType = format.sampleMimeType; + return MimeTypes.TEXT_VTT.equals(mimeType) + || MimeTypes.TEXT_SSA.equals(mimeType) + || MimeTypes.APPLICATION_TTML.equals(mimeType) + || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) + || MimeTypes.APPLICATION_SUBRIP.equals(mimeType) + || MimeTypes.APPLICATION_TX3G.equals(mimeType) + || MimeTypes.APPLICATION_CEA608.equals(mimeType) + || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) + || MimeTypes.APPLICATION_CEA708.equals(mimeType) + || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType) + || MimeTypes.APPLICATION_PGS.equals(mimeType); + } + @Override + public SubtitleDecoder createDecoder(Format format) { + @Nullable String mimeType = format.sampleMimeType; + if (mimeType != null) { + switch (mimeType) { + case MimeTypes.TEXT_VTT: + return new WebvttDecoder(); + case MimeTypes.TEXT_SSA: + return new SsaDecoder(format.initializationData); + case MimeTypes.APPLICATION_MP4VTT: + return new Mp4WebvttDecoder(); + case MimeTypes.APPLICATION_TTML: + return new TtmlDecoder(); + case MimeTypes.APPLICATION_SUBRIP: + return new SubripDecoder(); + case MimeTypes.APPLICATION_TX3G: + return new Tx3gDecoder(format.initializationData); + case MimeTypes.APPLICATION_CEA608: + case MimeTypes.APPLICATION_MP4CEA608: + return new Cea608Decoder(mimeType, format.accessibilityChannel); + case MimeTypes.APPLICATION_CEA708: + return new Cea708Decoder(format.accessibilityChannel, format.initializationData); + case MimeTypes.APPLICATION_DVBSUBS: + return new DvbDecoder(format.initializationData); + case MimeTypes.APPLICATION_PGS: + return new PgsDecoder(); + default: + break; + } + } + throw new IllegalArgumentException( + "Attempted to create decoder for unsupported MIME type: " + mimeType); + } + }; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleInputBuffer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleInputBuffer.java index 5cb2771fac2f..dbcfe649b8d7 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleInputBuffer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleInputBuffer.java @@ -15,15 +15,11 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.text; -import android.support.annotation.NonNull; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; -/** - * A {@link DecoderInputBuffer} for a {@link SubtitleDecoder}. - */ -public final class SubtitleInputBuffer extends DecoderInputBuffer - implements Comparable { +/** A {@link DecoderInputBuffer} for a {@link SubtitleDecoder}. */ +public class SubtitleInputBuffer extends DecoderInputBuffer { /** * An offset that must be added to the subtitle's event times after it's been decoded, or @@ -35,13 +31,4 @@ public final class SubtitleInputBuffer extends DecoderInputBuffer super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); } - @Override - public int compareTo(@NonNull SubtitleInputBuffer other) { - long delta = timeUs - other.timeUs; - if (delta == 0) { - return 0; - } - return delta > 0 ? 1 : -1; - } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java index e5decf43a420..9cc7671b2415 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java @@ -15,8 +15,10 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.text; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.OutputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.util.List; /** @@ -24,7 +26,7 @@ import java.util.List; */ public abstract class SubtitleOutputBuffer extends OutputBuffer implements Subtitle { - private Subtitle subtitle; + @Nullable private Subtitle subtitle; private long subsampleOffsetUs; /** @@ -45,22 +47,22 @@ public abstract class SubtitleOutputBuffer extends OutputBuffer implements Subti @Override public int getEventTimeCount() { - return subtitle.getEventTimeCount(); + return Assertions.checkNotNull(subtitle).getEventTimeCount(); } @Override public long getEventTime(int index) { - return subtitle.getEventTime(index) + subsampleOffsetUs; + return Assertions.checkNotNull(subtitle).getEventTime(index) + subsampleOffsetUs; } @Override public int getNextEventTimeIndex(long timeUs) { - return subtitle.getNextEventTimeIndex(timeUs - subsampleOffsetUs); + return Assertions.checkNotNull(subtitle).getNextEventTimeIndex(timeUs - subsampleOffsetUs); } @Override public List getCues(long timeUs) { - return subtitle.getCues(timeUs - subsampleOffsetUs); + return Assertions.checkNotNull(subtitle).getCues(timeUs - subsampleOffsetUs); } @Override diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/TextOutput.java similarity index 55% rename from mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java rename to mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/TextOutput.java index f5eb1602b3bb..b15a2f1b35c9 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/TextOutput.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2017 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. @@ -13,28 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea; +package org.mozilla.thirdparty.com.google.android.exoplayer2.text; -import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleOutputBuffer; +import java.util.List; /** - * A {@link SubtitleOutputBuffer} for {@link CeaDecoder}s. + * Receives text output. */ -public final class CeaOutputBuffer extends SubtitleOutputBuffer { - - private final CeaDecoder owner; +public interface TextOutput { /** - * @param owner The decoder that owns this buffer. + * Called when there is a change in the {@link Cue}s. + * + * @param cues The {@link Cue}s. May be empty. */ - public CeaOutputBuffer(CeaDecoder owner) { - super(); - this.owner = owner; - } - - @Override - public final void release() { - owner.releaseOutputBuffer(this); - } - + void onCues(List cues); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/TextRenderer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/TextRenderer.java index aa924346e94e..428b106fcd93 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/TextRenderer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/TextRenderer.java @@ -19,14 +19,18 @@ import android.os.Handler; import android.os.Handler.Callback; import android.os.Looper; import android.os.Message; -import android.support.annotation.IntDef; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.BaseRenderer; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; -import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; @@ -37,27 +41,19 @@ import java.util.List; *

* {@link Subtitle}s are decoded from sample data using {@link SubtitleDecoder} instances obtained * from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s is - * delegated to an {@link Output}. + * delegated to a {@link TextOutput}. */ public final class TextRenderer extends BaseRenderer implements Callback { - /** - * Receives output from a {@link TextRenderer}. - */ - public interface Output { - - /** - * Called each time there is a change in the {@link Cue}s. - * - * @param cues The {@link Cue}s. - */ - void onCues(List cues); - - } + private static final String TAG = "TextRenderer"; + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({REPLACEMENT_STATE_NONE, REPLACEMENT_STATE_SIGNAL_END_OF_STREAM, - REPLACEMENT_STATE_WAIT_END_OF_STREAM}) + @IntDef({ + REPLACEMENT_STATE_NONE, + REPLACEMENT_STATE_SIGNAL_END_OF_STREAM, + REPLACEMENT_STATE_WAIT_END_OF_STREAM + }) private @interface ReplacementState {} /** * The decoder does not need to be replaced. @@ -78,59 +74,67 @@ public final class TextRenderer extends BaseRenderer implements Callback { private static final int MSG_UPDATE_OUTPUT = 0; - private final Handler outputHandler; - private final Output output; + @Nullable private final Handler outputHandler; + private final TextOutput output; private final SubtitleDecoderFactory decoderFactory; private final FormatHolder formatHolder; private boolean inputStreamEnded; private boolean outputStreamEnded; @ReplacementState private int decoderReplacementState; - private Format streamFormat; - private SubtitleDecoder decoder; - private SubtitleInputBuffer nextInputBuffer; - private SubtitleOutputBuffer subtitle; - private SubtitleOutputBuffer nextSubtitle; + @Nullable private Format streamFormat; + @Nullable private SubtitleDecoder decoder; + @Nullable private SubtitleInputBuffer nextInputBuffer; + @Nullable private SubtitleOutputBuffer subtitle; + @Nullable private SubtitleOutputBuffer nextSubtitle; private int nextSubtitleEventIndex; /** * @param output The output. - * @param outputLooper The looper associated with the thread on which the output should be - * called. If the output makes use of standard Android UI components, then this should - * normally be the looper associated with the application's main thread, which can be obtained - * using {@link android.app.Activity#getMainLooper()}. Null may be passed if the output - * should be called directly on the player's internal rendering thread. + * @param outputLooper The looper associated with the thread on which the output should be called. + * If the output makes use of standard Android UI components, then this should normally be the + * looper associated with the application's main thread, which can be obtained using {@link + * android.app.Activity#getMainLooper()}. Null may be passed if the output should be called + * directly on the player's internal rendering thread. */ - public TextRenderer(Output output, Looper outputLooper) { + public TextRenderer(TextOutput output, @Nullable Looper outputLooper) { this(output, outputLooper, SubtitleDecoderFactory.DEFAULT); } /** * @param output The output. - * @param outputLooper The looper associated with the thread on which the output should be - * called. If the output makes use of standard Android UI components, then this should - * normally be the looper associated with the application's main thread, which can be obtained - * using {@link android.app.Activity#getMainLooper()}. Null may be passed if the output - * should be called directly on the player's internal rendering thread. + * @param outputLooper The looper associated with the thread on which the output should be called. + * If the output makes use of standard Android UI components, then this should normally be the + * looper associated with the application's main thread, which can be obtained using {@link + * android.app.Activity#getMainLooper()}. Null may be passed if the output should be called + * directly on the player's internal rendering thread. * @param decoderFactory A factory from which to obtain {@link SubtitleDecoder} instances. */ - public TextRenderer(Output output, Looper outputLooper, SubtitleDecoderFactory decoderFactory) { + public TextRenderer( + TextOutput output, @Nullable Looper outputLooper, SubtitleDecoderFactory decoderFactory) { super(C.TRACK_TYPE_TEXT); this.output = Assertions.checkNotNull(output); - this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this); + this.outputHandler = + outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this); this.decoderFactory = decoderFactory; formatHolder = new FormatHolder(); } @Override + @Capabilities public int supportsFormat(Format format) { - return decoderFactory.supportsFormat(format) ? FORMAT_HANDLED - : (MimeTypes.isText(format.sampleMimeType) ? FORMAT_UNSUPPORTED_SUBTYPE - : FORMAT_UNSUPPORTED_TYPE); + if (decoderFactory.supportsFormat(format)) { + return RendererCapabilities.create( + supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM); + } else if (MimeTypes.isText(format.sampleMimeType)) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); + } else { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); + } } @Override - protected void onStreamChanged(Format[] formats) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) { streamFormat = formats[0]; if (decoder != null) { decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM; @@ -141,19 +145,13 @@ public final class TextRenderer extends BaseRenderer implements Callback { @Override protected void onPositionReset(long positionUs, boolean joining) { - clearOutput(); inputStreamEnded = false; outputStreamEnded = false; - if (decoderReplacementState != REPLACEMENT_STATE_NONE) { - replaceDecoder(); - } else { - releaseBuffers(); - decoder.flush(); - } + resetOutputAndDecoder(); } @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + public void render(long positionUs, long elapsedRealtimeUs) { if (outputStreamEnded) { return; } @@ -163,7 +161,8 @@ public final class TextRenderer extends BaseRenderer implements Callback { try { nextSubtitle = decoder.dequeueOutputBuffer(); } catch (SubtitleDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + handleDecoderError(e); + return; } } @@ -245,7 +244,8 @@ public final class TextRenderer extends BaseRenderer implements Callback { } } } catch (SubtitleDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + handleDecoderError(e); + return; } } @@ -254,7 +254,6 @@ public final class TextRenderer extends BaseRenderer implements Callback { streamFormat = null; clearOutput(); releaseDecoder(); - super.onDisabled(); } @Override @@ -295,9 +294,9 @@ public final class TextRenderer extends BaseRenderer implements Callback { } private long getNextEventTime() { - return ((nextSubtitleEventIndex == C.INDEX_UNSET) - || (nextSubtitleEventIndex >= subtitle.getEventTimeCount())) ? Long.MAX_VALUE - : (subtitle.getEventTime(nextSubtitleEventIndex)); + return nextSubtitleEventIndex == C.INDEX_UNSET + || nextSubtitleEventIndex >= subtitle.getEventTimeCount() + ? Long.MAX_VALUE : subtitle.getEventTime(nextSubtitleEventIndex); } private void updateOutput(List cues) { @@ -309,7 +308,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { } private void clearOutput() { - updateOutput(Collections.emptyList()); + updateOutput(Collections.emptyList()); } @SuppressWarnings("unchecked") @@ -328,4 +327,24 @@ public final class TextRenderer extends BaseRenderer implements Callback { output.onCues(cues); } + /** + * Called when {@link #decoder} throws an exception, so it can be logged and playback can + * continue. + * + *

Logs {@code e} and resets state to allow decoding the next sample. + */ + private void handleDecoderError(SubtitleDecoderException e) { + Log.e(TAG, "Subtitle decoding failed. streamFormat=" + streamFormat, e); + resetOutputAndDecoder(); + } + + private void resetOutputAndDecoder() { + clearOutput(); + if (decoderReplacementState != REPLACEMENT_STATE_NONE) { + replaceDecoder(); + } else { + releaseBuffers(); + decoder.flush(); + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index fea57a1e6d8d..320b4f3f07c3 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -21,19 +21,19 @@ import android.text.Layout.Alignment; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; -import android.text.style.CharacterStyle; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; -import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleDecoder; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleInputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; -import java.util.LinkedList; +import java.util.Collections; import java.util.List; /** @@ -41,13 +41,16 @@ import java.util.List; */ public final class Cea608Decoder extends CeaDecoder { + private static final String TAG = "Cea608Decoder"; + private static final int CC_VALID_FLAG = 0x04; private static final int CC_TYPE_FLAG = 0x02; private static final int CC_FIELD_FLAG = 0x01; private static final int NTSC_CC_FIELD_1 = 0x00; private static final int NTSC_CC_FIELD_2 = 0x01; - private static final int CC_VALID_608_ID = 0x04; + private static final int NTSC_CC_CHANNEL_1 = 0x00; + private static final int NTSC_CC_CHANNEL_2 = 0x01; private static final int CC_MODE_UNKNOWN = 0; private static final int CC_MODE_ROLL_UP = 1; @@ -56,15 +59,13 @@ public final class Cea608Decoder extends CeaDecoder { private static final int[] ROW_INDICES = new int[] {11, 1, 3, 12, 14, 5, 7, 9}; private static final int[] COLUMN_INDICES = new int[] {0, 4, 8, 12, 16, 20, 24, 28}; - private static final int[] COLORS = new int[] { - Color.WHITE, - Color.GREEN, - Color.BLUE, - Color.CYAN, - Color.RED, - Color.YELLOW, - Color.MAGENTA, - }; + + private static final int[] STYLE_COLORS = + new int[] { + Color.WHITE, Color.GREEN, Color.BLUE, Color.CYAN, Color.RED, Color.YELLOW, Color.MAGENTA + }; + private static final int STYLE_ITALICS = 0x07; + private static final int STYLE_UNCHANGED = 0x08; // The default number of rows to display in roll-up captions mode. private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4; @@ -79,6 +80,11 @@ public final class Cea608Decoder extends CeaDecoder { * at which point the non-displayed memory becomes the displayed memory (and vice versa). */ private static final byte CTRL_RESUME_CAPTION_LOADING = 0x20; + + private static final byte CTRL_BACKSPACE = 0x21; + + private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24; + /** * Command initiating roll-up style captioning, with the maximum of 2 rows displayed * simultaneously. @@ -94,25 +100,31 @@ public final class Cea608Decoder extends CeaDecoder { * simultaneously. */ private static final byte CTRL_ROLL_UP_CAPTIONS_4_ROWS = 0x27; + /** * Command initiating paint-on style captioning. Subsequent data should be addressed immediately * to displayed memory without need for the {@link #CTRL_RESUME_CAPTION_LOADING} command. */ private static final byte CTRL_RESUME_DIRECT_CAPTIONING = 0x29; /** - * Command indicating the end of a pop-on style caption. At this point the caption loaded in - * non-displayed memory should be swapped with the one in displayed memory. If no - * {@link #CTRL_RESUME_CAPTION_LOADING} command has been received, this command forces the - * receiver into pop-on style. + * TEXT commands are switching to TEXT service. All consecutive incoming data must be filtered out + * until a command is received that switches back to the CAPTION service. */ - private static final byte CTRL_END_OF_CAPTION = 0x2F; + private static final byte CTRL_TEXT_RESTART = 0x2A; + + private static final byte CTRL_RESUME_TEXT_DISPLAY = 0x2B; private static final byte CTRL_ERASE_DISPLAYED_MEMORY = 0x2C; private static final byte CTRL_CARRIAGE_RETURN = 0x2D; private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E; - private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24; - private static final byte CTRL_BACKSPACE = 0x21; + /** + * Command indicating the end of a pop-on style caption. At this point the caption loaded in + * non-displayed memory should be swapped with the one in displayed memory. If no {@link + * #CTRL_RESUME_CAPTION_LOADING} command has been received, this command forces the receiver into + * pop-on style. + */ + private static final byte CTRL_END_OF_CAPTION = 0x2F; // Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20). private static final int[] BASIC_CHARACTER_SET = new int[] { @@ -182,10 +194,46 @@ public final class Cea608Decoder extends CeaDecoder { 0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518 }; + private static final boolean[] ODD_PARITY_BYTE_TABLE = { + false, true, true, false, true, false, false, true, // 0 + true, false, false, true, false, true, true, false, // 8 + true, false, false, true, false, true, true, false, // 16 + false, true, true, false, true, false, false, true, // 24 + true, false, false, true, false, true, true, false, // 32 + false, true, true, false, true, false, false, true, // 40 + false, true, true, false, true, false, false, true, // 48 + true, false, false, true, false, true, true, false, // 56 + true, false, false, true, false, true, true, false, // 64 + false, true, true, false, true, false, false, true, // 72 + false, true, true, false, true, false, false, true, // 80 + true, false, false, true, false, true, true, false, // 88 + false, true, true, false, true, false, false, true, // 96 + true, false, false, true, false, true, true, false, // 104 + true, false, false, true, false, true, true, false, // 112 + false, true, true, false, true, false, false, true, // 120 + true, false, false, true, false, true, true, false, // 128 + false, true, true, false, true, false, false, true, // 136 + false, true, true, false, true, false, false, true, // 144 + true, false, false, true, false, true, true, false, // 152 + false, true, true, false, true, false, false, true, // 160 + true, false, false, true, false, true, true, false, // 168 + true, false, false, true, false, true, true, false, // 176 + false, true, true, false, true, false, false, true, // 184 + false, true, true, false, true, false, false, true, // 192 + true, false, false, true, false, true, true, false, // 200 + true, false, false, true, false, true, true, false, // 208 + false, true, true, false, true, false, false, true, // 216 + true, false, false, true, false, true, true, false, // 224 + false, true, true, false, true, false, false, true, // 232 + false, true, true, false, true, false, false, true, // 240 + true, false, false, true, false, true, true, false, // 248 + }; + private final ParsableByteArray ccData; private final int packetLength; private final int selectedField; - private final LinkedList cueBuilders; + private final int selectedChannel; + private final ArrayList cueBuilders; private CueBuilder currentCueBuilder; private List cues; @@ -194,29 +242,49 @@ public final class Cea608Decoder extends CeaDecoder { private int captionMode; private int captionRowCount; + private boolean isCaptionValid; private boolean repeatableControlSet; private byte repeatableControlCc1; private byte repeatableControlCc2; + private int currentChannel; + + // The incoming characters may belong to 3 different services based on the last received control + // codes. The 3 services are Captioning, Text and XDS. The decoder only processes Captioning + // service bytes and drops the rest. + private boolean isInCaptionService; public Cea608Decoder(String mimeType, int accessibilityChannel) { ccData = new ParsableByteArray(); - cueBuilders = new LinkedList<>(); + cueBuilders = new ArrayList<>(); currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT); + currentChannel = NTSC_CC_CHANNEL_1; packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3; switch (accessibilityChannel) { - case 3: - case 4: - selectedField = 2; - break; case 1: + selectedChannel = NTSC_CC_CHANNEL_1; + selectedField = NTSC_CC_FIELD_1; + break; case 2: - case Format.NO_VALUE: + selectedChannel = NTSC_CC_CHANNEL_2; + selectedField = NTSC_CC_FIELD_1; + break; + case 3: + selectedChannel = NTSC_CC_CHANNEL_1; + selectedField = NTSC_CC_FIELD_2; + break; + case 4: + selectedChannel = NTSC_CC_CHANNEL_2; + selectedField = NTSC_CC_FIELD_2; + break; default: - selectedField = 1; + Log.w(TAG, "Invalid channel. Defaulting to CC1."); + selectedChannel = NTSC_CC_CHANNEL_1; + selectedField = NTSC_CC_FIELD_1; } setCaptionMode(CC_MODE_UNKNOWN); resetCueBuilders(); + isInCaptionService = true; } @Override @@ -230,11 +298,14 @@ public final class Cea608Decoder extends CeaDecoder { cues = null; lastCues = null; setCaptionMode(CC_MODE_UNKNOWN); + setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT); resetCueBuilders(); - captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; + isCaptionValid = false; repeatableControlSet = false; repeatableControlCc1 = 0; repeatableControlCc2 = 0; + currentChannel = NTSC_CC_CHANNEL_1; + isInCaptionService = true; } @Override @@ -253,147 +324,151 @@ public final class Cea608Decoder extends CeaDecoder { return new CeaSubtitle(cues); } + @SuppressWarnings("ByteBufferBackingArray") @Override protected void decode(SubtitleInputBuffer inputBuffer) { ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit()); boolean captionDataProcessed = false; - boolean isRepeatableControl = false; while (ccData.bytesLeft() >= packetLength) { - byte ccDataHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER + byte ccHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER : (byte) ccData.readUnsignedByte(); - byte ccData1 = (byte) (ccData.readUnsignedByte() & 0x7F); // strip the parity bit - byte ccData2 = (byte) (ccData.readUnsignedByte() & 0x7F); // strip the parity bit + int ccByte1 = ccData.readUnsignedByte(); + int ccByte2 = ccData.readUnsignedByte(); - // Only examine valid CEA-608 packets // TODO: We're currently ignoring the top 5 marker bits, which should all be 1s according // to the CEA-608 specification. We need to determine if the data should be handled // differently when that is not the case. - if ((ccDataHeader & (CC_VALID_FLAG | CC_TYPE_FLAG)) != CC_VALID_608_ID) { + + if ((ccHeader & CC_TYPE_FLAG) != 0) { + // Do not process anything that is not part of the 608 byte stream. continue; } - // Only examine packets within the selected field - if ((selectedField == 1 && (ccDataHeader & CC_FIELD_FLAG) != NTSC_CC_FIELD_1) - || (selectedField == 2 && (ccDataHeader & CC_FIELD_FLAG) != NTSC_CC_FIELD_2)) { + if ((ccHeader & CC_FIELD_FLAG) != selectedField) { + // Do not process packets not within the selected field. continue; } - // Ignore empty captions. + // Strip the parity bit from each byte to get CC data. + byte ccData1 = (byte) (ccByte1 & 0x7F); + byte ccData2 = (byte) (ccByte2 & 0x7F); + if (ccData1 == 0 && ccData2 == 0) { + // Ignore empty captions. continue; } - // If we've reached this point then there is data to process; flag that work has been done. - captionDataProcessed = true; + boolean previousIsCaptionValid = isCaptionValid; + isCaptionValid = + (ccHeader & CC_VALID_FLAG) == CC_VALID_FLAG + && ODD_PARITY_BYTE_TABLE[ccByte1] + && ODD_PARITY_BYTE_TABLE[ccByte2]; - // Special North American character set. - // ccData1 - 0|0|0|1|C|0|0|1 - // ccData2 - 0|0|1|1|X|X|X|X - if (((ccData1 & 0xF7) == 0x11) && ((ccData2 & 0xF0) == 0x30)) { - // TODO: Make use of the channel toggle - currentCueBuilder.append(getSpecialChar(ccData2)); + if (isRepeatedCommand(isCaptionValid, ccData1, ccData2)) { + // Ignore repeated valid commands. continue; } - // Extended Western European character set. - // ccData1 - 0|0|0|1|C|0|1|S - // ccData2 - 0|0|1|X|X|X|X|X - if (((ccData1 & 0xF6) == 0x12) && (ccData2 & 0xE0) == 0x20) { - // TODO: Make use of the channel toggle - // Remove standard equivalent of the special extended char before appending new one - currentCueBuilder.backspace(); - if ((ccData1 & 0x01) == 0x00) { - // Extended Spanish/Miscellaneous and French character set (S = 0). - currentCueBuilder.append(getExtendedEsFrChar(ccData2)); - } else { - // Extended Portuguese and German/Danish character set (S = 1). - currentCueBuilder.append(getExtendedPtDeChar(ccData2)); + if (!isCaptionValid) { + if (previousIsCaptionValid) { + // The encoder has flipped the validity bit to indicate captions are being turned off. + resetCueBuilders(); + captionDataProcessed = true; } continue; } - // Control character. - // ccData1 - 0|0|0|X|X|X|X|X - if ((ccData1 & 0xE0) == 0x00) { - isRepeatableControl = handleCtrl(ccData1, ccData2); + maybeUpdateIsInCaptionService(ccData1, ccData2); + if (!isInCaptionService) { + // Only the Captioning service is supported. Drop all other bytes. continue; } - // Basic North American character set. - currentCueBuilder.append(getChar(ccData1)); - if ((ccData2 & 0xE0) != 0x00) { - currentCueBuilder.append(getChar(ccData2)); + if (!updateAndVerifyCurrentChannel(ccData1)) { + // Wrong channel. + continue; } + + if (isCtrlCode(ccData1)) { + if (isSpecialNorthAmericanChar(ccData1, ccData2)) { + currentCueBuilder.append(getSpecialNorthAmericanChar(ccData2)); + } else if (isExtendedWestEuropeanChar(ccData1, ccData2)) { + // Remove standard equivalent of the special extended char before appending new one. + currentCueBuilder.backspace(); + currentCueBuilder.append(getExtendedWestEuropeanChar(ccData1, ccData2)); + } else if (isMidrowCtrlCode(ccData1, ccData2)) { + handleMidrowCtrl(ccData2); + } else if (isPreambleAddressCode(ccData1, ccData2)) { + handlePreambleAddressCode(ccData1, ccData2); + } else if (isTabCtrlCode(ccData1, ccData2)) { + currentCueBuilder.tabOffset = ccData2 - 0x20; + } else if (isMiscCode(ccData1, ccData2)) { + handleMiscCode(ccData2); + } + } else { + // Basic North American character set. + currentCueBuilder.append(getBasicChar(ccData1)); + if ((ccData2 & 0xE0) != 0x00) { + currentCueBuilder.append(getBasicChar(ccData2)); + } + } + captionDataProcessed = true; } if (captionDataProcessed) { - if (!isRepeatableControl) { - repeatableControlSet = false; - } if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { cues = getDisplayCues(); } } } - private boolean handleCtrl(byte cc1, byte cc2) { - boolean isRepeatableControl = isRepeatable(cc1); + private boolean updateAndVerifyCurrentChannel(byte cc1) { + if (isCtrlCode(cc1)) { + currentChannel = getChannel(cc1); + } + return currentChannel == selectedChannel; + } - // Most control commands are sent twice in succession to ensure they are received properly. - // We don't want to process duplicate commands, so if we see the same repeatable command twice - // in a row, ignore the second one. - if (isRepeatableControl) { - if (repeatableControlSet - && repeatableControlCc1 == cc1 - && repeatableControlCc2 == cc2) { - // This is a duplicate. Clear the repeatable control flag and return. + private boolean isRepeatedCommand(boolean captionValid, byte cc1, byte cc2) { + // Most control commands are sent twice in succession to ensure they are received properly. We + // don't want to process duplicate commands, so if we see the same repeatable command twice in a + // row then we ignore the second one. + if (captionValid && isRepeatable(cc1)) { + if (repeatableControlSet && repeatableControlCc1 == cc1 && repeatableControlCc2 == cc2) { + // This is a repeated command, so we ignore it. repeatableControlSet = false; return true; } else { - // This is a repeatable command, but we haven't see it yet, so set the repeabable control - // flag (to ensure we ignore the next one should it be a duplicate) and continue processing - // the command. + // This is the first occurrence of a repeatable command. Set the repeatable control + // variables so that we can recognize and ignore a duplicate (if there is one), and then + // continue to process the command below. repeatableControlSet = true; repeatableControlCc1 = cc1; repeatableControlCc2 = cc2; } + } else { + // This command is not repeatable. + repeatableControlSet = false; } - - if (isMidrowCtrlCode(cc1, cc2)) { - handleMidrowCtrl(cc2); - } else if (isPreambleAddressCode(cc1, cc2)) { - handlePreambleAddressCode(cc1, cc2); - } else if (isTabCtrlCode(cc1, cc2)) { - currentCueBuilder.setTab(cc2 - 0x20); - } else if (isMiscCode(cc1, cc2)) { - handleMiscCode(cc2); - } - - return isRepeatableControl; + return false; } private void handleMidrowCtrl(byte cc2) { // TODO: support the extended styles (i.e. backgrounds and transparencies) - // cc2 - 0|0|1|0|ATRBT|U - // ATRBT is the 3-byte encoded attribute, and U is the underline toggle - boolean isUnderlined = (cc2 & 0x01) == 0x01; - currentCueBuilder.setUnderline(isUnderlined); + // A midrow control code advances the cursor. + currentCueBuilder.append(' '); - int attribute = (cc2 >> 1) & 0x0F; - if (attribute == 0x07) { - currentCueBuilder.setMidrowStyle(new StyleSpan(Typeface.ITALIC), 2); - currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(Color.WHITE), 1); - } else { - currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(COLORS[attribute]), 1); - } + // cc2 - 0|0|1|0|STYLE|U + boolean underline = (cc2 & 0x01) == 0x01; + int style = (cc2 >> 1) & 0x07; + currentCueBuilder.setStyle(style, underline); } private void handlePreambleAddressCode(byte cc1, byte cc2) { // cc1 - 0|0|0|1|C|E|ROW // C is the channel toggle, E is the extended flag, and ROW is the encoded row int row = ROW_INDICES[cc1 & 0x07]; - // TODO: Make use of the channel toggle // TODO: support the extended address and style // cc2 - 0|1|N|ATTRBTE|U @@ -404,46 +479,42 @@ public final class Cea608Decoder extends CeaDecoder { row++; } - if (row != currentCueBuilder.getRow()) { + if (row != currentCueBuilder.row) { if (captionMode != CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) { currentCueBuilder = new CueBuilder(captionMode, captionRowCount); cueBuilders.add(currentCueBuilder); } - currentCueBuilder.setRow(row); - } - - if ((cc2 & 0x01) == 0x01) { - currentCueBuilder.setPreambleStyle(new UnderlineSpan()); + currentCueBuilder.row = row; } // cc2 - 0|1|N|0|STYLE|U // cc2 - 0|1|N|1|CURSR|U - int attribute = cc2 >> 1 & 0x0F; - if (attribute <= 0x07) { - if (attribute == 0x07) { - currentCueBuilder.setPreambleStyle(new StyleSpan(Typeface.ITALIC)); - currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(Color.WHITE)); - } else { - currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(COLORS[attribute])); - } - } else { - currentCueBuilder.setIndent(COLUMN_INDICES[attribute & 0x07]); + boolean isCursor = (cc2 & 0x10) == 0x10; + boolean underline = (cc2 & 0x01) == 0x01; + int cursorOrStyle = (cc2 >> 1) & 0x07; + + // We need to call setStyle even for the isCursor case, to update the underline bit. + // STYLE_UNCHANGED is used for this case. + currentCueBuilder.setStyle(isCursor ? STYLE_UNCHANGED : cursorOrStyle, underline); + + if (isCursor) { + currentCueBuilder.indent = COLUMN_INDICES[cursorOrStyle]; } } private void handleMiscCode(byte cc2) { switch (cc2) { case CTRL_ROLL_UP_CAPTIONS_2_ROWS: - captionRowCount = 2; setCaptionMode(CC_MODE_ROLL_UP); + setCaptionRowCount(2); return; case CTRL_ROLL_UP_CAPTIONS_3_ROWS: - captionRowCount = 3; setCaptionMode(CC_MODE_ROLL_UP); + setCaptionRowCount(3); return; case CTRL_ROLL_UP_CAPTIONS_4_ROWS: - captionRowCount = 4; setCaptionMode(CC_MODE_ROLL_UP); + setCaptionRowCount(4); return; case CTRL_RESUME_CAPTION_LOADING: setCaptionMode(CC_MODE_POP_ON); @@ -451,6 +522,9 @@ public final class Cea608Decoder extends CeaDecoder { case CTRL_RESUME_DIRECT_CAPTIONING: setCaptionMode(CC_MODE_PAINT_ON); return; + default: + // Fall through. + break; } if (captionMode == CC_MODE_UNKNOWN) { @@ -459,7 +533,7 @@ public final class Cea608Decoder extends CeaDecoder { switch (cc2) { case CTRL_ERASE_DISPLAYED_MEMORY: - cues = null; + cues = Collections.emptyList(); if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { resetCueBuilders(); } @@ -484,17 +558,41 @@ public final class Cea608Decoder extends CeaDecoder { case CTRL_DELETE_TO_END_OF_ROW: // TODO: implement break; + default: + // Fall through. + break; } } private List getDisplayCues() { - List displayCues = new ArrayList<>(); - for (int i = 0; i < cueBuilders.size(); i++) { - Cue cue = cueBuilders.get(i).build(); + // CEA-608 does not define middle and end alignment, however content providers artificially + // introduce them using whitespace. When each cue is built, we try and infer the alignment based + // on the amount of whitespace either side of the text. To avoid consecutive cues being aligned + // differently, we force all cues to have the same alignment, with start alignment given + // preference, then middle alignment, then end alignment. + @Cue.AnchorType int positionAnchor = Cue.ANCHOR_TYPE_END; + int cueBuilderCount = cueBuilders.size(); + List cueBuilderCues = new ArrayList<>(cueBuilderCount); + for (int i = 0; i < cueBuilderCount; i++) { + Cue cue = cueBuilders.get(i).build(/* forcedPositionAnchor= */ Cue.TYPE_UNSET); + cueBuilderCues.add(cue); if (cue != null) { + positionAnchor = Math.min(positionAnchor, cue.positionAnchor); + } + } + + // Skip null cues and rebuild any that don't have the preferred alignment. + List displayCues = new ArrayList<>(cueBuilderCount); + for (int i = 0; i < cueBuilderCount; i++) { + Cue cue = cueBuilderCues.get(i); + if (cue != null) { + if (cue.positionAnchor != positionAnchor) { + cue = cueBuilders.get(i).build(positionAnchor); + } displayCues.add(cue); } } + return displayCues; } @@ -506,31 +604,89 @@ public final class Cea608Decoder extends CeaDecoder { int oldCaptionMode = this.captionMode; this.captionMode = captionMode; + if (captionMode == CC_MODE_PAINT_ON) { + // Switching to paint-on mode should have no effect except to select the mode. + for (int i = 0; i < cueBuilders.size(); i++) { + cueBuilders.get(i).setCaptionMode(captionMode); + } + return; + } + // Clear the working memory. resetCueBuilders(); if (oldCaptionMode == CC_MODE_PAINT_ON || captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_UNKNOWN) { // When switching from paint-on or to roll-up or unknown, we also need to clear the caption. - cues = null; + cues = Collections.emptyList(); } } + private void setCaptionRowCount(int captionRowCount) { + this.captionRowCount = captionRowCount; + currentCueBuilder.setCaptionRowCount(captionRowCount); + } + private void resetCueBuilders() { - currentCueBuilder.reset(captionMode, captionRowCount); + currentCueBuilder.reset(captionMode); cueBuilders.clear(); cueBuilders.add(currentCueBuilder); } - private static char getChar(byte ccData) { + private void maybeUpdateIsInCaptionService(byte cc1, byte cc2) { + if (isXdsControlCode(cc1)) { + isInCaptionService = false; + } else if (isServiceSwitchCommand(cc1)) { + switch (cc2) { + case CTRL_TEXT_RESTART: + case CTRL_RESUME_TEXT_DISPLAY: + isInCaptionService = false; + break; + case CTRL_END_OF_CAPTION: + case CTRL_RESUME_CAPTION_LOADING: + case CTRL_RESUME_DIRECT_CAPTIONING: + case CTRL_ROLL_UP_CAPTIONS_2_ROWS: + case CTRL_ROLL_UP_CAPTIONS_3_ROWS: + case CTRL_ROLL_UP_CAPTIONS_4_ROWS: + isInCaptionService = true; + break; + default: + // No update. + } + } + } + + private static char getBasicChar(byte ccData) { int index = (ccData & 0x7F) - 0x20; return (char) BASIC_CHARACTER_SET[index]; } - private static char getSpecialChar(byte ccData) { + private static boolean isSpecialNorthAmericanChar(byte cc1, byte cc2) { + // cc1 - 0|0|0|1|C|0|0|1 + // cc2 - 0|0|1|1|X|X|X|X + return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x30); + } + + private static char getSpecialNorthAmericanChar(byte ccData) { int index = ccData & 0x0F; return (char) SPECIAL_CHARACTER_SET[index]; } + private static boolean isExtendedWestEuropeanChar(byte cc1, byte cc2) { + // cc1 - 0|0|0|1|C|0|1|S + // cc2 - 0|0|1|X|X|X|X|X + return ((cc1 & 0xF6) == 0x12) && ((cc2 & 0xE0) == 0x20); + } + + private static char getExtendedWestEuropeanChar(byte cc1, byte cc2) { + if ((cc1 & 0x01) == 0x00) { + // Extended Spanish/Miscellaneous and French character set (S = 0). + return getExtendedEsFrChar(cc2); + } else { + // Extended Portuguese and German/Danish character set (S = 1). + return getExtendedPtDeChar(cc2); + } + } + private static char getExtendedEsFrChar(byte ccData) { int index = ccData & 0x1F; return (char) SPECIAL_ES_FR_CHARACTER_SET[index]; @@ -541,6 +697,16 @@ public final class Cea608Decoder extends CeaDecoder { return (char) SPECIAL_PT_DE_CHARACTER_SET[index]; } + private static boolean isCtrlCode(byte cc1) { + // cc1 - 0|0|0|X|X|X|X|X + return (cc1 & 0xE0) == 0x00; + } + + private static int getChannel(byte cc1) { + // cc1 - X|X|X|X|C|X|X|X + return (cc1 >> 3) & 0x1; + } + private static boolean isMidrowCtrlCode(byte cc1, byte cc2) { // cc1 - 0|0|0|1|C|0|0|1 // cc2 - 0|0|1|0|X|X|X|X @@ -560,9 +726,9 @@ public final class Cea608Decoder extends CeaDecoder { } private static boolean isMiscCode(byte cc1, byte cc2) { - // cc1 - 0|0|0|1|C|1|0|0 + // cc1 - 0|0|0|1|C|1|0|F // cc2 - 0|0|1|0|X|X|X|X - return ((cc1 & 0xF7) == 0x14) && ((cc2 & 0xF0) == 0x20); + return ((cc1 & 0xF6) == 0x14) && ((cc2 & 0xF0) == 0x20); } private static boolean isRepeatable(byte cc1) { @@ -570,105 +736,82 @@ public final class Cea608Decoder extends CeaDecoder { return (cc1 & 0xF0) == 0x10; } - private static class CueBuilder { + private static boolean isXdsControlCode(byte cc1) { + return 0x01 <= cc1 && cc1 <= 0x0F; + } - private static final int POSITION_UNSET = -1; + private static boolean isServiceSwitchCommand(byte cc1) { + // cc1 - 0|0|0|1|C|1|0|0 + return (cc1 & 0xF7) == 0x14; + } + + private static class CueBuilder { // 608 captions define a 15 row by 32 column screen grid. These constants convert from 608 // positions to normalized screen position. private static final int SCREEN_CHARWIDTH = 32; private static final int BASE_ROW = 15; - private final List preambleStyles; - private final List midrowStyles; + private final List cueStyles; private final List rolledUpCaptions; - private final SpannableStringBuilder captionStringBuilder; + private final StringBuilder captionStringBuilder; private int row; private int indent; private int tabOffset; private int captionMode; private int captionRowCount; - private int underlineStartPosition; public CueBuilder(int captionMode, int captionRowCount) { - preambleStyles = new ArrayList<>(); - midrowStyles = new ArrayList<>(); - rolledUpCaptions = new LinkedList<>(); - captionStringBuilder = new SpannableStringBuilder(); - reset(captionMode, captionRowCount); + cueStyles = new ArrayList<>(); + rolledUpCaptions = new ArrayList<>(); + captionStringBuilder = new StringBuilder(); + reset(captionMode); + setCaptionRowCount(captionRowCount); } - public void reset(int captionMode, int captionRowCount) { - preambleStyles.clear(); - midrowStyles.clear(); + public void reset(int captionMode) { + this.captionMode = captionMode; + cueStyles.clear(); rolledUpCaptions.clear(); - captionStringBuilder.clear(); + captionStringBuilder.setLength(0); row = BASE_ROW; indent = 0; tabOffset = 0; - this.captionMode = captionMode; - this.captionRowCount = captionRowCount; - underlineStartPosition = POSITION_UNSET; } public boolean isEmpty() { - return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty() + return cueStyles.isEmpty() + && rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0; } + public void setCaptionMode(int captionMode) { + this.captionMode = captionMode; + } + + public void setCaptionRowCount(int captionRowCount) { + this.captionRowCount = captionRowCount; + } + + public void setStyle(int style, boolean underline) { + cueStyles.add(new CueStyle(style, underline, captionStringBuilder.length())); + } + public void backspace() { int length = captionStringBuilder.length(); if (length > 0) { captionStringBuilder.delete(length - 1, length); - } - } - - public int getRow() { - return row; - } - - public void setRow(int row) { - this.row = row; - } - - public void rollUp() { - rolledUpCaptions.add(buildSpannableString()); - captionStringBuilder.clear(); - preambleStyles.clear(); - midrowStyles.clear(); - underlineStartPosition = POSITION_UNSET; - - int numRows = Math.min(captionRowCount, row); - while (rolledUpCaptions.size() >= numRows) { - rolledUpCaptions.remove(0); - } - } - - public void setIndent(int indent) { - this.indent = indent; - } - - public void setTab(int tabs) { - tabOffset = tabs; - } - - public void setPreambleStyle(CharacterStyle style) { - preambleStyles.add(style); - } - - public void setMidrowStyle(CharacterStyle style, int nextStyleIncrement) { - midrowStyles.add(new CueStyle(style, captionStringBuilder.length(), nextStyleIncrement)); - } - - public void setUnderline(boolean enabled) { - if (enabled) { - underlineStartPosition = captionStringBuilder.length(); - } else if (underlineStartPosition != POSITION_UNSET) { - // underline spans won't overlap, so it's safe to modify the builder directly with them - captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, - captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - underlineStartPosition = POSITION_UNSET; + // Decrement style start positions if necessary. + for (int i = cueStyles.size() - 1; i >= 0; i--) { + CueStyle style = cueStyles.get(i); + if (style.start == length) { + style.start--; + } else { + // All earlier cues must have style.start < length. + break; + } + } } } @@ -676,35 +819,17 @@ public final class Cea608Decoder extends CeaDecoder { captionStringBuilder.append(text); } - public SpannableString buildSpannableString() { - int length = captionStringBuilder.length(); - - // preamble styles apply to the entire cue - for (int i = 0; i < preambleStyles.size(); i++) { - captionStringBuilder.setSpan(preambleStyles.get(i), 0, length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + public void rollUp() { + rolledUpCaptions.add(buildCurrentLine()); + captionStringBuilder.setLength(0); + cueStyles.clear(); + int numRows = Math.min(captionRowCount, row); + while (rolledUpCaptions.size() >= numRows) { + rolledUpCaptions.remove(0); } - - // midrow styles only apply to part of the cue, and after preamble styles - for (int i = 0; i < midrowStyles.size(); i++) { - CueStyle cueStyle = midrowStyles.get(i); - int end = (i < midrowStyles.size() - cueStyle.nextStyleIncrement) - ? midrowStyles.get(i + cueStyle.nextStyleIncrement).start - : length; - captionStringBuilder.setSpan(cueStyle.style, cueStyle.start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - // special case for midrow underlines that went to the end of the cue - if (underlineStartPosition != POSITION_UNSET) { - captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - return new SpannableString(captionStringBuilder); } - public Cue build() { + public Cue build(@Cue.AnchorType int forcedPositionAnchor) { SpannableStringBuilder cueString = new SpannableStringBuilder(); // Add any rolled up captions, separated by new lines. for (int i = 0; i < rolledUpCaptions.size(); i++) { @@ -712,38 +837,53 @@ public final class Cea608Decoder extends CeaDecoder { cueString.append('\n'); } // Add the current line. - cueString.append(buildSpannableString()); + cueString.append(buildCurrentLine()); if (cueString.length() == 0) { // The cue is empty. return null; } - float position; int positionAnchor; // The number of empty columns before the start of the text, in the range [0-31]. int startPadding = indent + tabOffset; // The number of empty columns after the end of the text, in the same range. int endPadding = SCREEN_CHARWIDTH - startPadding - cueString.length(); int startEndPaddingDelta = startPadding - endPadding; - if (captionMode == CC_MODE_POP_ON && Math.abs(startEndPaddingDelta) < 3) { - // Treat approximately centered pop-on captions are middle aligned. - position = 0.5f; + if (forcedPositionAnchor != Cue.TYPE_UNSET) { + positionAnchor = forcedPositionAnchor; + } else if (captionMode == CC_MODE_POP_ON + && (Math.abs(startEndPaddingDelta) < 3 || endPadding < 0)) { + // Treat approximately centered pop-on captions as middle aligned. We also treat captions + // that are wider than they should be in this way. See + // https://github.com/google/ExoPlayer/issues/3534. positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; } else if (captionMode == CC_MODE_POP_ON && startEndPaddingDelta > 0) { // Treat pop-on captions with less padding at the end than the start as end aligned. - position = (float) (SCREEN_CHARWIDTH - endPadding) / SCREEN_CHARWIDTH; - // Adjust the position to fit within the safe area. - position = position * 0.8f + 0.1f; positionAnchor = Cue.ANCHOR_TYPE_END; } else { // For all other cases assume start aligned. - position = (float) startPadding / SCREEN_CHARWIDTH; - // Adjust the position to fit within the safe area. - position = position * 0.8f + 0.1f; positionAnchor = Cue.ANCHOR_TYPE_START; } + float position; + switch (positionAnchor) { + case Cue.ANCHOR_TYPE_MIDDLE: + position = 0.5f; + break; + case Cue.ANCHOR_TYPE_END: + position = (float) (SCREEN_CHARWIDTH - endPadding) / SCREEN_CHARWIDTH; + // Adjust the position to fit within the safe area. + position = position * 0.8f + 0.1f; + break; + case Cue.ANCHOR_TYPE_START: + default: + position = (float) startPadding / SCREEN_CHARWIDTH; + // Adjust the position to fit within the safe area. + position = position * 0.8f + 0.1f; + break; + } + int lineAnchor; int line; // Note: Row indices are in the range [1-15]. @@ -760,25 +900,111 @@ public final class Cea608Decoder extends CeaDecoder { line = row; } - return new Cue(cueString, Alignment.ALIGN_NORMAL, line, Cue.LINE_TYPE_NUMBER, lineAnchor, - position, positionAnchor, Cue.DIMEN_UNSET); + return new Cue( + cueString, + Alignment.ALIGN_NORMAL, + line, + Cue.LINE_TYPE_NUMBER, + lineAnchor, + position, + positionAnchor, + Cue.DIMEN_UNSET); } - @Override - public String toString() { - return captionStringBuilder.toString(); + private SpannableString buildCurrentLine() { + SpannableStringBuilder builder = new SpannableStringBuilder(captionStringBuilder); + int length = builder.length(); + + int underlineStartPosition = C.INDEX_UNSET; + int italicStartPosition = C.INDEX_UNSET; + int colorStartPosition = 0; + int color = Color.WHITE; + + boolean nextItalic = false; + int nextColor = Color.WHITE; + + for (int i = 0; i < cueStyles.size(); i++) { + CueStyle cueStyle = cueStyles.get(i); + boolean underline = cueStyle.underline; + int style = cueStyle.style; + if (style != STYLE_UNCHANGED) { + // If the style is a color then italic is cleared. + nextItalic = style == STYLE_ITALICS; + // If the style is italic then the color is left unchanged. + nextColor = style == STYLE_ITALICS ? nextColor : STYLE_COLORS[style]; + } + + int position = cueStyle.start; + int nextPosition = (i + 1) < cueStyles.size() ? cueStyles.get(i + 1).start : length; + if (position == nextPosition) { + // There are more cueStyles to process at the current position. + continue; + } + + // Process changes to underline up to the current position. + if (underlineStartPosition != C.INDEX_UNSET && !underline) { + setUnderlineSpan(builder, underlineStartPosition, position); + underlineStartPosition = C.INDEX_UNSET; + } else if (underlineStartPosition == C.INDEX_UNSET && underline) { + underlineStartPosition = position; + } + // Process changes to italic up to the current position. + if (italicStartPosition != C.INDEX_UNSET && !nextItalic) { + setItalicSpan(builder, italicStartPosition, position); + italicStartPosition = C.INDEX_UNSET; + } else if (italicStartPosition == C.INDEX_UNSET && nextItalic) { + italicStartPosition = position; + } + // Process changes to color up to the current position. + if (nextColor != color) { + setColorSpan(builder, colorStartPosition, position, color); + color = nextColor; + colorStartPosition = position; + } + } + + // Add any final spans. + if (underlineStartPosition != C.INDEX_UNSET && underlineStartPosition != length) { + setUnderlineSpan(builder, underlineStartPosition, length); + } + if (italicStartPosition != C.INDEX_UNSET && italicStartPosition != length) { + setItalicSpan(builder, italicStartPosition, length); + } + if (colorStartPosition != length) { + setColorSpan(builder, colorStartPosition, length, color); + } + + return new SpannableString(builder); + } + + private static void setUnderlineSpan(SpannableStringBuilder builder, int start, int end) { + builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private static void setItalicSpan(SpannableStringBuilder builder, int start, int end) { + builder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private static void setColorSpan( + SpannableStringBuilder builder, int start, int end, int color) { + if (color == Color.WHITE) { + // White is treated as the default color (i.e. no span is attached). + return; + } + builder.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } private static class CueStyle { - public final CharacterStyle style; - public final int start; - public final int nextStyleIncrement; + public final int style; + public final boolean underline; - public CueStyle(CharacterStyle style, int start, int nextStyleIncrement) { + public int start; + + public CueStyle(int style, boolean underline, int start) { this.style = style; + this.underline = underline; this.start = start; - this.nextStyleIncrement = nextStyleIncrement; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Cue.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Cue.java index e446f4e1bb13..268b6baec03f 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Cue.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Cue.java @@ -15,8 +15,8 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea; -import android.support.annotation.NonNull; import android.text.Layout.Alignment; +import androidx.annotation.NonNull; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; /** @@ -24,11 +24,6 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; */ /* package */ final class Cea708Cue extends Cue implements Comparable { - /** - * An unset priority. - */ - public static final int PRIORITY_UNSET = -1; - /** * The priority of the cue box. */ @@ -64,5 +59,4 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; } return 0; } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index 742890dea37b..c8af0ed35001 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -25,7 +25,7 @@ import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; -import android.util.Log; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; @@ -34,11 +34,11 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleDecoder; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleInputBuffer; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; /** @@ -104,7 +104,7 @@ public final class Cea708Decoder extends CeaDecoder { private static final int COMMAND_DF1 = 0x99; // DefineWindow 1 (+6 bytes) private static final int COMMAND_DF2 = 0x9A; // DefineWindow 2 (+6 bytes) private static final int COMMAND_DF3 = 0x9B; // DefineWindow 3 (+6 bytes) - private static final int COMMAND_DS4 = 0x9C; // DefineWindow 4 (+6 bytes) + private static final int COMMAND_DF4 = 0x9C; // DefineWindow 4 (+6 bytes) private static final int COMMAND_DF5 = 0x9D; // DefineWindow 5 (+6 bytes) private static final int COMMAND_DF6 = 0x9E; // DefineWindow 6 (+6 bytes) private static final int COMMAND_DF7 = 0x9F; // DefineWindow 7 (+6 bytes) @@ -153,10 +153,11 @@ public final class Cea708Decoder extends CeaDecoder { private DtvCcPacket currentDtvCcPacket; private int currentWindow; - public Cea708Decoder(int accessibilityChannel) { + // TODO: Retrieve isWideAspectRatio from initializationData and use it. + public Cea708Decoder(int accessibilityChannel, @Nullable List initializationData) { ccData = new ParsableByteArray(); serviceBlockPacket = new ParsableBitArray(); - selectedServiceNumber = (accessibilityChannel == Format.NO_VALUE) ? 1 : accessibilityChannel; + selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel; cueBuilders = new CueBuilder[NUM_WINDOWS]; for (int i = 0; i < NUM_WINDOWS; i++) { @@ -196,7 +197,10 @@ public final class Cea708Decoder extends CeaDecoder { @Override protected void decode(SubtitleInputBuffer inputBuffer) { - ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit()); + // Subtitle input buffers are non-direct and the position is zero, so calling array() is safe. + @SuppressWarnings("ByteBufferBackingArray") + byte[] inputBufferData = inputBuffer.data.array(); + ccData.reset(inputBufferData, inputBuffer.data.limit()); while (ccData.bytesLeft() >= 3) { int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07); @@ -270,7 +274,10 @@ public final class Cea708Decoder extends CeaDecoder { if (serviceNumber == 7) { // extended service numbers serviceBlockPacket.skipBits(2); - serviceNumber += serviceBlockPacket.readBits(6); + serviceNumber = serviceBlockPacket.readBits(6); + if (serviceNumber < 7) { + Log.w(TAG, "Invalid extended service number: " + serviceNumber); + } } // Ignore packets in which blockSize is 0 @@ -464,7 +471,7 @@ public final class Cea708Decoder extends CeaDecoder { case COMMAND_DF1: case COMMAND_DF2: case COMMAND_DF3: - case COMMAND_DS4: + case COMMAND_DF4: case COMMAND_DF5: case COMMAND_DF6: case COMMAND_DF7: @@ -741,7 +748,7 @@ public final class Cea708Decoder extends CeaDecoder { } } Collections.sort(displayCues); - return Collections.unmodifiableList(displayCues); + return Collections.unmodifiableList(displayCues); } private void resetCueBuilders() { @@ -811,43 +818,43 @@ public final class Cea708Decoder extends CeaDecoder { private static final int PEN_OFFSET_NORMAL = 1; // The window style properties are specified in the CEA-708 specification. - private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[]{ + private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[] { JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_CENTER, JUSTIFICATION_LEFT }; - private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[]{ + private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[] { DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_TOP_TO_BOTTOM }; - private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[]{ + private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[] { DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_RIGHT_TO_LEFT }; - private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[]{ + private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[] { false, false, false, true, true, true, false }; - private static final int[] WINDOW_STYLE_FILL = new int[]{ + private static final int[] WINDOW_STYLE_FILL = new int[] { COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK }; // The pen style properties are specified in the CEA-708 specification. - private static final int[] PEN_STYLE_FONT_STYLE = new int[]{ + private static final int[] PEN_STYLE_FONT_STYLE = new int[] { PEN_FONT_STYLE_DEFAULT, PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS, PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS, PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS, PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS }; - private static final int[] PEN_STYLE_EDGE_TYPE = new int[]{ + private static final int[] PEN_STYLE_EDGE_TYPE = new int[] { BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_UNIFORM, BORDER_AND_EDGE_TYPE_UNIFORM }; - private static final int[] PEN_STYLE_BACKGROUND = new int[]{ + private static final int[] PEN_STYLE_BACKGROUND = new int[] { COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_TRANSPARENT}; @@ -879,7 +886,7 @@ public final class Cea708Decoder extends CeaDecoder { private int row; public CueBuilder() { - rolledUpCaptions = new LinkedList<>(); + rolledUpCaptions = new ArrayList<>(); captionStringBuilder = new SpannableStringBuilder(); reset(); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java new file mode 100644 index 000000000000..5d63ca8e82d4 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea; + +import java.util.Collections; +import java.util.List; + +/** Initialization data for CEA-708 decoders. */ +public final class Cea708InitializationData { + + /** + * Whether the closed caption service is formatted for displays with 16:9 aspect ratio. If false, + * the closed caption service is formatted for 4:3 displays. + */ + public final boolean isWideAspectRatio; + + private Cea708InitializationData(List initializationData) { + isWideAspectRatio = initializationData.get(0)[0] != 0; + } + + /** + * Returns an object representation of CEA-708 initialization data + * + * @param initializationData Binary CEA-708 initialization data. + * @return The object representation. + */ + public static Cea708InitializationData fromData(List initializationData) { + return new Cea708InitializationData(initializationData); + } + + /** + * Builds binary CEA-708 initialization data. + * + * @param isWideAspectRatio Whether the closed caption service is formatted for displays with 16:9 + * aspect ratio. + * @return Binary CEA-708 initializaton data. + */ + public static List buildData(boolean isWideAspectRatio) { + return Collections.singletonList(new byte[] {(byte) (isWideAspectRatio ? 1 : 0)}); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaDecoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaDecoder.java index f5bfe18e4ae1..42fa915fc549 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaDecoder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaDecoder.java @@ -15,6 +15,7 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea; +import androidx.annotation.NonNull; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle; @@ -23,8 +24,8 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleDecoder import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleInputBuffer; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleOutputBuffer; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; -import java.util.LinkedList; -import java.util.TreeSet; +import java.util.ArrayDeque; +import java.util.PriorityQueue; /** * Base class for subtitle parsers for CEA captions. @@ -34,23 +35,24 @@ import java.util.TreeSet; private static final int NUM_INPUT_BUFFERS = 10; private static final int NUM_OUTPUT_BUFFERS = 2; - private final LinkedList availableInputBuffers; - private final LinkedList availableOutputBuffers; - private final TreeSet queuedInputBuffers; + private final ArrayDeque availableInputBuffers; + private final ArrayDeque availableOutputBuffers; + private final PriorityQueue queuedInputBuffers; - private SubtitleInputBuffer dequeuedInputBuffer; + private CeaInputBuffer dequeuedInputBuffer; private long playbackPositionUs; + private long queuedInputBufferCount; public CeaDecoder() { - availableInputBuffers = new LinkedList<>(); + availableInputBuffers = new ArrayDeque<>(); for (int i = 0; i < NUM_INPUT_BUFFERS; i++) { - availableInputBuffers.add(new SubtitleInputBuffer()); + availableInputBuffers.add(new CeaInputBuffer()); } - availableOutputBuffers = new LinkedList<>(); + availableOutputBuffers = new ArrayDeque<>(); for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) { - availableOutputBuffers.add(new CeaOutputBuffer(this)); + availableOutputBuffers.add(new CeaOutputBuffer()); } - queuedInputBuffers = new TreeSet<>(); + queuedInputBuffers = new PriorityQueue<>(); } @Override @@ -73,14 +75,14 @@ import java.util.TreeSet; @Override public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException { - Assertions.checkArgument(inputBuffer != null); Assertions.checkArgument(inputBuffer == dequeuedInputBuffer); if (inputBuffer.isDecodeOnly()) { // We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow // for decoding to begin mid-stream. - releaseInputBuffer(inputBuffer); + releaseInputBuffer(dequeuedInputBuffer); } else { - queuedInputBuffers.add(inputBuffer); + dequeuedInputBuffer.queuedInputBufferCount = queuedInputBufferCount++; + queuedInputBuffers.add(dequeuedInputBuffer); } dequeuedInputBuffer = null; } @@ -90,13 +92,12 @@ import java.util.TreeSet; if (availableOutputBuffers.isEmpty()) { return null; } - // iterate through all available input buffers whose timestamps are less than or equal // to the current playback position; processing input buffers for future content should // be deferred until they would be applicable while (!queuedInputBuffers.isEmpty() - && queuedInputBuffers.first().timeUs <= playbackPositionUs) { - SubtitleInputBuffer inputBuffer = queuedInputBuffers.pollFirst(); + && queuedInputBuffers.peek().timeUs <= playbackPositionUs) { + CeaInputBuffer inputBuffer = queuedInputBuffers.poll(); // If the input buffer indicates we've reached the end of the stream, we can // return immediately with an output buffer propagating that @@ -128,7 +129,7 @@ import java.util.TreeSet; return null; } - private void releaseInputBuffer(SubtitleInputBuffer inputBuffer) { + private void releaseInputBuffer(CeaInputBuffer inputBuffer) { inputBuffer.clear(); availableInputBuffers.add(inputBuffer); } @@ -140,9 +141,10 @@ import java.util.TreeSet; @Override public void flush() { + queuedInputBufferCount = 0; playbackPositionUs = 0; while (!queuedInputBuffers.isEmpty()) { - releaseInputBuffer(queuedInputBuffers.pollFirst()); + releaseInputBuffer(queuedInputBuffers.poll()); } if (dequeuedInputBuffer != null) { releaseInputBuffer(dequeuedInputBuffer); @@ -171,4 +173,32 @@ import java.util.TreeSet; */ protected abstract void decode(SubtitleInputBuffer inputBuffer); + private static final class CeaInputBuffer extends SubtitleInputBuffer + implements Comparable { + + private long queuedInputBufferCount; + + @Override + public int compareTo(@NonNull CeaInputBuffer other) { + if (isEndOfStream() != other.isEndOfStream()) { + return isEndOfStream() ? 1 : -1; + } + long delta = timeUs - other.timeUs; + if (delta == 0) { + delta = queuedInputBufferCount - other.queuedInputBufferCount; + if (delta == 0) { + return 0; + } + } + return delta > 0 ? 1 : -1; + } + } + + private final class CeaOutputBuffer extends SubtitleOutputBuffer { + + @Override + public final void release() { + releaseOutputBuffer(this); + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaSubtitle.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaSubtitle.java index c9312fcc0600..f4649c4c4ba6 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaSubtitle.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaSubtitle.java @@ -54,7 +54,7 @@ import java.util.List; @Override public List getCues(long timeUs) { - return timeUs >= 0 ? cues : Collections.emptyList(); + return timeUs >= 0 ? cues : Collections.emptyList(); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaUtil.java index fdc228af2805..ced169ba176d 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaUtil.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaUtil.java @@ -15,23 +15,23 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea; -import android.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; -/** - * Utility methods for handling CEA-608/708 messages. - */ +/** Utility methods for handling CEA-608/708 messages. Defined in A/53 Part 4:2009. */ public final class CeaUtil { private static final String TAG = "CeaUtil"; + public static final int USER_DATA_IDENTIFIER_GA94 = 0x47413934; + public static final int USER_DATA_TYPE_CODE_MPEG_CC = 0x3; + private static final int PAYLOAD_TYPE_CC = 4; private static final int COUNTRY_CODE = 0xB5; - private static final int PROVIDER_CODE = 0x31; - private static final int USER_ID = 0x47413934; // "GA94" - private static final int USER_DATA_TYPE_CODE = 0x3; + private static final int PROVIDER_CODE_ATSC = 0x31; + private static final int PROVIDER_CODE_DIRECTV = 0x2F; /** * Consumes the unescaped content of an SEI NAL unit, writing the content of any CEA-608 messages @@ -46,33 +46,69 @@ public final class CeaUtil { while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { int payloadType = readNon255TerminatedValue(seiBuffer); int payloadSize = readNon255TerminatedValue(seiBuffer); + int nextPayloadPosition = seiBuffer.getPosition() + payloadSize; // Process the payload. if (payloadSize == -1 || payloadSize > seiBuffer.bytesLeft()) { // This might occur if we're trying to read an encrypted SEI NAL unit. Log.w(TAG, "Skipping remainder of malformed SEI NAL unit."); - seiBuffer.setPosition(seiBuffer.limit()); - } else if (isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) { - // Ignore country_code (1) + provider_code (2) + user_identifier (4) - // + user_data_type_code (1). - seiBuffer.skipBytes(8); - // Ignore first three bits: reserved (1) + process_cc_data_flag (1) + zero_bit (1). - int ccCount = seiBuffer.readUnsignedByte() & 0x1F; - // Ignore em_data (1) - seiBuffer.skipBytes(1); - // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2) - // + cc_data_1 (8) + cc_data_2 (8). - int sampleLength = ccCount * 3; - int sampleStartPosition = seiBuffer.getPosition(); - for (TrackOutput output : outputs) { - seiBuffer.setPosition(sampleStartPosition); - output.sampleData(seiBuffer, sampleLength); - output.sampleMetadata(presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null); + nextPayloadPosition = seiBuffer.limit(); + } else if (payloadType == PAYLOAD_TYPE_CC && payloadSize >= 8) { + int countryCode = seiBuffer.readUnsignedByte(); + int providerCode = seiBuffer.readUnsignedShort(); + int userIdentifier = 0; + if (providerCode == PROVIDER_CODE_ATSC) { + userIdentifier = seiBuffer.readInt(); + } + int userDataTypeCode = seiBuffer.readUnsignedByte(); + if (providerCode == PROVIDER_CODE_DIRECTV) { + seiBuffer.skipBytes(1); // user_data_length. + } + boolean messageIsSupportedCeaCaption = + countryCode == COUNTRY_CODE + && (providerCode == PROVIDER_CODE_ATSC || providerCode == PROVIDER_CODE_DIRECTV) + && userDataTypeCode == USER_DATA_TYPE_CODE_MPEG_CC; + if (providerCode == PROVIDER_CODE_ATSC) { + messageIsSupportedCeaCaption &= userIdentifier == USER_DATA_IDENTIFIER_GA94; + } + if (messageIsSupportedCeaCaption) { + consumeCcData(presentationTimeUs, seiBuffer, outputs); } - // Ignore trailing information in SEI, if any. - seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3)); - } else { - seiBuffer.skipBytes(payloadSize); } + seiBuffer.setPosition(nextPayloadPosition); + } + } + + /** + * Consumes caption data (cc_data), writing the content as samples to all of the provided outputs. + * + * @param presentationTimeUs The presentation time in microseconds for any samples. + * @param ccDataBuffer The buffer containing the caption data. + * @param outputs The outputs to which any samples should be written. + */ + public static void consumeCcData( + long presentationTimeUs, ParsableByteArray ccDataBuffer, TrackOutput[] outputs) { + // First byte contains: reserved (1), process_cc_data_flag (1), zero_bit (1), cc_count (5). + int firstByte = ccDataBuffer.readUnsignedByte(); + boolean processCcDataFlag = (firstByte & 0x40) != 0; + if (!processCcDataFlag) { + // No need to process. + return; + } + int ccCount = firstByte & 0x1F; + ccDataBuffer.skipBytes(1); // Ignore em_data + // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2) + // + cc_data_1 (8) + cc_data_2 (8). + int sampleLength = ccCount * 3; + int sampleStartPosition = ccDataBuffer.getPosition(); + for (TrackOutput output : outputs) { + ccDataBuffer.setPosition(sampleStartPosition); + output.sampleData(ccDataBuffer, sampleLength); + output.sampleMetadata( + presentationTimeUs, + C.BUFFER_FLAG_KEY_FRAME, + sampleLength, + /* offset= */ 0, + /* encryptionData= */ null); } } @@ -82,7 +118,7 @@ public final class CeaUtil { * number of 0xFF bytes and T is the value of the terminating byte. * * @param buffer The buffer from which to read the value. - * @returns The read value, or -1 if the end of the buffer is reached before a value is read. + * @return The read value, or -1 if the end of the buffer is reached before a value is read. */ private static int readNon255TerminatedValue(ParsableByteArray buffer) { int b; @@ -97,31 +133,6 @@ public final class CeaUtil { return value; } - /** - * Inspects an sei message to determine whether it contains CEA-608. - *

- * The position of {@code payload} is left unchanged. - * - * @param payloadType The payload type of the message. - * @param payloadLength The length of the payload. - * @param payload A {@link ParsableByteArray} containing the payload. - * @return Whether the sei message contains CEA-608. - */ - private static boolean isSeiMessageCea608(int payloadType, int payloadLength, - ParsableByteArray payload) { - if (payloadType != PAYLOAD_TYPE_CC || payloadLength < 8) { - return false; - } - int startPosition = payload.getPosition(); - int countryCode = payload.readUnsignedByte(); - int providerCode = payload.readUnsignedShort(); - int userIdentifier = payload.readInt(); - int userDataTypeCode = payload.readUnsignedByte(); - payload.setPosition(startPosition); - return countryCode == COUNTRY_CODE && providerCode == PROVIDER_CODE - && userIdentifier == USER_ID && userDataTypeCode == USER_DATA_TYPE_CODE; - } - private CeaUtil() {} } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/package-info.java new file mode 100644 index 000000000000..e80d06586a9d --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbDecoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbDecoder.java index 4ef12d6d5ccd..063872ae2ebf 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbDecoder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbDecoder.java @@ -16,12 +16,11 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.text.dvb; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import java.util.List; -/** - * A {@link SimpleSubtitleDecoder} for DVB Subtitles. - */ +/** A {@link SimpleSubtitleDecoder} for DVB subtitles. */ public final class DvbDecoder extends SimpleSubtitleDecoder { private final DvbParser parser; @@ -40,7 +39,7 @@ public final class DvbDecoder extends SimpleSubtitleDecoder { } @Override - protected DvbSubtitle decode(byte[] data, int length, boolean reset) { + protected Subtitle decode(byte[] data, int length, boolean reset) { if (reset) { parser.reset(); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbParser.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbParser.java index f5f1548d36b3..839c206ad787 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbParser.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbParser.java @@ -21,15 +21,16 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; -import android.graphics.Region; -import android.util.Log; import android.util.SparseArray; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Parses {@link Cue}s from a DVB subtitle bitstream. @@ -86,7 +87,7 @@ import java.util.List; private final ClutDefinition defaultClutDefinition; private final SubtitleService subtitleService; - private Bitmap bitmap; + @MonotonicNonNull private Bitmap bitmap; /** * Construct an instance for the given subtitle and ancillary page ids. @@ -132,7 +133,8 @@ import java.util.List; parseSubtitlingSegment(dataBitArray, subtitleService); } - if (subtitleService.pageComposition == null) { + @Nullable PageComposition pageComposition = subtitleService.pageComposition; + if (pageComposition == null) { return Collections.emptyList(); } @@ -148,8 +150,10 @@ import java.util.List; // Build the cues. List cues = new ArrayList<>(); - SparseArray pageRegions = subtitleService.pageComposition.regions; + SparseArray pageRegions = pageComposition.regions; for (int i = 0; i < pageRegions.size(); i++) { + // Save clean clipping state. + canvas.save(); PageRegion pageRegion = pageRegions.valueAt(i); int regionId = pageRegions.keyAt(i); RegionComposition regionComposition = subtitleService.regions.get(regionId); @@ -163,9 +167,7 @@ import java.util.List; displayDefinition.horizontalPositionMaximum); int clipBottom = Math.min(baseVerticalAddress + regionComposition.height, displayDefinition.verticalPositionMaximum); - canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom, - Region.Op.REPLACE); - + canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom); ClutDefinition clutDefinition = subtitleService.cluts.get(regionComposition.clutId); if (clutDefinition == null) { clutDefinition = subtitleService.ancillaryCluts.get(regionComposition.clutId); @@ -183,7 +185,7 @@ import java.util.List; objectData = subtitleService.ancillaryObjects.get(objectId); } if (objectData != null) { - Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint; + @Nullable Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint; paintPixelDataSubBlocks(objectData, clutDefinition, regionComposition.depth, baseHorizontalAddress + regionObject.horizontalPosition, baseVerticalAddress + regionObject.verticalPosition, paint, canvas); @@ -214,9 +216,11 @@ import java.util.List; (float) regionComposition.height / displayDefinition.height)); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + // Restore clean clipping state. + canvas.restore(); } - return cues; + return Collections.unmodifiableList(cues); } // Static parsing. @@ -247,7 +251,7 @@ import java.util.List; break; case SEGMENT_TYPE_PAGE_COMPOSITION: if (pageId == service.subtitlePageId) { - PageComposition current = service.pageComposition; + @Nullable PageComposition current = service.pageComposition; PageComposition pageComposition = parsePageComposition(data, dataFieldLength); if (pageComposition.state != PAGE_STATE_NORMAL) { service.pageComposition = pageComposition; @@ -260,11 +264,15 @@ import java.util.List; } break; case SEGMENT_TYPE_REGION_COMPOSITION: - PageComposition pageComposition = service.pageComposition; + @Nullable PageComposition pageComposition = service.pageComposition; if (pageId == service.subtitlePageId && pageComposition != null) { RegionComposition regionComposition = parseRegionComposition(data, dataFieldLength); if (pageComposition.state == PAGE_STATE_NORMAL) { - regionComposition.mergeFrom(service.regions.get(regionComposition.id)); + @Nullable + RegionComposition existingRegionComposition = service.regions.get(regionComposition.id); + if (existingRegionComposition != null) { + regionComposition.mergeFrom(existingRegionComposition); + } } service.regions.put(regionComposition.id, regionComposition); } @@ -469,8 +477,8 @@ import java.util.List; boolean nonModifyingColorFlag = data.readBit(); data.skipBits(1); // Skip reserved. - byte[] topFieldData = null; - byte[] bottomFieldData = null; + @Nullable byte[] topFieldData = null; + @Nullable byte[] bottomFieldData = null; if (objectCodingMethod == OBJECT_CODING_STRING) { int numberOfCodes = data.readBits(8); @@ -576,11 +584,15 @@ import java.util.List; // Static drawing. - /** - * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. - */ - private static void paintPixelDataSubBlocks(ObjectData objectData, ClutDefinition clutDefinition, - int regionDepth, int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) { + /** Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. */ + private static void paintPixelDataSubBlocks( + ObjectData objectData, + ClutDefinition clutDefinition, + int regionDepth, + int horizontalAddress, + int verticalAddress, + @Nullable Paint paint, + Canvas canvas) { int[] clutEntries; if (regionDepth == REGION_DEPTH_8_BIT) { clutEntries = clutDefinition.clutEntries8Bit; @@ -595,23 +607,27 @@ import java.util.List; verticalAddress + 1, paint, canvas); } - /** - * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. - */ - private static void paintPixelDataSubBlock(byte[] pixelData, int[] clutEntries, int regionDepth, - int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) { + /** Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. */ + private static void paintPixelDataSubBlock( + byte[] pixelData, + int[] clutEntries, + int regionDepth, + int horizontalAddress, + int verticalAddress, + @Nullable Paint paint, + Canvas canvas) { ParsableBitArray data = new ParsableBitArray(pixelData); int column = horizontalAddress; int line = verticalAddress; - byte[] clutMapTable2To4 = null; - byte[] clutMapTable2To8 = null; - byte[] clutMapTable4To8 = null; + @Nullable byte[] clutMapTable2To4 = null; + @Nullable byte[] clutMapTable2To8 = null; + @Nullable byte[] clutMapTable4To8 = null; while (data.bitsLeft() != 0) { int dataType = data.readBits(8); switch (dataType) { case DATA_TYPE_2BP_CODE_STRING: - byte[] clutMapTable2ToX; + @Nullable byte[] clutMapTable2ToX; if (regionDepth == REGION_DEPTH_8_BIT) { clutMapTable2ToX = clutMapTable2To8 == null ? defaultMap2To8 : clutMapTable2To8; } else if (regionDepth == REGION_DEPTH_4_BIT) { @@ -624,7 +640,7 @@ import java.util.List; data.byteAlign(); break; case DATA_TYPE_4BP_CODE_STRING: - byte[] clutMapTable4ToX; + @Nullable byte[] clutMapTable4ToX; if (regionDepth == REGION_DEPTH_8_BIT) { clutMapTable4ToX = clutMapTable4To8 == null ? defaultMap4To8 : clutMapTable4To8; } else { @@ -635,7 +651,9 @@ import java.util.List; data.byteAlign(); break; case DATA_TYPE_8BP_CODE_STRING: - column = paint8BitPixelCodeString(data, clutEntries, null, column, line, paint, canvas); + column = + paint8BitPixelCodeString( + data, clutEntries, /* clutMapTable= */ null, column, line, paint, canvas); break; case DATA_TYPE_24_TABLE_DATA: clutMapTable2To4 = buildClutMapTable(4, 4, data); @@ -644,7 +662,7 @@ import java.util.List; clutMapTable2To8 = buildClutMapTable(4, 8, data); break; case DATA_TYPE_48_TABLE_DATA: - clutMapTable2To8 = buildClutMapTable(16, 8, data); + clutMapTable4To8 = buildClutMapTable(16, 8, data); break; case DATA_TYPE_END_LINE: column = horizontalAddress; @@ -657,23 +675,29 @@ import java.util.List; } } - /** - * Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. - */ - private static int paint2BitPixelCodeString(ParsableBitArray data, int[] clutEntries, - byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + /** Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint2BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { boolean endOfPixelCodeString = false; do { int runLength = 0; int clutIndex = 0; int peek = data.readBits(2); - if (!data.readBit()) { + if (peek != 0x00) { runLength = 1; clutIndex = peek; } else if (data.readBit()) { runLength = 3 + data.readBits(3); clutIndex = data.readBits(2); - } else if (!data.readBit()) { + } else if (data.readBit()) { + runLength = 1; + } else { switch (data.readBits(2)) { case 0x00: endOfPixelCodeString = true; @@ -703,11 +727,15 @@ import java.util.List; return column; } - /** - * Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. - */ - private static int paint4BitPixelCodeString(ParsableBitArray data, int[] clutEntries, - byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + /** Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint4BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { boolean endOfPixelCodeString = false; do { int runLength = 0; @@ -757,11 +785,15 @@ import java.util.List; return column; } - /** - * Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. - */ - private static int paint8BitPixelCodeString(ParsableBitArray data, int[] clutEntries, - byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + /** Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint8BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { boolean endOfPixelCodeString = false; do { int runLength = 0; @@ -813,18 +845,23 @@ import java.util.List; public final int subtitlePageId; public final int ancillaryPageId; - public final SparseArray regions = new SparseArray<>(); - public final SparseArray cluts = new SparseArray<>(); - public final SparseArray objects = new SparseArray<>(); - public final SparseArray ancillaryCluts = new SparseArray<>(); - public final SparseArray ancillaryObjects = new SparseArray<>(); + public final SparseArray regions; + public final SparseArray cluts; + public final SparseArray objects; + public final SparseArray ancillaryCluts; + public final SparseArray ancillaryObjects; - public DisplayDefinition displayDefinition; - public PageComposition pageComposition; + @Nullable public DisplayDefinition displayDefinition; + @Nullable public PageComposition pageComposition; public SubtitleService(int subtitlePageId, int ancillaryPageId) { this.subtitlePageId = subtitlePageId; this.ancillaryPageId = ancillaryPageId; + regions = new SparseArray<>(); + cluts = new SparseArray<>(); + objects = new SparseArray<>(); + ancillaryCluts = new SparseArray<>(); + ancillaryObjects = new SparseArray<>(); } public void reset() { @@ -941,9 +978,6 @@ import java.util.List; } public void mergeFrom(RegionComposition otherRegionComposition) { - if (otherRegionComposition == null) { - return; - } SparseArray otherRegionObjects = otherRegionComposition.regionObjects; for (int i = 0; i < otherRegionObjects.size(); i++) { regionObjects.put(otherRegionObjects.keyAt(i), otherRegionObjects.valueAt(i)); diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/package-info.java new file mode 100644 index 000000000000..be6b16c5e631 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.text.dvb; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/package-info.java new file mode 100644 index 000000000000..0b6e0d1f8c1a --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.text; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/PgsDecoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/PgsDecoder.java new file mode 100644 index 000000000000..859d240e9ba3 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/PgsDecoder.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.text.pgs; + +import android.graphics.Bitmap; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleDecoderException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.zip.Inflater; + +/** A {@link SimpleSubtitleDecoder} for PGS subtitles. */ +public final class PgsDecoder extends SimpleSubtitleDecoder { + + private static final int SECTION_TYPE_PALETTE = 0x14; + private static final int SECTION_TYPE_BITMAP_PICTURE = 0x15; + private static final int SECTION_TYPE_IDENTIFIER = 0x16; + private static final int SECTION_TYPE_END = 0x80; + + private static final byte INFLATE_HEADER = 0x78; + + private final ParsableByteArray buffer; + private final ParsableByteArray inflatedBuffer; + private final CueBuilder cueBuilder; + + @Nullable private Inflater inflater; + + public PgsDecoder() { + super("PgsDecoder"); + buffer = new ParsableByteArray(); + inflatedBuffer = new ParsableByteArray(); + cueBuilder = new CueBuilder(); + } + + @Override + protected Subtitle decode(byte[] data, int size, boolean reset) throws SubtitleDecoderException { + buffer.reset(data, size); + maybeInflateData(buffer); + cueBuilder.reset(); + ArrayList cues = new ArrayList<>(); + while (buffer.bytesLeft() >= 3) { + Cue cue = readNextSection(buffer, cueBuilder); + if (cue != null) { + cues.add(cue); + } + } + return new PgsSubtitle(Collections.unmodifiableList(cues)); + } + + private void maybeInflateData(ParsableByteArray buffer) { + if (buffer.bytesLeft() > 0 && buffer.peekUnsignedByte() == INFLATE_HEADER) { + if (inflater == null) { + inflater = new Inflater(); + } + if (Util.inflate(buffer, inflatedBuffer, inflater)) { + buffer.reset(inflatedBuffer.data, inflatedBuffer.limit()); + } // else assume data is not compressed. + } + } + + @Nullable + private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) { + int limit = buffer.limit(); + int sectionType = buffer.readUnsignedByte(); + int sectionLength = buffer.readUnsignedShort(); + + int nextSectionPosition = buffer.getPosition() + sectionLength; + if (nextSectionPosition > limit) { + buffer.setPosition(limit); + return null; + } + + Cue cue = null; + switch (sectionType) { + case SECTION_TYPE_PALETTE: + cueBuilder.parsePaletteSection(buffer, sectionLength); + break; + case SECTION_TYPE_BITMAP_PICTURE: + cueBuilder.parseBitmapSection(buffer, sectionLength); + break; + case SECTION_TYPE_IDENTIFIER: + cueBuilder.parseIdentifierSection(buffer, sectionLength); + break; + case SECTION_TYPE_END: + cue = cueBuilder.build(); + cueBuilder.reset(); + break; + default: + break; + } + + buffer.setPosition(nextSectionPosition); + return cue; + } + + private static final class CueBuilder { + + private final ParsableByteArray bitmapData; + private final int[] colors; + + private boolean colorsSet; + private int planeWidth; + private int planeHeight; + private int bitmapX; + private int bitmapY; + private int bitmapWidth; + private int bitmapHeight; + + public CueBuilder() { + bitmapData = new ParsableByteArray(); + colors = new int[256]; + } + + private void parsePaletteSection(ParsableByteArray buffer, int sectionLength) { + if ((sectionLength % 5) != 2) { + // Section must be two bytes followed by a whole number of (index, y, cb, cr, a) entries. + return; + } + buffer.skipBytes(2); + + Arrays.fill(colors, 0); + int entryCount = sectionLength / 5; + for (int i = 0; i < entryCount; i++) { + int index = buffer.readUnsignedByte(); + int y = buffer.readUnsignedByte(); + int cr = buffer.readUnsignedByte(); + int cb = buffer.readUnsignedByte(); + int a = buffer.readUnsignedByte(); + int r = (int) (y + (1.40200 * (cr - 128))); + int g = (int) (y - (0.34414 * (cb - 128)) - (0.71414 * (cr - 128))); + int b = (int) (y + (1.77200 * (cb - 128))); + colors[index] = + (a << 24) + | (Util.constrainValue(r, 0, 255) << 16) + | (Util.constrainValue(g, 0, 255) << 8) + | Util.constrainValue(b, 0, 255); + } + colorsSet = true; + } + + private void parseBitmapSection(ParsableByteArray buffer, int sectionLength) { + if (sectionLength < 4) { + return; + } + buffer.skipBytes(3); // Id (2 bytes), version (1 byte). + boolean isBaseSection = (0x80 & buffer.readUnsignedByte()) != 0; + sectionLength -= 4; + + if (isBaseSection) { + if (sectionLength < 7) { + return; + } + int totalLength = buffer.readUnsignedInt24(); + if (totalLength < 4) { + return; + } + bitmapWidth = buffer.readUnsignedShort(); + bitmapHeight = buffer.readUnsignedShort(); + bitmapData.reset(totalLength - 4); + sectionLength -= 7; + } + + int position = bitmapData.getPosition(); + int limit = bitmapData.limit(); + if (position < limit && sectionLength > 0) { + int bytesToRead = Math.min(sectionLength, limit - position); + buffer.readBytes(bitmapData.data, position, bytesToRead); + bitmapData.setPosition(position + bytesToRead); + } + } + + private void parseIdentifierSection(ParsableByteArray buffer, int sectionLength) { + if (sectionLength < 19) { + return; + } + planeWidth = buffer.readUnsignedShort(); + planeHeight = buffer.readUnsignedShort(); + buffer.skipBytes(11); + bitmapX = buffer.readUnsignedShort(); + bitmapY = buffer.readUnsignedShort(); + } + + @Nullable + public Cue build() { + if (planeWidth == 0 + || planeHeight == 0 + || bitmapWidth == 0 + || bitmapHeight == 0 + || bitmapData.limit() == 0 + || bitmapData.getPosition() != bitmapData.limit() + || !colorsSet) { + return null; + } + // Build the bitmapData. + bitmapData.setPosition(0); + int[] argbBitmapData = new int[bitmapWidth * bitmapHeight]; + int argbBitmapDataIndex = 0; + while (argbBitmapDataIndex < argbBitmapData.length) { + int colorIndex = bitmapData.readUnsignedByte(); + if (colorIndex != 0) { + argbBitmapData[argbBitmapDataIndex++] = colors[colorIndex]; + } else { + int switchBits = bitmapData.readUnsignedByte(); + if (switchBits != 0) { + int runLength = + (switchBits & 0x40) == 0 + ? (switchBits & 0x3F) + : (((switchBits & 0x3F) << 8) | bitmapData.readUnsignedByte()); + int color = (switchBits & 0x80) == 0 ? 0 : colors[bitmapData.readUnsignedByte()]; + Arrays.fill( + argbBitmapData, argbBitmapDataIndex, argbBitmapDataIndex + runLength, color); + argbBitmapDataIndex += runLength; + } + } + } + Bitmap bitmap = + Bitmap.createBitmap(argbBitmapData, bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); + // Build the cue. + return new Cue( + bitmap, + (float) bitmapX / planeWidth, + Cue.ANCHOR_TYPE_START, + (float) bitmapY / planeHeight, + Cue.ANCHOR_TYPE_START, + (float) bitmapWidth / planeWidth, + (float) bitmapHeight / planeHeight); + } + + public void reset() { + planeWidth = 0; + planeHeight = 0; + bitmapX = 0; + bitmapY = 0; + bitmapWidth = 0; + bitmapHeight = 0; + bitmapData.reset(0); + colorsSet = false; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java new file mode 100644 index 000000000000..e875763a45de --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.text.pgs; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle; +import java.util.List; + +/** A representation of a PGS subtitle. */ +/* package */ final class PgsSubtitle implements Subtitle { + + private final List cues; + + public PgsSubtitle(List cues) { + this.cues = cues; + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + return C.INDEX_UNSET; + } + + @Override + public int getEventTimeCount() { + return 1; + } + + @Override + public long getEventTime(int index) { + return 0; + } + + @Override + public List getCues(long timeUs) { + return cues; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/package-info.java new file mode 100644 index 000000000000..ce385ea085b5 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.text.pgs; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaDecoder.java new file mode 100644 index 000000000000..8f878a998edf --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -0,0 +1,446 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.text.ssa; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.text.Layout; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** A {@link SimpleSubtitleDecoder} for SSA/ASS. */ +public final class SsaDecoder extends SimpleSubtitleDecoder { + + private static final String TAG = "SsaDecoder"; + + private static final Pattern SSA_TIMECODE_PATTERN = + Pattern.compile("(?:(\\d+):)?(\\d+):(\\d+)[:.](\\d+)"); + + /* package */ static final String FORMAT_LINE_PREFIX = "Format:"; + /* package */ static final String STYLE_LINE_PREFIX = "Style:"; + private static final String DIALOGUE_LINE_PREFIX = "Dialogue:"; + + private static final float DEFAULT_MARGIN = 0.05f; + + private final boolean haveInitializationData; + @Nullable private final SsaDialogueFormat dialogueFormatFromInitializationData; + + private @MonotonicNonNull Map styles; + + /** + * The horizontal resolution used by the subtitle author - all cue positions are relative to this. + * + *

Parsed from the {@code PlayResX} value in the {@code [Script Info]} section. + */ + private float screenWidth; + /** + * The vertical resolution used by the subtitle author - all cue positions are relative to this. + * + *

Parsed from the {@code PlayResY} value in the {@code [Script Info]} section. + */ + private float screenHeight; + + public SsaDecoder() { + this(/* initializationData= */ null); + } + + /** + * Constructs an SsaDecoder with optional format and header info. + * + * @param initializationData Optional initialization data for the decoder. If not null or empty, + * the initialization data must consist of two byte arrays. The first must contain an SSA + * format line. The second must contain an SSA header that will be assumed common to all + * samples. The header is everything in an SSA file before the {@code [Events]} section (i.e. + * {@code [Script Info]} and optional {@code [V4+ Styles]} section. + */ + public SsaDecoder(@Nullable List initializationData) { + super("SsaDecoder"); + screenWidth = Cue.DIMEN_UNSET; + screenHeight = Cue.DIMEN_UNSET; + + if (initializationData != null && !initializationData.isEmpty()) { + haveInitializationData = true; + String formatLine = Util.fromUtf8Bytes(initializationData.get(0)); + Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); + dialogueFormatFromInitializationData = + Assertions.checkNotNull(SsaDialogueFormat.fromFormatLine(formatLine)); + parseHeader(new ParsableByteArray(initializationData.get(1))); + } else { + haveInitializationData = false; + dialogueFormatFromInitializationData = null; + } + } + + @Override + protected Subtitle decode(byte[] bytes, int length, boolean reset) { + List> cues = new ArrayList<>(); + List cueTimesUs = new ArrayList<>(); + + ParsableByteArray data = new ParsableByteArray(bytes, length); + if (!haveInitializationData) { + parseHeader(data); + } + parseEventBody(data, cues, cueTimesUs); + return new SsaSubtitle(cues, cueTimesUs); + } + + /** + * Parses the header of the subtitle. + * + * @param data A {@link ParsableByteArray} from which the header should be read. + */ + private void parseHeader(ParsableByteArray data) { + @Nullable String currentLine; + while ((currentLine = data.readLine()) != null) { + if ("[Script Info]".equalsIgnoreCase(currentLine)) { + parseScriptInfo(data); + } else if ("[V4+ Styles]".equalsIgnoreCase(currentLine)) { + styles = parseStyles(data); + } else if ("[V4 Styles]".equalsIgnoreCase(currentLine)) { + Log.i(TAG, "[V4 Styles] are not supported"); + } else if ("[Events]".equalsIgnoreCase(currentLine)) { + // We've reached the [Events] section, so the header is over. + return; + } + } + } + + /** + * Parse the {@code [Script Info]} section. + * + *

When this returns, {@code data.position} will be set to the beginning of the first line that + * starts with {@code [} (i.e. the title of the next section). + * + * @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition() position} + * set to the beginning of of the first line after {@code [Script Info]}. + */ + private void parseScriptInfo(ParsableByteArray data) { + @Nullable String currentLine; + while ((currentLine = data.readLine()) != null + && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) { + String[] infoNameAndValue = currentLine.split(":"); + if (infoNameAndValue.length != 2) { + continue; + } + switch (Util.toLowerInvariant(infoNameAndValue[0].trim())) { + case "playresx": + try { + screenWidth = Float.parseFloat(infoNameAndValue[1].trim()); + } catch (NumberFormatException e) { + // Ignore invalid PlayResX value. + } + break; + case "playresy": + try { + screenHeight = Float.parseFloat(infoNameAndValue[1].trim()); + } catch (NumberFormatException e) { + // Ignore invalid PlayResY value. + } + break; + } + } + } + + /** + * Parse the {@code [V4+ Styles]} section. + * + *

When this returns, {@code data.position} will be set to the beginning of the first line that + * starts with {@code [} (i.e. the title of the next section). + * + * @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition()} pointing + * at the beginning of of the first line after {@code [V4+ Styles]}. + */ + private static Map parseStyles(ParsableByteArray data) { + Map styles = new LinkedHashMap<>(); + @Nullable SsaStyle.Format formatInfo = null; + @Nullable String currentLine; + while ((currentLine = data.readLine()) != null + && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) { + if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { + formatInfo = SsaStyle.Format.fromFormatLine(currentLine); + } else if (currentLine.startsWith(STYLE_LINE_PREFIX)) { + if (formatInfo == null) { + Log.w(TAG, "Skipping 'Style:' line before 'Format:' line: " + currentLine); + continue; + } + @Nullable SsaStyle style = SsaStyle.fromStyleLine(currentLine, formatInfo); + if (style != null) { + styles.put(style.name, style); + } + } + } + return styles; + } + + /** + * Parses the event body of the subtitle. + * + * @param data A {@link ParsableByteArray} from which the body should be read. + * @param cues A list to which parsed cues will be added. + * @param cueTimesUs A sorted list to which parsed cue timestamps will be added. + */ + private void parseEventBody(ParsableByteArray data, List> cues, List cueTimesUs) { + @Nullable + SsaDialogueFormat format = haveInitializationData ? dialogueFormatFromInitializationData : null; + @Nullable String currentLine; + while ((currentLine = data.readLine()) != null) { + if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { + format = SsaDialogueFormat.fromFormatLine(currentLine); + } else if (currentLine.startsWith(DIALOGUE_LINE_PREFIX)) { + if (format == null) { + Log.w(TAG, "Skipping dialogue line before complete format: " + currentLine); + continue; + } + parseDialogueLine(currentLine, format, cues, cueTimesUs); + } + } + } + + /** + * Parses a dialogue line. + * + * @param dialogueLine The dialogue values (i.e. everything after {@code Dialogue:}). + * @param format The dialogue format to use when parsing {@code dialogueLine}. + * @param cues A list to which parsed cues will be added. + * @param cueTimesUs A sorted list to which parsed cue timestamps will be added. + */ + private void parseDialogueLine( + String dialogueLine, SsaDialogueFormat format, List> cues, List cueTimesUs) { + Assertions.checkArgument(dialogueLine.startsWith(DIALOGUE_LINE_PREFIX)); + String[] lineValues = + dialogueLine.substring(DIALOGUE_LINE_PREFIX.length()).split(",", format.length); + if (lineValues.length != format.length) { + Log.w(TAG, "Skipping dialogue line with fewer columns than format: " + dialogueLine); + return; + } + + long startTimeUs = parseTimecodeUs(lineValues[format.startTimeIndex]); + if (startTimeUs == C.TIME_UNSET) { + Log.w(TAG, "Skipping invalid timing: " + dialogueLine); + return; + } + + long endTimeUs = parseTimecodeUs(lineValues[format.endTimeIndex]); + if (endTimeUs == C.TIME_UNSET) { + Log.w(TAG, "Skipping invalid timing: " + dialogueLine); + return; + } + + @Nullable + SsaStyle style = + styles != null && format.styleIndex != C.INDEX_UNSET + ? styles.get(lineValues[format.styleIndex].trim()) + : null; + String rawText = lineValues[format.textIndex]; + SsaStyle.Overrides styleOverrides = SsaStyle.Overrides.parseFromDialogue(rawText); + String text = + SsaStyle.Overrides.stripStyleOverrides(rawText) + .replaceAll("\\\\N", "\n") + .replaceAll("\\\\n", "\n"); + Cue cue = createCue(text, style, styleOverrides, screenWidth, screenHeight); + + int startTimeIndex = addCuePlacerholderByTime(startTimeUs, cueTimesUs, cues); + int endTimeIndex = addCuePlacerholderByTime(endTimeUs, cueTimesUs, cues); + // Iterate on cues from startTimeIndex until endTimeIndex, adding the current cue. + for (int i = startTimeIndex; i < endTimeIndex; i++) { + cues.get(i).add(cue); + } + } + + /** + * Parses an SSA timecode string. + * + * @param timeString The string to parse. + * @return The parsed timestamp in microseconds. + */ + private static long parseTimecodeUs(String timeString) { + Matcher matcher = SSA_TIMECODE_PATTERN.matcher(timeString.trim()); + if (!matcher.matches()) { + return C.TIME_UNSET; + } + long timestampUs = + Long.parseLong(castNonNull(matcher.group(1))) * 60 * 60 * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(castNonNull(matcher.group(2))) * 60 * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(castNonNull(matcher.group(3))) * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(castNonNull(matcher.group(4))) * 10000; // 100ths of a second. + return timestampUs; + } + + private static Cue createCue( + String text, + @Nullable SsaStyle style, + SsaStyle.Overrides styleOverrides, + float screenWidth, + float screenHeight) { + @SsaStyle.SsaAlignment int alignment; + if (styleOverrides.alignment != SsaStyle.SSA_ALIGNMENT_UNKNOWN) { + alignment = styleOverrides.alignment; + } else if (style != null) { + alignment = style.alignment; + } else { + alignment = SsaStyle.SSA_ALIGNMENT_UNKNOWN; + } + @Cue.AnchorType int positionAnchor = toPositionAnchor(alignment); + @Cue.AnchorType int lineAnchor = toLineAnchor(alignment); + + float position; + float line; + if (styleOverrides.position != null + && screenHeight != Cue.DIMEN_UNSET + && screenWidth != Cue.DIMEN_UNSET) { + position = styleOverrides.position.x / screenWidth; + line = styleOverrides.position.y / screenHeight; + } else { + // TODO: Read the MarginL, MarginR and MarginV values from the Style & Dialogue lines. + position = computeDefaultLineOrPosition(positionAnchor); + line = computeDefaultLineOrPosition(lineAnchor); + } + + return new Cue( + text, + toTextAlignment(alignment), + line, + Cue.LINE_TYPE_FRACTION, + lineAnchor, + position, + positionAnchor, + /* size= */ Cue.DIMEN_UNSET); + } + + @Nullable + private static Layout.Alignment toTextAlignment(@SsaStyle.SsaAlignment int alignment) { + switch (alignment) { + case SsaStyle.SSA_ALIGNMENT_BOTTOM_LEFT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_LEFT: + case SsaStyle.SSA_ALIGNMENT_TOP_LEFT: + return Layout.Alignment.ALIGN_NORMAL; + case SsaStyle.SSA_ALIGNMENT_BOTTOM_CENTER: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_CENTER: + case SsaStyle.SSA_ALIGNMENT_TOP_CENTER: + return Layout.Alignment.ALIGN_CENTER; + case SsaStyle.SSA_ALIGNMENT_BOTTOM_RIGHT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_RIGHT: + case SsaStyle.SSA_ALIGNMENT_TOP_RIGHT: + return Layout.Alignment.ALIGN_OPPOSITE; + case SsaStyle.SSA_ALIGNMENT_UNKNOWN: + return null; + default: + Log.w(TAG, "Unknown alignment: " + alignment); + return null; + } + } + + @Cue.AnchorType + private static int toLineAnchor(@SsaStyle.SsaAlignment int alignment) { + switch (alignment) { + case SsaStyle.SSA_ALIGNMENT_BOTTOM_LEFT: + case SsaStyle.SSA_ALIGNMENT_BOTTOM_CENTER: + case SsaStyle.SSA_ALIGNMENT_BOTTOM_RIGHT: + return Cue.ANCHOR_TYPE_END; + case SsaStyle.SSA_ALIGNMENT_MIDDLE_LEFT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_CENTER: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_RIGHT: + return Cue.ANCHOR_TYPE_MIDDLE; + case SsaStyle.SSA_ALIGNMENT_TOP_LEFT: + case SsaStyle.SSA_ALIGNMENT_TOP_CENTER: + case SsaStyle.SSA_ALIGNMENT_TOP_RIGHT: + return Cue.ANCHOR_TYPE_START; + case SsaStyle.SSA_ALIGNMENT_UNKNOWN: + return Cue.TYPE_UNSET; + default: + Log.w(TAG, "Unknown alignment: " + alignment); + return Cue.TYPE_UNSET; + } + } + + @Cue.AnchorType + private static int toPositionAnchor(@SsaStyle.SsaAlignment int alignment) { + switch (alignment) { + case SsaStyle.SSA_ALIGNMENT_BOTTOM_LEFT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_LEFT: + case SsaStyle.SSA_ALIGNMENT_TOP_LEFT: + return Cue.ANCHOR_TYPE_START; + case SsaStyle.SSA_ALIGNMENT_BOTTOM_CENTER: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_CENTER: + case SsaStyle.SSA_ALIGNMENT_TOP_CENTER: + return Cue.ANCHOR_TYPE_MIDDLE; + case SsaStyle.SSA_ALIGNMENT_BOTTOM_RIGHT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_RIGHT: + case SsaStyle.SSA_ALIGNMENT_TOP_RIGHT: + return Cue.ANCHOR_TYPE_END; + case SsaStyle.SSA_ALIGNMENT_UNKNOWN: + return Cue.TYPE_UNSET; + default: + Log.w(TAG, "Unknown alignment: " + alignment); + return Cue.TYPE_UNSET; + } + } + + private static float computeDefaultLineOrPosition(@Cue.AnchorType int anchor) { + switch (anchor) { + case Cue.ANCHOR_TYPE_START: + return DEFAULT_MARGIN; + case Cue.ANCHOR_TYPE_MIDDLE: + return 0.5f; + case Cue.ANCHOR_TYPE_END: + return 1.0f - DEFAULT_MARGIN; + case Cue.TYPE_UNSET: + default: + return Cue.DIMEN_UNSET; + } + } + + /** + * Searches for {@code timeUs} in {@code sortedCueTimesUs}, inserting it if it's not found, and + * returns the index. + * + *

If it's inserted, we also insert a matching entry to {@code cues}. + */ + private static int addCuePlacerholderByTime( + long timeUs, List sortedCueTimesUs, List> cues) { + int insertionIndex = 0; + for (int i = sortedCueTimesUs.size() - 1; i >= 0; i--) { + if (sortedCueTimesUs.get(i) == timeUs) { + return i; + } + + if (sortedCueTimesUs.get(i) < timeUs) { + insertionIndex = i + 1; + break; + } + } + sortedCueTimesUs.add(insertionIndex, timeUs); + // Copy over cues from left, or use an empty list if we're inserting at the beginning. + cues.add( + insertionIndex, + insertionIndex == 0 ? new ArrayList<>() : new ArrayList<>(cues.get(insertionIndex - 1))); + return insertionIndex; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java new file mode 100644 index 000000000000..312c779e23c3 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java @@ -0,0 +1,83 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.text.ssa; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.text.ssa.SsaDecoder.FORMAT_LINE_PREFIX; + +import android.text.TextUtils; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** + * Represents a {@code Format:} line from the {@code [Events]} section + * + *

The indices are used to determine the location of particular properties in each {@code + * Dialogue:} line. + */ +/* package */ final class SsaDialogueFormat { + + public final int startTimeIndex; + public final int endTimeIndex; + public final int styleIndex; + public final int textIndex; + public final int length; + + private SsaDialogueFormat( + int startTimeIndex, int endTimeIndex, int styleIndex, int textIndex, int length) { + this.startTimeIndex = startTimeIndex; + this.endTimeIndex = endTimeIndex; + this.styleIndex = styleIndex; + this.textIndex = textIndex; + this.length = length; + } + + /** + * Parses the format info from a 'Format:' line in the [Events] section. + * + * @return the parsed info, or null if {@code formatLine} doesn't contain both 'start' and 'end'. + */ + @Nullable + public static SsaDialogueFormat fromFormatLine(String formatLine) { + int startTimeIndex = C.INDEX_UNSET; + int endTimeIndex = C.INDEX_UNSET; + int styleIndex = C.INDEX_UNSET; + int textIndex = C.INDEX_UNSET; + Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); + String[] keys = TextUtils.split(formatLine.substring(FORMAT_LINE_PREFIX.length()), ","); + for (int i = 0; i < keys.length; i++) { + switch (Util.toLowerInvariant(keys[i].trim())) { + case "start": + startTimeIndex = i; + break; + case "end": + endTimeIndex = i; + break; + case "style": + styleIndex = i; + break; + case "text": + textIndex = i; + break; + } + } + return (startTimeIndex != C.INDEX_UNSET && endTimeIndex != C.INDEX_UNSET) + ? new SsaDialogueFormat(startTimeIndex, endTimeIndex, styleIndex, textIndex, keys.length) + : null; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaStyle.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaStyle.java new file mode 100644 index 000000000000..3c3639a3fb61 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaStyle.java @@ -0,0 +1,301 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.text.ssa; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.text.ssa.SsaDecoder.STYLE_LINE_PREFIX; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.graphics.PointF; +import android.text.TextUtils; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Represents a line from an SSA/ASS {@code [V4+ Styles]} section. */ +/* package */ final class SsaStyle { + + private static final String TAG = "SsaStyle"; + + /** + * The SSA/ASS alignments. + * + *

Allowed values: + * + *

    + *
  • {@link #SSA_ALIGNMENT_UNKNOWN} + *
  • {@link #SSA_ALIGNMENT_BOTTOM_LEFT} + *
  • {@link #SSA_ALIGNMENT_BOTTOM_CENTER} + *
  • {@link #SSA_ALIGNMENT_BOTTOM_RIGHT} + *
  • {@link #SSA_ALIGNMENT_MIDDLE_LEFT} + *
  • {@link #SSA_ALIGNMENT_MIDDLE_CENTER} + *
  • {@link #SSA_ALIGNMENT_MIDDLE_RIGHT} + *
  • {@link #SSA_ALIGNMENT_TOP_LEFT} + *
  • {@link #SSA_ALIGNMENT_TOP_CENTER} + *
  • {@link #SSA_ALIGNMENT_TOP_RIGHT} + *
+ */ + @IntDef({ + SSA_ALIGNMENT_UNKNOWN, + SSA_ALIGNMENT_BOTTOM_LEFT, + SSA_ALIGNMENT_BOTTOM_CENTER, + SSA_ALIGNMENT_BOTTOM_RIGHT, + SSA_ALIGNMENT_MIDDLE_LEFT, + SSA_ALIGNMENT_MIDDLE_CENTER, + SSA_ALIGNMENT_MIDDLE_RIGHT, + SSA_ALIGNMENT_TOP_LEFT, + SSA_ALIGNMENT_TOP_CENTER, + SSA_ALIGNMENT_TOP_RIGHT, + }) + @Documented + @Retention(SOURCE) + public @interface SsaAlignment {} + + // The numbering follows the ASS (v4+) spec (i.e. the points on the number pad). + public static final int SSA_ALIGNMENT_UNKNOWN = -1; + public static final int SSA_ALIGNMENT_BOTTOM_LEFT = 1; + public static final int SSA_ALIGNMENT_BOTTOM_CENTER = 2; + public static final int SSA_ALIGNMENT_BOTTOM_RIGHT = 3; + public static final int SSA_ALIGNMENT_MIDDLE_LEFT = 4; + public static final int SSA_ALIGNMENT_MIDDLE_CENTER = 5; + public static final int SSA_ALIGNMENT_MIDDLE_RIGHT = 6; + public static final int SSA_ALIGNMENT_TOP_LEFT = 7; + public static final int SSA_ALIGNMENT_TOP_CENTER = 8; + public static final int SSA_ALIGNMENT_TOP_RIGHT = 9; + + public final String name; + @SsaAlignment public final int alignment; + + private SsaStyle(String name, @SsaAlignment int alignment) { + this.name = name; + this.alignment = alignment; + } + + @Nullable + public static SsaStyle fromStyleLine(String styleLine, Format format) { + Assertions.checkArgument(styleLine.startsWith(STYLE_LINE_PREFIX)); + String[] styleValues = TextUtils.split(styleLine.substring(STYLE_LINE_PREFIX.length()), ","); + if (styleValues.length != format.length) { + Log.w( + TAG, + Util.formatInvariant( + "Skipping malformed 'Style:' line (expected %s values, found %s): '%s'", + format.length, styleValues.length, styleLine)); + return null; + } + try { + return new SsaStyle( + styleValues[format.nameIndex].trim(), parseAlignment(styleValues[format.alignmentIndex])); + } catch (RuntimeException e) { + Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e); + return null; + } + } + + @SsaAlignment + private static int parseAlignment(String alignmentStr) { + try { + @SsaAlignment int alignment = Integer.parseInt(alignmentStr.trim()); + if (isValidAlignment(alignment)) { + return alignment; + } + } catch (NumberFormatException e) { + // Swallow the exception and return UNKNOWN below. + } + Log.w(TAG, "Ignoring unknown alignment: " + alignmentStr); + return SSA_ALIGNMENT_UNKNOWN; + } + + private static boolean isValidAlignment(@SsaAlignment int alignment) { + switch (alignment) { + case SSA_ALIGNMENT_BOTTOM_CENTER: + case SSA_ALIGNMENT_BOTTOM_LEFT: + case SSA_ALIGNMENT_BOTTOM_RIGHT: + case SSA_ALIGNMENT_MIDDLE_CENTER: + case SSA_ALIGNMENT_MIDDLE_LEFT: + case SSA_ALIGNMENT_MIDDLE_RIGHT: + case SSA_ALIGNMENT_TOP_CENTER: + case SSA_ALIGNMENT_TOP_LEFT: + case SSA_ALIGNMENT_TOP_RIGHT: + return true; + case SSA_ALIGNMENT_UNKNOWN: + default: + return false; + } + } + + /** + * Represents a {@code Format:} line from the {@code [V4+ Styles]} section + * + *

The indices are used to determine the location of particular properties in each {@code + * Style:} line. + */ + /* package */ static final class Format { + + public final int nameIndex; + public final int alignmentIndex; + public final int length; + + private Format(int nameIndex, int alignmentIndex, int length) { + this.nameIndex = nameIndex; + this.alignmentIndex = alignmentIndex; + this.length = length; + } + + /** + * Parses the format info from a 'Format:' line in the [V4+ Styles] section. + * + * @return the parsed info, or null if {@code styleFormatLine} doesn't contain 'name'. + */ + @Nullable + public static Format fromFormatLine(String styleFormatLine) { + int nameIndex = C.INDEX_UNSET; + int alignmentIndex = C.INDEX_UNSET; + String[] keys = + TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ","); + for (int i = 0; i < keys.length; i++) { + switch (Util.toLowerInvariant(keys[i].trim())) { + case "name": + nameIndex = i; + break; + case "alignment": + alignmentIndex = i; + break; + } + } + return nameIndex != C.INDEX_UNSET ? new Format(nameIndex, alignmentIndex, keys.length) : null; + } + } + + /** + * Represents the style override information parsed from an SSA/ASS dialogue line. + * + *

Overrides are contained in braces embedded in the dialogue text of the cue. + */ + /* package */ static final class Overrides { + + private static final String TAG = "SsaStyle.Overrides"; + + /** Matches "{foo}" and returns "foo" in group 1 */ + // Warning that \\} can be replaced with } is bogus [internal: b/144480183]. + private static final Pattern BRACES_PATTERN = Pattern.compile("\\{([^}]*)\\}"); + + private static final String PADDED_DECIMAL_PATTERN = "\\s*\\d+(?:\\.\\d+)?\\s*"; + + /** Matches "\pos(x,y)" and returns "x" in group 1 and "y" in group 2 */ + private static final Pattern POSITION_PATTERN = + Pattern.compile(Util.formatInvariant("\\\\pos\\((%1$s),(%1$s)\\)", PADDED_DECIMAL_PATTERN)); + /** Matches "\move(x1,y1,x2,y2[,t1,t2])" and returns "x2" in group 1 and "y2" in group 2 */ + private static final Pattern MOVE_PATTERN = + Pattern.compile( + Util.formatInvariant( + "\\\\move\\(%1$s,%1$s,(%1$s),(%1$s)(?:,%1$s,%1$s)?\\)", PADDED_DECIMAL_PATTERN)); + + /** Matches "\anx" and returns x in group 1 */ + private static final Pattern ALIGNMENT_OVERRIDE_PATTERN = Pattern.compile("\\\\an(\\d+)"); + + @SsaAlignment public final int alignment; + @Nullable public final PointF position; + + private Overrides(@SsaAlignment int alignment, @Nullable PointF position) { + this.alignment = alignment; + this.position = position; + } + + public static Overrides parseFromDialogue(String text) { + @SsaAlignment int alignment = SSA_ALIGNMENT_UNKNOWN; + PointF position = null; + Matcher matcher = BRACES_PATTERN.matcher(text); + while (matcher.find()) { + String braceContents = matcher.group(1); + try { + PointF parsedPosition = parsePosition(braceContents); + if (parsedPosition != null) { + position = parsedPosition; + } + } catch (RuntimeException e) { + // Ignore invalid \pos() or \move() function. + } + try { + @SsaAlignment int parsedAlignment = parseAlignmentOverride(braceContents); + if (parsedAlignment != SSA_ALIGNMENT_UNKNOWN) { + alignment = parsedAlignment; + } + } catch (RuntimeException e) { + // Ignore invalid \an alignment override. + } + } + return new Overrides(alignment, position); + } + + public static String stripStyleOverrides(String dialogueLine) { + return BRACES_PATTERN.matcher(dialogueLine).replaceAll(""); + } + + /** + * Parses the position from a style override, returns null if no position is found. + * + *

The attribute is expected to be in the form {@code \pos(x,y)} or {@code + * \move(x1,y1,x2,y2,startTime,endTime)} (startTime and endTime are optional). In the case of + * {@code \move()}, this returns {@code (x2, y2)} (i.e. the end position of the move). + * + * @param styleOverride The string to parse. + * @return The parsed position, or null if no position is found. + */ + @Nullable + private static PointF parsePosition(String styleOverride) { + Matcher positionMatcher = POSITION_PATTERN.matcher(styleOverride); + Matcher moveMatcher = MOVE_PATTERN.matcher(styleOverride); + boolean hasPosition = positionMatcher.find(); + boolean hasMove = moveMatcher.find(); + + String x; + String y; + if (hasPosition) { + if (hasMove) { + Log.i( + TAG, + "Override has both \\pos(x,y) and \\move(x1,y1,x2,y2); using \\pos values. override='" + + styleOverride + + "'"); + } + x = positionMatcher.group(1); + y = positionMatcher.group(2); + } else if (hasMove) { + x = moveMatcher.group(1); + y = moveMatcher.group(2); + } else { + return null; + } + return new PointF( + Float.parseFloat(Assertions.checkNotNull(x).trim()), + Float.parseFloat(Assertions.checkNotNull(y).trim())); + } + + @SsaAlignment + private static int parseAlignmentOverride(String braceContents) { + Matcher matcher = ALIGNMENT_OVERRIDE_PATTERN.matcher(braceContents); + return matcher.find() ? parseAlignment(matcher.group(1)) : SSA_ALIGNMENT_UNKNOWN; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java new file mode 100644 index 000000000000..fb0544156d35 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.text.ssa; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.Collections; +import java.util.List; + +/** + * A representation of an SSA/ASS subtitle. + */ +/* package */ final class SsaSubtitle implements Subtitle { + + private final List> cues; + private final List cueTimesUs; + + /** + * @param cues The cues in the subtitle. + * @param cueTimesUs The cue times, in microseconds. + */ + public SsaSubtitle(List> cues, List cueTimesUs) { + this.cues = cues; + this.cueTimesUs = cueTimesUs; + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false); + return index < cueTimesUs.size() ? index : C.INDEX_UNSET; + } + + @Override + public int getEventTimeCount() { + return cueTimesUs.size(); + } + + @Override + public long getEventTime(int index) { + Assertions.checkArgument(index >= 0); + Assertions.checkArgument(index < cueTimesUs.size()); + return cueTimesUs.get(index); + } + + @Override + public List getCues(long timeUs) { + int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); + if (index == -1) { + // timeUs is earlier than the start of the first cue. + return Collections.emptyList(); + } else { + return cues.get(index); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/package-info.java new file mode 100644 index 000000000000..bc4b625d77f4 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.text.ssa; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index c5d502dc3454..36ebf6ead0c0 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -18,9 +18,11 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.text.subrip; import android.text.Html; import android.text.Spanned; import android.text.TextUtils; -import android.util.Log; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.LongArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; @@ -32,26 +34,49 @@ import java.util.regex.Pattern; */ public final class SubripDecoder extends SimpleSubtitleDecoder { + // Fractional positions for use when alignment tags are present. + private static final float START_FRACTION = 0.08f; + private static final float END_FRACTION = 1 - START_FRACTION; + private static final float MID_FRACTION = 0.5f; + private static final String TAG = "SubripDecoder"; - private static final String SUBRIP_TIMECODE = "(?:(\\d+):)?(\\d+):(\\d+),(\\d+)"; + // Some SRT files don't include hours or milliseconds in the timecode, so we use optional groups. + private static final String SUBRIP_TIMECODE = "(?:(\\d+):)?(\\d+):(\\d+)(?:,(\\d+))?"; private static final Pattern SUBRIP_TIMING_LINE = - Pattern.compile("\\s*(" + SUBRIP_TIMECODE + ")\\s*-->\\s*(" + SUBRIP_TIMECODE + ")?\\s*"); + Pattern.compile("\\s*(" + SUBRIP_TIMECODE + ")\\s*-->\\s*(" + SUBRIP_TIMECODE + ")\\s*"); + + // NOTE: Android Studio's suggestion to simplify '\\}' is incorrect [internal: b/144480183]. + private static final Pattern SUBRIP_TAG_PATTERN = Pattern.compile("\\{\\\\.*?\\}"); + private static final String SUBRIP_ALIGNMENT_TAG = "\\{\\\\an[1-9]\\}"; + + // Alignment tags for SSA V4+. + private static final String ALIGN_BOTTOM_LEFT = "{\\an1}"; + private static final String ALIGN_BOTTOM_MID = "{\\an2}"; + private static final String ALIGN_BOTTOM_RIGHT = "{\\an3}"; + private static final String ALIGN_MID_LEFT = "{\\an4}"; + private static final String ALIGN_MID_MID = "{\\an5}"; + private static final String ALIGN_MID_RIGHT = "{\\an6}"; + private static final String ALIGN_TOP_LEFT = "{\\an7}"; + private static final String ALIGN_TOP_MID = "{\\an8}"; + private static final String ALIGN_TOP_RIGHT = "{\\an9}"; private final StringBuilder textBuilder; + private final ArrayList tags; public SubripDecoder() { super("SubripDecoder"); textBuilder = new StringBuilder(); + tags = new ArrayList<>(); } @Override - protected SubripSubtitle decode(byte[] bytes, int length, boolean reset) { + protected Subtitle decode(byte[] bytes, int length, boolean reset) { ArrayList cues = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); ParsableByteArray subripData = new ParsableByteArray(bytes, length); - String currentLine; + @Nullable String currentLine; while ((currentLine = subripData.readLine()) != null) { if (currentLine.length() == 0) { // Skip blank lines. @@ -67,34 +92,46 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } // Read and parse the timing line. - boolean haveEndTimecode = false; currentLine = subripData.readLine(); + if (currentLine == null) { + Log.w(TAG, "Unexpected end"); + break; + } + Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine); if (matcher.matches()) { - cueTimesUs.add(parseTimecode(matcher, 1)); - if (!TextUtils.isEmpty(matcher.group(6))) { - haveEndTimecode = true; - cueTimesUs.add(parseTimecode(matcher, 6)); - } + cueTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 1)); + cueTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 6)); } else { Log.w(TAG, "Skipping invalid timing: " + currentLine); continue; } - // Read and parse the text. + // Read and parse the text and tags. textBuilder.setLength(0); - while (!TextUtils.isEmpty(currentLine = subripData.readLine())) { + tags.clear(); + currentLine = subripData.readLine(); + while (!TextUtils.isEmpty(currentLine)) { if (textBuilder.length() > 0) { textBuilder.append("
"); } - textBuilder.append(currentLine.trim()); + textBuilder.append(processLine(currentLine, tags)); + currentLine = subripData.readLine(); } Spanned text = Html.fromHtml(textBuilder.toString()); - cues.add(new Cue(text)); - if (haveEndTimecode) { - cues.add(null); + + @Nullable String alignmentTag = null; + for (int i = 0; i < tags.size(); i++) { + String tag = tags.get(i); + if (tag.matches(SUBRIP_ALIGNMENT_TAG)) { + alignmentTag = tag; + // Subsequent alignment tags should be ignored. + break; + } } + cues.add(buildCue(text, alignmentTag)); + cues.add(Cue.EMPTY); } Cue[] cuesArray = new Cue[cues.size()]; @@ -103,12 +140,120 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { return new SubripSubtitle(cuesArray, cueTimesUsArray); } + /** + * Trims and removes tags from the given line. The removed tags are added to {@code tags}. + * + * @param line The line to process. + * @param tags A list to which removed tags will be added. + * @return The processed line. + */ + private String processLine(String line, ArrayList tags) { + line = line.trim(); + + int removedCharacterCount = 0; + StringBuilder processedLine = new StringBuilder(line); + Matcher matcher = SUBRIP_TAG_PATTERN.matcher(line); + while (matcher.find()) { + String tag = matcher.group(); + tags.add(tag); + int start = matcher.start() - removedCharacterCount; + int tagLength = tag.length(); + processedLine.replace(start, /* end= */ start + tagLength, /* str= */ ""); + removedCharacterCount += tagLength; + } + + return processedLine.toString(); + } + + /** + * Build a {@link Cue} based on the given text and alignment tag. + * + * @param text The text. + * @param alignmentTag The alignment tag, or {@code null} if no alignment tag is available. + * @return Built cue + */ + private Cue buildCue(Spanned text, @Nullable String alignmentTag) { + if (alignmentTag == null) { + return new Cue(text); + } + + // Horizontal alignment. + @Cue.AnchorType int positionAnchor; + switch (alignmentTag) { + case ALIGN_BOTTOM_LEFT: + case ALIGN_MID_LEFT: + case ALIGN_TOP_LEFT: + positionAnchor = Cue.ANCHOR_TYPE_START; + break; + case ALIGN_BOTTOM_RIGHT: + case ALIGN_MID_RIGHT: + case ALIGN_TOP_RIGHT: + positionAnchor = Cue.ANCHOR_TYPE_END; + break; + case ALIGN_BOTTOM_MID: + case ALIGN_MID_MID: + case ALIGN_TOP_MID: + default: + positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; + break; + } + + // Vertical alignment. + @Cue.AnchorType int lineAnchor; + switch (alignmentTag) { + case ALIGN_BOTTOM_LEFT: + case ALIGN_BOTTOM_MID: + case ALIGN_BOTTOM_RIGHT: + lineAnchor = Cue.ANCHOR_TYPE_END; + break; + case ALIGN_TOP_LEFT: + case ALIGN_TOP_MID: + case ALIGN_TOP_RIGHT: + lineAnchor = Cue.ANCHOR_TYPE_START; + break; + case ALIGN_MID_LEFT: + case ALIGN_MID_MID: + case ALIGN_MID_RIGHT: + default: + lineAnchor = Cue.ANCHOR_TYPE_MIDDLE; + break; + } + + return new Cue( + text, + /* textAlignment= */ null, + getFractionalPositionForAnchorType(lineAnchor), + Cue.LINE_TYPE_FRACTION, + lineAnchor, + getFractionalPositionForAnchorType(positionAnchor), + positionAnchor, + Cue.DIMEN_UNSET); + } + private static long parseTimecode(Matcher matcher, int groupOffset) { - long timestampMs = Long.parseLong(matcher.group(groupOffset + 1)) * 60 * 60 * 1000; + @Nullable String hours = matcher.group(groupOffset + 1); + long timestampMs = hours != null ? Long.parseLong(hours) * 60 * 60 * 1000 : 0; timestampMs += Long.parseLong(matcher.group(groupOffset + 2)) * 60 * 1000; timestampMs += Long.parseLong(matcher.group(groupOffset + 3)) * 1000; - timestampMs += Long.parseLong(matcher.group(groupOffset + 4)); + @Nullable String millis = matcher.group(groupOffset + 4); + if (millis != null) { + timestampMs += Long.parseLong(millis); + } return timestampMs * 1000; } + /* package */ static float getFractionalPositionForAnchorType(@Cue.AnchorType int anchorType) { + switch (anchorType) { + case Cue.ANCHOR_TYPE_START: + return SubripDecoder.START_FRACTION; + case Cue.ANCHOR_TYPE_MIDDLE: + return SubripDecoder.MID_FRACTION; + case Cue.ANCHOR_TYPE_END: + return SubripDecoder.END_FRACTION; + case Cue.TYPE_UNSET: + default: + // Should never happen. + throw new IllegalArgumentException(); + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java index db661c6dd86a..d011f5d7c515 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java @@ -32,7 +32,7 @@ import java.util.List; private final long[] cueTimesUs; /** - * @param cues The cues in the subtitle. Null entries may be used to represent empty cues. + * @param cues The cues in the subtitle. * @param cueTimesUs The cue times, in microseconds. */ public SubripSubtitle(Cue[] cues, long[] cueTimesUs) { @@ -61,7 +61,7 @@ import java.util.List; @Override public List getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues[index] == null) { + if (index == -1 || cues[index] == Cue.EMPTY) { // timeUs is earlier than the start of the first cue, or we have an empty cue. return Collections.emptyList(); } else { diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/package-info.java new file mode 100644 index 000000000000..fb990cb74853 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.text.subrip; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java index 6da654730e36..502281c2decf 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java @@ -16,19 +16,19 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.text.ttml; import android.text.Layout; -import android.util.Log; -import android.util.Pair; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleDecoderException; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ColorParser; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.XmlPullParserUtil; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.ArrayDeque; import java.util.HashMap; -import java.util.LinkedList; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -39,6 +39,7 @@ import org.xmlpull.v1.XmlPullParserFactory; /** * A {@link SimpleSubtitleDecoder} for TTML supporting the DFXP presentation profile. Features * supported by this decoder are: + * *

    *
  • content *
  • core @@ -52,7 +53,9 @@ import org.xmlpull.v1.XmlPullParserFactory; *
  • time-clock *
  • time-offset-with-frames *
  • time-offset-with-ticks + *
  • cell-resolution *
+ * * @see TTML specification */ public final class TtmlDecoder extends SimpleSubtitleDecoder { @@ -66,6 +69,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { private static final String ATTR_END = "end"; private static final String ATTR_STYLE = "style"; private static final String ATTR_REGION = "region"; + private static final String ATTR_IMAGE = "backgroundImage"; private static final Pattern CLOCK_TIME = Pattern.compile("^([0-9][0-9]+):([0-9][0-9]):([0-9][0-9])" @@ -75,11 +79,16 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$"); private static final Pattern PERCENTAGE_COORDINATES = Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$"); + private static final Pattern PIXEL_COORDINATES = + Pattern.compile("^(\\d+\\.?\\d*?)px (\\d+\\.?\\d*?)px$"); + private static final Pattern CELL_RESOLUTION = Pattern.compile("^(\\d+) (\\d+)$"); private static final int DEFAULT_FRAME_RATE = 30; private static final FrameAndTickRate DEFAULT_FRAME_AND_TICK_RATE = new FrameAndTickRate(DEFAULT_FRAME_RATE, 1, 1); + private static final CellResolution DEFAULT_CELL_RESOLUTION = + new CellResolution(/* columns= */ 32, /* rows= */ 15); private final XmlPullParserFactory xmlParserFactory; @@ -94,37 +103,42 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } @Override - protected TtmlSubtitle decode(byte[] bytes, int length, boolean reset) + protected Subtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { try { XmlPullParser xmlParser = xmlParserFactory.newPullParser(); Map globalStyles = new HashMap<>(); Map regionMap = new HashMap<>(); - regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion()); + Map imageMap = new HashMap<>(); + regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion(null)); ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length); xmlParser.setInput(inputStream, null); TtmlSubtitle ttmlSubtitle = null; - LinkedList nodeStack = new LinkedList<>(); + ArrayDeque nodeStack = new ArrayDeque<>(); int unsupportedNodeDepth = 0; int eventType = xmlParser.getEventType(); FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE; + CellResolution cellResolution = DEFAULT_CELL_RESOLUTION; + TtsExtent ttsExtent = null; while (eventType != XmlPullParser.END_DOCUMENT) { - TtmlNode parent = nodeStack.peekLast(); + TtmlNode parent = nodeStack.peek(); if (unsupportedNodeDepth == 0) { String name = xmlParser.getName(); if (eventType == XmlPullParser.START_TAG) { if (TtmlNode.TAG_TT.equals(name)) { frameAndTickRate = parseFrameAndTickRates(xmlParser); + cellResolution = parseCellResolution(xmlParser, DEFAULT_CELL_RESOLUTION); + ttsExtent = parseTtsExtent(xmlParser); } if (!isSupportedTag(name)) { Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName()); unsupportedNodeDepth++; } else if (TtmlNode.TAG_HEAD.equals(name)) { - parseHeader(xmlParser, globalStyles, regionMap); + parseHeader(xmlParser, globalStyles, cellResolution, ttsExtent, regionMap, imageMap); } else { try { TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate); - nodeStack.addLast(node); + nodeStack.push(node); if (parent != null) { parent.addChild(node); } @@ -138,9 +152,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { parent.addChild(TtmlNode.buildTextNode(xmlParser.getText())); } else if (eventType == XmlPullParser.END_TAG) { if (xmlParser.getName().equals(TtmlNode.TAG_TT)) { - ttmlSubtitle = new TtmlSubtitle(nodeStack.getLast(), globalStyles, regionMap); + ttmlSubtitle = new TtmlSubtitle(nodeStack.peek(), globalStyles, regionMap, imageMap); } - nodeStack.removeLast(); + nodeStack.pop(); } } else { if (eventType == XmlPullParser.START_TAG) { @@ -171,7 +185,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { float frameRateMultiplier = 1; String frameRateMultiplierString = xmlParser.getAttributeValue(TTP, "frameRateMultiplier"); if (frameRateMultiplierString != null) { - String[] parts = frameRateMultiplierString.split(" "); + String[] parts = Util.split(frameRateMultiplierString, " "); if (parts.length != 2) { throw new SubtitleDecoderException("frameRateMultiplier doesn't have 2 parts"); } @@ -194,8 +208,59 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate); } - private Map parseHeader(XmlPullParser xmlParser, - Map globalStyles, Map globalRegions) + private CellResolution parseCellResolution(XmlPullParser xmlParser, CellResolution defaultValue) + throws SubtitleDecoderException { + String cellResolution = xmlParser.getAttributeValue(TTP, "cellResolution"); + if (cellResolution == null) { + return defaultValue; + } + + Matcher cellResolutionMatcher = CELL_RESOLUTION.matcher(cellResolution); + if (!cellResolutionMatcher.matches()) { + Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution); + return defaultValue; + } + try { + int columns = Integer.parseInt(cellResolutionMatcher.group(1)); + int rows = Integer.parseInt(cellResolutionMatcher.group(2)); + if (columns == 0 || rows == 0) { + throw new SubtitleDecoderException("Invalid cell resolution " + columns + " " + rows); + } + return new CellResolution(columns, rows); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution); + return defaultValue; + } + } + + private TtsExtent parseTtsExtent(XmlPullParser xmlParser) { + String ttsExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT); + if (ttsExtent == null) { + return null; + } + + Matcher extentMatcher = PIXEL_COORDINATES.matcher(ttsExtent); + if (!extentMatcher.matches()) { + Log.w(TAG, "Ignoring non-pixel tts extent: " + ttsExtent); + return null; + } + try { + int width = Integer.parseInt(extentMatcher.group(1)); + int height = Integer.parseInt(extentMatcher.group(2)); + return new TtsExtent(width, height); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed tts extent: " + ttsExtent); + return null; + } + } + + private Map parseHeader( + XmlPullParser xmlParser, + Map globalStyles, + CellResolution cellResolution, + TtsExtent ttsExtent, + Map globalRegions, + Map imageMap) throws IOException, XmlPullParserException { do { xmlParser.next(); @@ -211,55 +276,168 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { globalStyles.put(style.getId(), style); } } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) { - Pair ttmlRegionInfo = parseRegionAttributes(xmlParser); - if (ttmlRegionInfo != null) { - globalRegions.put(ttmlRegionInfo.first, ttmlRegionInfo.second); + TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution, ttsExtent); + if (ttmlRegion != null) { + globalRegions.put(ttmlRegion.id, ttmlRegion); } + } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_METADATA)) { + parseMetadata(xmlParser, imageMap); } } while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD)); return globalStyles; } - /** - * Parses a region declaration. Supports origin and extent definition but only when defined in - * terms of percentage of the viewport. Regions that do not correctly declare origin are ignored. - */ - private Pair parseRegionAttributes(XmlPullParser xmlParser) { - String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID); - String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN); - String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT); - if (regionOrigin == null || regionId == null) { - return null; - } - float position = Cue.DIMEN_UNSET; - float line = Cue.DIMEN_UNSET; - Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin); - if (originMatcher.matches()) { - try { - position = Float.parseFloat(originMatcher.group(1)) / 100.f; - line = Float.parseFloat(originMatcher.group(2)) / 100.f; - } catch (NumberFormatException e) { - Log.w(TAG, "Ignoring region with malformed origin: '" + regionOrigin + "'", e); - position = Cue.DIMEN_UNSET; - } - } - float width = Cue.DIMEN_UNSET; - if (regionExtent != null) { - Matcher extentMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent); - if (extentMatcher.matches()) { - try { - width = Float.parseFloat(extentMatcher.group(1)) / 100.f; - } catch (NumberFormatException e) { - Log.w(TAG, "Ignoring malformed region extent: '" + regionExtent + "'", e); + private void parseMetadata(XmlPullParser xmlParser, Map imageMap) + throws IOException, XmlPullParserException { + do { + xmlParser.next(); + if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_IMAGE)) { + String id = XmlPullParserUtil.getAttributeValue(xmlParser, "id"); + if (id != null) { + String encodedBitmapData = xmlParser.nextText(); + imageMap.put(id, encodedBitmapData); } } + } while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_METADATA)); + } + + /** + * Parses a region declaration. + * + *

Supports both percentage and pixel defined regions. In case of pixel defined regions the + * passed {@code ttsExtent} is used as a reference window to convert the pixel values to + * fractions. In case of missing tts:extent the pixel defined regions can't be parsed, and null is + * returned. + */ + private TtmlRegion parseRegionAttributes( + XmlPullParser xmlParser, CellResolution cellResolution, TtsExtent ttsExtent) { + String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID); + if (regionId == null) { + return null; } - return position != Cue.DIMEN_UNSET ? new Pair<>(regionId, new TtmlRegion(position, line, - Cue.LINE_TYPE_FRACTION, width)) : null; + + float position; + float line; + + String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN); + if (regionOrigin != null) { + Matcher originPercentageMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin); + Matcher originPixelMatcher = PIXEL_COORDINATES.matcher(regionOrigin); + if (originPercentageMatcher.matches()) { + try { + position = Float.parseFloat(originPercentageMatcher.group(1)) / 100f; + line = Float.parseFloat(originPercentageMatcher.group(2)) / 100f; + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin); + return null; + } + } else if (originPixelMatcher.matches()) { + if (ttsExtent == null) { + Log.w(TAG, "Ignoring region with missing tts:extent: " + regionOrigin); + return null; + } + try { + int width = Integer.parseInt(originPixelMatcher.group(1)); + int height = Integer.parseInt(originPixelMatcher.group(2)); + // Convert pixel values to fractions. + position = width / (float) ttsExtent.width; + line = height / (float) ttsExtent.height; + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin); + return null; + } + } else { + Log.w(TAG, "Ignoring region with unsupported origin: " + regionOrigin); + return null; + } + } else { + Log.w(TAG, "Ignoring region without an origin"); + return null; + // TODO: Should default to top left as below in this case, but need to fix + // https://github.com/google/ExoPlayer/issues/2953 first. + // Origin is omitted. Default to top left. + // position = 0; + // line = 0; + } + + float width; + float height; + String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT); + if (regionExtent != null) { + Matcher extentPercentageMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent); + Matcher extentPixelMatcher = PIXEL_COORDINATES.matcher(regionExtent); + if (extentPercentageMatcher.matches()) { + try { + width = Float.parseFloat(extentPercentageMatcher.group(1)) / 100f; + height = Float.parseFloat(extentPercentageMatcher.group(2)) / 100f; + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin); + return null; + } + } else if (extentPixelMatcher.matches()) { + if (ttsExtent == null) { + Log.w(TAG, "Ignoring region with missing tts:extent: " + regionOrigin); + return null; + } + try { + int extentWidth = Integer.parseInt(extentPixelMatcher.group(1)); + int extentHeight = Integer.parseInt(extentPixelMatcher.group(2)); + // Convert pixel values to fractions. + width = extentWidth / (float) ttsExtent.width; + height = extentHeight / (float) ttsExtent.height; + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin); + return null; + } + } else { + Log.w(TAG, "Ignoring region with unsupported extent: " + regionOrigin); + return null; + } + } else { + Log.w(TAG, "Ignoring region without an extent"); + return null; + // TODO: Should default to extent of parent as below in this case, but need to fix + // https://github.com/google/ExoPlayer/issues/2953 first. + // Extent is omitted. Default to extent of parent. + // width = 1; + // height = 1; + } + + @Cue.AnchorType int lineAnchor = Cue.ANCHOR_TYPE_START; + String displayAlign = XmlPullParserUtil.getAttributeValue(xmlParser, + TtmlNode.ATTR_TTS_DISPLAY_ALIGN); + if (displayAlign != null) { + switch (Util.toLowerInvariant(displayAlign)) { + case "center": + lineAnchor = Cue.ANCHOR_TYPE_MIDDLE; + line += height / 2; + break; + case "after": + lineAnchor = Cue.ANCHOR_TYPE_END; + line += height; + break; + default: + // Default "before" case. Do nothing. + break; + } + } + + float regionTextHeight = 1.0f / cellResolution.rows; + return new TtmlRegion( + regionId, + position, + line, + /* lineType= */ Cue.LINE_TYPE_FRACTION, + lineAnchor, + width, + height, + /* textSizeType= */ Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING, + /* textSize= */ regionTextHeight); } private String[] parseStyleIds(String parentStyleIds) { - return parentStyleIds.split("\\s+"); + parentStyleIds = parentStyleIds.trim(); + return parentStyleIds.isEmpty() ? new String[0] : Util.split(parentStyleIds, "\\s+"); } private TtmlStyle parseStyleAttributes(XmlPullParser parser, TtmlStyle style) { @@ -277,7 +455,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { try { style.setBackgroundColor(ColorParser.parseTtmlColor(attributeValue)); } catch (IllegalArgumentException e) { - Log.w(TAG, "failed parsing background value: '" + attributeValue + "'"); + Log.w(TAG, "Failed parsing background value: " + attributeValue); } break; case TtmlNode.ATTR_TTS_COLOR: @@ -285,7 +463,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { try { style.setFontColor(ColorParser.parseTtmlColor(attributeValue)); } catch (IllegalArgumentException e) { - Log.w(TAG, "failed parsing color value: '" + attributeValue + "'"); + Log.w(TAG, "Failed parsing color value: " + attributeValue); } break; case TtmlNode.ATTR_TTS_FONT_FAMILY: @@ -296,7 +474,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { style = createIfNull(style); parseFontSize(attributeValue, style); } catch (SubtitleDecoderException e) { - Log.w(TAG, "failed parsing fontSize value: '" + attributeValue + "'"); + Log.w(TAG, "Failed parsing fontSize value: " + attributeValue); } break; case TtmlNode.ATTR_TTS_FONT_WEIGHT: @@ -361,6 +539,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { long startTime = C.TIME_UNSET; long endTime = C.TIME_UNSET; String regionId = TtmlNode.ANONYMOUS_REGION_ID; + String imageId = null; String[] styleIds = null; int attributeCount = parser.getAttributeCount(); TtmlStyle style = parseStyleAttributes(parser, null); @@ -391,6 +570,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { regionId = value; } break; + case ATTR_IMAGE: + // Parse URI reference only if refers to an element in the same document (it must start + // with '#'). Resolving URIs from external sources is not supported. + if (value.startsWith("#")) { + imageId = value.substring(1); + } + break; default: // Do nothing. break; @@ -413,7 +599,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { endTime = parent.endTimeUs; } } - return TtmlNode.buildNode(parser.getName(), startTime, endTime, style, styleIds, regionId); + return TtmlNode.buildNode( + parser.getName(), startTime, endTime, style, styleIds, regionId, imageId); } private static boolean isSupportedTag(String tag) { @@ -429,14 +616,14 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { || tag.equals(TtmlNode.TAG_LAYOUT) || tag.equals(TtmlNode.TAG_REGION) || tag.equals(TtmlNode.TAG_METADATA) - || tag.equals(TtmlNode.TAG_SMPTE_IMAGE) - || tag.equals(TtmlNode.TAG_SMPTE_DATA) - || tag.equals(TtmlNode.TAG_SMPTE_INFORMATION); + || tag.equals(TtmlNode.TAG_IMAGE) + || tag.equals(TtmlNode.TAG_DATA) + || tag.equals(TtmlNode.TAG_INFORMATION); } private static void parseFontSize(String expression, TtmlStyle out) throws SubtitleDecoderException { - String[] expressions = expression.split("\\s+"); + String[] expressions = Util.split(expression, "\\s+"); Matcher matcher; if (expressions.length == 1) { matcher = FONT_SIZE.matcher(expression); @@ -544,4 +731,26 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { this.tickRate = tickRate; } } + + /** Represents the cell resolution for a TTML file. */ + private static final class CellResolution { + final int columns; + final int rows; + + CellResolution(int columns, int rows) { + this.columns = columns; + this.rows = rows; + } + } + + /** Represents the tts:extent for a TTML file. */ + private static final class TtsExtent { + final int width; + final int height; + + TtsExtent(int width, int height) { + this.width = width; + this.height = height; + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlNode.java index c9284a1921b6..16d0f28f6b98 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlNode.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlNode.java @@ -15,7 +15,12 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.text.ttml; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.text.SpannableStringBuilder; +import android.util.Base64; +import android.util.Pair; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; @@ -44,20 +49,21 @@ import java.util.TreeSet; public static final String TAG_LAYOUT = "layout"; public static final String TAG_REGION = "region"; public static final String TAG_METADATA = "metadata"; - public static final String TAG_SMPTE_IMAGE = "smpte:image"; - public static final String TAG_SMPTE_DATA = "smpte:data"; - public static final String TAG_SMPTE_INFORMATION = "smpte:information"; + public static final String TAG_IMAGE = "image"; + public static final String TAG_DATA = "data"; + public static final String TAG_INFORMATION = "information"; public static final String ANONYMOUS_REGION_ID = ""; public static final String ATTR_ID = "id"; - public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor"; + public static final String ATTR_TTS_ORIGIN = "origin"; public static final String ATTR_TTS_EXTENT = "extent"; + public static final String ATTR_TTS_DISPLAY_ALIGN = "displayAlign"; + public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor"; public static final String ATTR_TTS_FONT_STYLE = "fontStyle"; public static final String ATTR_TTS_FONT_SIZE = "fontSize"; public static final String ATTR_TTS_FONT_FAMILY = "fontFamily"; public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight"; public static final String ATTR_TTS_COLOR = "color"; - public static final String ATTR_TTS_ORIGIN = "origin"; public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration"; public static final String ATTR_TTS_TEXT_ALIGN = "textAlign"; @@ -74,34 +80,57 @@ import java.util.TreeSet; public static final String START = "start"; public static final String END = "end"; - public final String tag; - public final String text; + @Nullable public final String tag; + @Nullable public final String text; public final boolean isTextNode; public final long startTimeUs; public final long endTimeUs; - public final TtmlStyle style; + @Nullable public final TtmlStyle style; + @Nullable private final String[] styleIds; public final String regionId; + @Nullable public final String imageId; - private final String[] styleIds; private final HashMap nodeStartsByRegion; private final HashMap nodeEndsByRegion; private List children; public static TtmlNode buildTextNode(String text) { - return new TtmlNode(null, TtmlRenderUtil.applyTextElementSpacePolicy(text), C.TIME_UNSET, - C.TIME_UNSET, null, null, ANONYMOUS_REGION_ID); + return new TtmlNode( + /* tag= */ null, + TtmlRenderUtil.applyTextElementSpacePolicy(text), + /* startTimeUs= */ C.TIME_UNSET, + /* endTimeUs= */ C.TIME_UNSET, + /* style= */ null, + /* styleIds= */ null, + ANONYMOUS_REGION_ID, + /* imageId= */ null); } - public static TtmlNode buildNode(String tag, long startTimeUs, long endTimeUs, - TtmlStyle style, String[] styleIds, String regionId) { - return new TtmlNode(tag, null, startTimeUs, endTimeUs, style, styleIds, regionId); + public static TtmlNode buildNode( + @Nullable String tag, + long startTimeUs, + long endTimeUs, + @Nullable TtmlStyle style, + @Nullable String[] styleIds, + String regionId, + @Nullable String imageId) { + return new TtmlNode( + tag, /* text= */ null, startTimeUs, endTimeUs, style, styleIds, regionId, imageId); } - private TtmlNode(String tag, String text, long startTimeUs, long endTimeUs, - TtmlStyle style, String[] styleIds, String regionId) { + private TtmlNode( + @Nullable String tag, + @Nullable String text, + long startTimeUs, + long endTimeUs, + @Nullable TtmlStyle style, + @Nullable String[] styleIds, + String regionId, + @Nullable String imageId) { this.tag = tag; this.text = text; + this.imageId = imageId; this.style = style; this.styleIds = styleIds; this.isTextNode = text != null; @@ -150,7 +179,8 @@ import java.util.TreeSet; private void getEventTimes(TreeSet out, boolean descendsPNode) { boolean isPNode = TAG_P.equals(tag); - if (descendsPNode || isPNode) { + boolean isDivNode = TAG_DIV.equals(tag); + if (descendsPNode || isPNode || (isDivNode && imageId != null)) { if (startTimeUs != C.TIME_UNSET) { out.add(startTimeUs); } @@ -170,39 +200,101 @@ import java.util.TreeSet; return styleIds; } - public List getCues(long timeUs, Map globalStyles, - Map regionMap) { - TreeMap regionOutputs = new TreeMap<>(); - traverseForText(timeUs, false, regionId, regionOutputs); - traverseForStyle(globalStyles, regionOutputs); + public List getCues( + long timeUs, + Map globalStyles, + Map regionMap, + Map imageMap) { + + List> regionImageOutputs = new ArrayList<>(); + traverseForImage(timeUs, regionId, regionImageOutputs); + + TreeMap regionTextOutputs = new TreeMap<>(); + traverseForText(timeUs, false, regionId, regionTextOutputs); + traverseForStyle(timeUs, globalStyles, regionTextOutputs); + List cues = new ArrayList<>(); - for (Entry entry : regionOutputs.entrySet()) { - TtmlRegion region = regionMap.get(entry.getKey()); - cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType, - Cue.TYPE_UNSET, region.position, Cue.TYPE_UNSET, region.width)); + + // Create image based cues. + for (Pair regionImagePair : regionImageOutputs) { + String encodedBitmapData = imageMap.get(regionImagePair.second); + if (encodedBitmapData == null) { + // Image reference points to an invalid image. Do nothing. + continue; + } + + byte[] bitmapData = Base64.decode(encodedBitmapData, Base64.DEFAULT); + Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, /* offset= */ 0, bitmapData.length); + TtmlRegion region = regionMap.get(regionImagePair.first); + + cues.add( + new Cue( + bitmap, + region.position, + Cue.ANCHOR_TYPE_START, + region.line, + region.lineAnchor, + region.width, + region.height)); } + + // Create text based cues. + for (Entry entry : regionTextOutputs.entrySet()) { + TtmlRegion region = regionMap.get(entry.getKey()); + cues.add( + new Cue( + cleanUpText(entry.getValue()), + /* textAlignment= */ null, + region.line, + region.lineType, + region.lineAnchor, + region.position, + /* positionAnchor= */ Cue.TYPE_UNSET, + region.width, + region.textSizeType, + region.textSize)); + } + return cues; } - private void traverseForText(long timeUs, boolean descendsPNode, - String inheritedRegion, Map regionOutputs) { + private void traverseForImage( + long timeUs, String inheritedRegion, List> regionImageList) { + String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId; + if (isActive(timeUs) && TAG_DIV.equals(tag) && imageId != null) { + regionImageList.add(new Pair<>(resolvedRegionId, imageId)); + return; + } + for (int i = 0; i < getChildCount(); ++i) { + getChild(i).traverseForImage(timeUs, resolvedRegionId, regionImageList); + } + } + + private void traverseForText( + long timeUs, + boolean descendsPNode, + String inheritedRegion, + Map regionOutputs) { nodeStartsByRegion.clear(); nodeEndsByRegion.clear(); - String resolvedRegionId = regionId; - if (ANONYMOUS_REGION_ID.equals(resolvedRegionId)) { - resolvedRegionId = inheritedRegion; + if (TAG_METADATA.equals(tag)) { + // Ignore metadata tag. + return; } + + String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId; + if (isTextNode && descendsPNode) { getRegionOutput(resolvedRegionId, regionOutputs).append(text); } else if (TAG_BR.equals(tag) && descendsPNode) { getRegionOutput(resolvedRegionId, regionOutputs).append('\n'); - } else if (TAG_METADATA.equals(tag)) { - // Do nothing. } else if (isActive(timeUs)) { - boolean isPNode = TAG_P.equals(tag); + // This is a container node, which can contain zero or more children. for (Entry entry : regionOutputs.entrySet()) { nodeStartsByRegion.put(entry.getKey(), entry.getValue().length()); } + + boolean isPNode = TAG_P.equals(tag); for (int i = 0; i < getChildCount(); i++) { getChild(i).traverseForText(timeUs, descendsPNode || isPNode, resolvedRegionId, regionOutputs); @@ -210,39 +302,50 @@ import java.util.TreeSet; if (isPNode) { TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs)); } + for (Entry entry : regionOutputs.entrySet()) { nodeEndsByRegion.put(entry.getKey(), entry.getValue().length()); } } } - private static SpannableStringBuilder getRegionOutput(String resolvedRegionId, - Map regionOutputs) { + private static SpannableStringBuilder getRegionOutput( + String resolvedRegionId, Map regionOutputs) { if (!regionOutputs.containsKey(resolvedRegionId)) { regionOutputs.put(resolvedRegionId, new SpannableStringBuilder()); } return regionOutputs.get(resolvedRegionId); } - private void traverseForStyle(Map globalStyles, + private void traverseForStyle( + long timeUs, + Map globalStyles, Map regionOutputs) { + if (!isActive(timeUs)) { + return; + } for (Entry entry : nodeEndsByRegion.entrySet()) { String regionId = entry.getKey(); int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0; - applyStyleToOutput(globalStyles, regionOutputs.get(regionId), start, entry.getValue()); - for (int i = 0; i < getChildCount(); ++i) { - getChild(i).traverseForStyle(globalStyles, regionOutputs); + int end = entry.getValue(); + if (start != end) { + SpannableStringBuilder regionOutput = regionOutputs.get(regionId); + applyStyleToOutput(globalStyles, regionOutput, start, end); } } + for (int i = 0; i < getChildCount(); ++i) { + getChild(i).traverseForStyle(timeUs, globalStyles, regionOutputs); + } } - private void applyStyleToOutput(Map globalStyles, - SpannableStringBuilder regionOutput, int start, int end) { - if (start != end) { - TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); - if (resolvedStyle != null) { - TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle); - } + private void applyStyleToOutput( + Map globalStyles, + SpannableStringBuilder regionOutput, + int start, + int end) { + TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); + if (resolvedStyle != null) { + TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRegion.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRegion.java index b083245eccbe..d14e547d4963 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRegion.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRegion.java @@ -22,21 +22,48 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; */ /* package */ final class TtmlRegion { + public final String id; public final float position; public final float line; - @Cue.LineType - public final int lineType; + public final @Cue.LineType int lineType; + public final @Cue.AnchorType int lineAnchor; public final float width; + public final float height; + public final @Cue.TextSizeType int textSizeType; + public final float textSize; - public TtmlRegion() { - this(Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); + public TtmlRegion(String id) { + this( + id, + /* position= */ Cue.DIMEN_UNSET, + /* line= */ Cue.DIMEN_UNSET, + /* lineType= */ Cue.TYPE_UNSET, + /* lineAnchor= */ Cue.TYPE_UNSET, + /* width= */ Cue.DIMEN_UNSET, + /* height= */ Cue.DIMEN_UNSET, + /* textSizeType= */ Cue.TYPE_UNSET, + /* textSize= */ Cue.DIMEN_UNSET); } - public TtmlRegion(float position, float line, @Cue.LineType int lineType, float width) { + public TtmlRegion( + String id, + float position, + float line, + @Cue.LineType int lineType, + @Cue.AnchorType int lineAnchor, + float width, + float height, + int textSizeType, + float textSize) { + this.id = id; this.position = position; this.line = line; this.lineType = lineType; + this.lineAnchor = lineAnchor; this.width = width; + this.height = height; + this.textSizeType = textSizeType; + this.textSize = textSize; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlStyle.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlStyle.java index 9a9c73e6f0bb..57faaecb696e 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlStyle.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlStyle.java @@ -16,9 +16,10 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.text.ttml; import android.graphics.Typeface; -import android.support.annotation.IntDef; import android.text.Layout; +import androidx.annotation.IntDef; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -29,25 +30,32 @@ import java.lang.annotation.RetentionPolicy; public static final int UNSPECIFIED = -1; + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC, - STYLE_BOLD_ITALIC}) + @IntDef( + flag = true, + value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC, STYLE_BOLD_ITALIC}) public @interface StyleFlags {} + public static final int STYLE_NORMAL = Typeface.NORMAL; public static final int STYLE_BOLD = Typeface.BOLD; public static final int STYLE_ITALIC = Typeface.ITALIC; public static final int STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC; + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({UNSPECIFIED, FONT_SIZE_UNIT_PIXEL, FONT_SIZE_UNIT_EM, FONT_SIZE_UNIT_PERCENT}) public @interface FontSizeUnit {} + public static final int FONT_SIZE_UNIT_PIXEL = 1; public static final int FONT_SIZE_UNIT_EM = 2; public static final int FONT_SIZE_UNIT_PERCENT = 3; + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({UNSPECIFIED, OFF, ON}) private @interface OptionalBoolean {} + private static final int OFF = 0; private static final int ON = 1; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java index de3aa6d2e8f8..52bd3898182a 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java @@ -15,6 +15,7 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.text.ttml; +import androidx.annotation.VisibleForTesting; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle; @@ -32,13 +33,18 @@ import java.util.Map; private final long[] eventTimesUs; private final Map globalStyles; private final Map regionMap; + private final Map imageMap; - public TtmlSubtitle(TtmlNode root, Map globalStyles, - Map regionMap) { + public TtmlSubtitle( + TtmlNode root, + Map globalStyles, + Map regionMap, + Map imageMap) { this.root = root; this.regionMap = regionMap; - this.globalStyles = globalStyles != null - ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap(); + this.imageMap = imageMap; + this.globalStyles = + globalStyles != null ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap(); this.eventTimesUs = root.getEventTimesUs(); } @@ -58,17 +64,17 @@ import java.util.Map; return eventTimesUs[index]; } - /* @VisibleForTesting */ + @VisibleForTesting /* package */ TtmlNode getRoot() { return root; } @Override public List getCues(long timeUs) { - return root.getCues(timeUs, globalStyles, regionMap); + return root.getCues(timeUs, globalStyles, regionMap, imageMap); } - /* @VisibleForTesting */ + @VisibleForTesting /* package */ Map getGlobalStyles() { return globalStyles; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/package-info.java new file mode 100644 index 000000000000..e6e7a5a8e359 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.text.ttml; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java index e23d492e12cf..a6b9ab5c63f9 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java @@ -43,8 +43,8 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { private static final char BOM_UTF16_BE = '\uFEFF'; private static final char BOM_UTF16_LE = '\uFFFE'; - private static final int TYPE_STYL = Util.getIntegerCodeForString("styl"); - private static final int TYPE_TBOX = Util.getIntegerCodeForString("tbox"); + private static final int TYPE_STYL = 0x7374796c; + private static final int TYPE_TBOX = 0x74626f78; private static final String TX3G_SERIF = "Serif"; private static final int SIZE_ATOM_HEADER = 8; @@ -56,8 +56,8 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { private static final int FONT_FACE_ITALIC = 0x0002; private static final int FONT_FACE_UNDERLINE = 0x0004; - private static final int SPAN_PRIORITY_LOW = (0xFF << Spanned.SPAN_PRIORITY_SHIFT); - private static final int SPAN_PRIORITY_HIGH = (0x00 << Spanned.SPAN_PRIORITY_SHIFT); + private static final int SPAN_PRIORITY_LOW = 0xFF << Spanned.SPAN_PRIORITY_SHIFT; + private static final int SPAN_PRIORITY_HIGH = 0; private static final int DEFAULT_FONT_FACE = 0; private static final int DEFAULT_COLOR = Color.WHITE; @@ -65,6 +65,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { private static final float DEFAULT_VERTICAL_PLACEMENT = 0.85f; private final ParsableByteArray parsableByteArray; + private boolean customVerticalPlacement; private int defaultFontFace; private int defaultColorRgba; @@ -80,10 +81,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { public Tx3gDecoder(List initializationData) { super("Tx3gDecoder"); parsableByteArray = new ParsableByteArray(); - decodeInitializationData(initializationData); - } - private void decodeInitializationData(List initializationData) { if (initializationData != null && initializationData.size() == 1 && (initializationData.get(0).length == 48 || initializationData.get(0).length == 53)) { byte[] initializationBytes = initializationData.get(0); @@ -92,7 +90,8 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { | ((initializationBytes[27] & 0xFF) << 16) | ((initializationBytes[28] & 0xFF) << 8) | (initializationBytes[29] & 0xFF); - String fontFamily = new String(initializationBytes, 43, initializationBytes.length - 43); + String fontFamily = + Util.fromUtf8Bytes(initializationBytes, 43, initializationBytes.length - 43); defaultFontFamily = TX3G_SERIF.equals(fontFamily) ? C.SERIF_NAME : C.SANS_SERIF_NAME; //font size (initializationBytes[25]) is 5% of video height calculatedVideoTrackHeight = 20 * initializationBytes[25]; @@ -150,8 +149,16 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { } parsableByteArray.setPosition(position + atomSize); } - return new Tx3gSubtitle(new Cue(cueText, null, verticalPlacement, Cue.LINE_TYPE_FRACTION, - Cue.ANCHOR_TYPE_START, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET)); + return new Tx3gSubtitle( + new Cue( + cueText, + /* textAlignment= */ null, + verticalPlacement, + Cue.LINE_TYPE_FRACTION, + Cue.ANCHOR_TYPE_START, + Cue.DIMEN_UNSET, + Cue.TYPE_UNSET, + Cue.DIMEN_UNSET)); } private static String readSubtitleText(ParsableByteArray parsableByteArray) diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java index 98049e98fbbf..93bc6034d1cf 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java @@ -57,7 +57,7 @@ import java.util.List; @Override public List getCues(long timeUs) { - return timeUs >= 0 ? cues : Collections.emptyList(); + return timeUs >= 0 ? cues : Collections.emptyList(); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/package-info.java new file mode 100644 index 000000000000..7bac8c12b698 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.text.tx3g; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/CssParser.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/CssParser.java index 54ca2efa0197..3337cc3481ea 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/CssParser.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/CssParser.java @@ -16,9 +16,12 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt; import android.text.TextUtils; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ColorParser; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; -import java.util.Arrays; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -34,8 +37,8 @@ import java.util.regex.Pattern; private static final String PROPERTY_TEXT_DECORATION = "text-decoration"; private static final String VALUE_BOLD = "bold"; private static final String VALUE_UNDERLINE = "underline"; - private static final String BLOCK_START = "{"; - private static final String BLOCK_END = "}"; + private static final String RULE_START = "{"; + private static final String RULE_END = "}"; private static final String PROPERTY_FONT_STYLE = "font-style"; private static final String VALUE_ITALIC = "italic"; @@ -51,47 +54,58 @@ import java.util.regex.Pattern; } /** - * Takes a CSS style block and consumes up to the first empty line found. Attempts to parse the - * contents of the style block and returns a {@link WebvttCssStyle} instance if successful, or - * {@code null} otherwise. + * Takes a CSS style block and consumes up to the first empty line. Attempts to parse the contents + * of the style block and returns a list of {@link WebvttCssStyle} instances if successful. If + * parsing fails, it returns a list including only the styles which have been successfully parsed + * up to the style rule which was malformed. * * @param input The input from which the style block should be read. - * @return A {@link WebvttCssStyle} that represents the parsed block. + * @return A list of {@link WebvttCssStyle}s that represents the parsed block, or a list + * containing the styles up to the parsing failure. */ - public WebvttCssStyle parseBlock(ParsableByteArray input) { + public List parseBlock(ParsableByteArray input) { stringBuilder.setLength(0); int initialInputPosition = input.getPosition(); skipStyleBlock(input); styleInput.reset(input.data, input.getPosition()); styleInput.setPosition(initialInputPosition); - String selector = parseSelector(styleInput, stringBuilder); - if (selector == null || !BLOCK_START.equals(parseNextToken(styleInput, stringBuilder))) { - return null; - } - WebvttCssStyle style = new WebvttCssStyle(); - applySelectorToStyle(style, selector); - String token = null; - boolean blockEndFound = false; - while (!blockEndFound) { - int position = styleInput.getPosition(); - token = parseNextToken(styleInput, stringBuilder); - blockEndFound = token == null || BLOCK_END.equals(token); - if (!blockEndFound) { - styleInput.setPosition(position); - parseStyleDeclaration(styleInput, style, stringBuilder); + + List styles = new ArrayList<>(); + String selector; + while ((selector = parseSelector(styleInput, stringBuilder)) != null) { + if (!RULE_START.equals(parseNextToken(styleInput, stringBuilder))) { + return styles; + } + WebvttCssStyle style = new WebvttCssStyle(); + applySelectorToStyle(style, selector); + String token = null; + boolean blockEndFound = false; + while (!blockEndFound) { + int position = styleInput.getPosition(); + token = parseNextToken(styleInput, stringBuilder); + blockEndFound = token == null || RULE_END.equals(token); + if (!blockEndFound) { + styleInput.setPosition(position); + parseStyleDeclaration(styleInput, style, stringBuilder); + } + } + // Check that the style rule ended correctly. + if (RULE_END.equals(token)) { + styles.add(style); } } - return BLOCK_END.equals(token) ? style : null; // Check that the style block ended correctly. + return styles; } /** - * Returns a string containing the selector. The input is expected to have the form - * {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional. + * Returns a string containing the selector. The input is expected to have the form {@code + * ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional. * * @param input From which the selector is obtained. - * @return A string containing the target, empty string if the selector is universal - * (targets all cues) or null if an error was encountered. + * @return A string containing the target, empty string if the selector is universal (targets all + * cues) or null if an error was encountered. */ + @Nullable private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) { skipWhitespaceAndComments(input); if (input.bytesLeft() < 5) { @@ -106,7 +120,7 @@ import java.util.regex.Pattern; if (token == null) { return null; } - if (BLOCK_START.equals(token)) { + if (RULE_START.equals(token)) { input.setPosition(position); return ""; } @@ -115,7 +129,7 @@ import java.util.regex.Pattern; target = readCueTarget(input); } token = parseNextToken(input, stringBuilder); - if (!")".equals(token) || token == null) { + if (!")".equals(token)) { return null; } return target; @@ -155,7 +169,7 @@ import java.util.regex.Pattern; String token = parseNextToken(input, stringBuilder); if (";".equals(token)) { // The style declaration is well formed. - } else if (BLOCK_END.equals(token)) { + } else if (RULE_END.equals(token)) { // The style declaration is well formed and we can go on, but the closing bracket had to be // fed back. input.setPosition(position); @@ -195,6 +209,7 @@ import java.util.regex.Pattern; } // Visible for testing. + @Nullable /* package */ static String parseNextToken(ParsableByteArray input, StringBuilder stringBuilder) { skipWhitespaceAndComments(input); if (input.bytesLeft() == 0) { @@ -236,6 +251,7 @@ import java.util.regex.Pattern; return (char) input.data[position]; } + @Nullable private static String parsePropertyValue(ParsableByteArray input, StringBuilder stringBuilder) { StringBuilder expressionBuilder = new StringBuilder(); String token; @@ -249,7 +265,7 @@ import java.util.regex.Pattern; // Syntax error. return null; } - if (BLOCK_END.equals(token) || ";".equals(token)) { + if (RULE_END.equals(token) || ";".equals(token)) { input.setPosition(position); expressionEndFound = true; } else { @@ -314,7 +330,7 @@ import java.util.regex.Pattern; } selector = selector.substring(0, voiceStartIndex); } - String[] classDivision = selector.split("\\."); + String[] classDivision = Util.split(selector, "\\."); String tagAndIdDivision = classDivision[0]; int idPrefixIndex = tagAndIdDivision.indexOf('#'); if (idPrefixIndex != -1) { @@ -324,7 +340,7 @@ import java.util.regex.Pattern; style.setTargetTagName(tagAndIdDivision); } if (classDivision.length > 1) { - style.setTargetClasses(Arrays.copyOfRange(classDivision, 1, classDivision.length)); + style.setTargetClasses(Util.nullSafeArrayCopyOfRange(classDivision, 1, classDivision.length)); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java index 2e973f83f415..3df35c789b12 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java @@ -17,6 +17,7 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleDecoderException; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; @@ -24,16 +25,20 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -/** - * A {@link SimpleSubtitleDecoder} for Webvtt embedded in a Mp4 container file. - */ +/** A {@link SimpleSubtitleDecoder} for Webvtt embedded in a Mp4 container file. */ +@SuppressWarnings("ConstantField") public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { private static final int BOX_HEADER_SIZE = 8; - private static final int TYPE_payl = Util.getIntegerCodeForString("payl"); - private static final int TYPE_sttg = Util.getIntegerCodeForString("sttg"); - private static final int TYPE_vttc = Util.getIntegerCodeForString("vttc"); + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_payl = 0x7061796c; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_sttg = 0x73747467; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_vttc = 0x76747463; private final ParsableByteArray sampleData; private final WebvttCue.Builder builder; @@ -45,7 +50,7 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { } @Override - protected Mp4WebvttSubtitle decode(byte[] bytes, int length, boolean reset) + protected Subtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { // Webvtt in Mp4 samples have boxes inside of them, so we have to do a traditional box parsing: // first 4 bytes size and then 4 bytes type. @@ -78,14 +83,14 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { int boxType = sampleData.readInt(); remainingCueBoxBytes -= BOX_HEADER_SIZE; int payloadLength = boxSize - BOX_HEADER_SIZE; - String boxPayload = new String(sampleData.data, sampleData.getPosition(), payloadLength); + String boxPayload = + Util.fromUtf8Bytes(sampleData.data, sampleData.getPosition(), payloadLength); sampleData.skipBytes(payloadLength); remainingCueBoxBytes -= payloadLength; if (boxType == TYPE_sttg) { WebvttCueParser.parseCueSettingsList(boxPayload, builder); } else if (boxType == TYPE_payl) { - WebvttCueParser.parseCueText(null, boxPayload.trim(), builder, - Collections.emptyList()); + WebvttCueParser.parseCueText(null, boxPayload.trim(), builder, Collections.emptyList()); } else { // Other VTTCueBox children are still not supported and are ignored. } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java index 0478e2af36ec..545e8b2511a1 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java @@ -51,6 +51,6 @@ import java.util.List; @Override public List getCues(long timeUs) { - return timeUs >= 0 ? cues : Collections.emptyList(); + return timeUs >= 0 ? cues : Collections.emptyList(); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java index 913b784b1825..da37cfbdf365 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -16,14 +16,18 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt; import android.graphics.Typeface; -import android.support.annotation.IntDef; import android.text.Layout; +import android.text.TextUtils; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; /** * Style object of a Css style block in a Webvtt file. @@ -31,29 +35,44 @@ import java.util.List; * @see W3C specification - Apply * CSS properties */ -/* package */ final class WebvttCssStyle { +public final class WebvttCssStyle { public static final int UNSPECIFIED = -1; + /** + * Style flag enum. Possible flag values are {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link + * #STYLE_BOLD}, {@link #STYLE_ITALIC} and {@link #STYLE_BOLD_ITALIC}. + */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC, - STYLE_BOLD_ITALIC}) + @IntDef( + flag = true, + value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC, STYLE_BOLD_ITALIC}) public @interface StyleFlags {} + public static final int STYLE_NORMAL = Typeface.NORMAL; public static final int STYLE_BOLD = Typeface.BOLD; public static final int STYLE_ITALIC = Typeface.ITALIC; public static final int STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC; + /** + * Font size unit enum. One of {@link #UNSPECIFIED}, {@link #FONT_SIZE_UNIT_PIXEL}, {@link + * #FONT_SIZE_UNIT_EM} or {@link #FONT_SIZE_UNIT_PERCENT}. + */ + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({UNSPECIFIED, FONT_SIZE_UNIT_PIXEL, FONT_SIZE_UNIT_EM, FONT_SIZE_UNIT_PERCENT}) public @interface FontSizeUnit {} + public static final int FONT_SIZE_UNIT_PIXEL = 1; public static final int FONT_SIZE_UNIT_EM = 2; public static final int FONT_SIZE_UNIT_PERCENT = 3; + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({UNSPECIFIED, OFF, ON}) private @interface OptionalBoolean {} + private static final int OFF = 0; private static final int ON = 1; @@ -64,7 +83,7 @@ import java.util.List; private String targetVoice; // Style properties. - private String fontFamily; + @Nullable private String fontFamily; private int fontColor; private boolean hasFontColor; private int backgroundColor; @@ -75,12 +94,16 @@ import java.util.List; @OptionalBoolean private int italic; @FontSizeUnit private int fontSizeUnit; private float fontSize; - private Layout.Alignment textAlign; + @Nullable private Layout.Alignment textAlign; + // Calling reset() is forbidden because `this` isn't initialized. This can be safely suppressed + // because reset() only assigns fields, it doesn't read any. + @SuppressWarnings("nullness:method.invocation.invalid") public WebvttCssStyle() { reset(); } + @EnsuresNonNull({"targetId", "targetTag", "targetClasses", "targetVoice"}) public void reset() { targetId = ""; targetTag = ""; @@ -117,14 +140,13 @@ import java.util.List; * Returns a value in a score system compliant with the CSS Specificity rules. * * @see CSS Cascading - * - * The score works as follows: - *

    - *
  • Id match adds 0x40000000 to the score. - *
  • Each class and voice match adds 4 to the score. - *
  • Tag matching adds 2 to the score. - *
  • Universal selector matching scores 1. - *
+ *

The score works as follows: + *

    + *
  • Id match adds 0x40000000 to the score. + *
  • Each class and voice match adds 4 to the score. + *
  • Tag matching adds 2 to the score. + *
  • Universal selector matching scores 1. + *
* * @param id The id of the cue if present, {@code null} otherwise. * @param tag Name of the tag, {@code null} if it refers to the entire cue. @@ -132,12 +154,13 @@ import java.util.List; * @param voice Annotated voice if present, {@code null} otherwise. * @return The score of the match, zero if there is no match. */ - public int getSpecificityScore(String id, String tag, String[] classes, String voice) { + public int getSpecificityScore( + @Nullable String id, @Nullable String tag, String[] classes, @Nullable String voice) { if (targetId.isEmpty() && targetTag.isEmpty() && targetClasses.isEmpty() && targetVoice.isEmpty()) { // The selector is universal. It matches with the minimum score if and only if the given // element is a whole cue. - return tag.isEmpty() ? 1 : 0; + return TextUtils.isEmpty(tag) ? 1 : 0; } int score = 0; score = updateScoreForMatch(score, targetId, id, 0x40000000); @@ -192,11 +215,12 @@ import java.util.List; return this; } + @Nullable public String getFontFamily() { return fontFamily; } - public WebvttCssStyle setFontFamily(String fontFamily) { + public WebvttCssStyle setFontFamily(@Nullable String fontFamily) { this.fontFamily = Util.toLowerInvariant(fontFamily); return this; } @@ -235,11 +259,12 @@ import java.util.List; return hasBackgroundColor; } + @Nullable public Layout.Alignment getTextAlign() { return textAlign; } - public WebvttCssStyle setTextAlign(Layout.Alignment textAlign) { + public WebvttCssStyle setTextAlign(@Nullable Layout.Alignment textAlign) { this.textAlign = textAlign; return this; } @@ -293,8 +318,8 @@ import java.util.List; } } - private static int updateScoreForMatch(int currentScore, String target, String actual, - int score) { + private static int updateScoreForMatch( + int currentScore, String target, @Nullable String actual, int score) { if (target.isEmpty() || currentScore == -1) { return currentScore; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCue.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCue.java index 37987a025985..af701d8f547e 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCue.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCue.java @@ -15,31 +15,36 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt; -import android.text.Layout.Alignment; -import android.text.SpannableStringBuilder; -import android.util.Log; -import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; +import static java.lang.annotation.RetentionPolicy.SOURCE; -/** - * A representation of a WebVTT cue. - */ -/* package */ final class WebvttCue extends Cue { +import android.text.Layout.Alignment; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +/** A representation of a WebVTT cue. */ +public final class WebvttCue extends Cue { + + private static final float DEFAULT_POSITION = 0.5f; public final long startTime; public final long endTime; - public WebvttCue(CharSequence text) { - this(0, 0, text); - } - - public WebvttCue(long startTime, long endTime, CharSequence text) { - this(startTime, endTime, text, null, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, - Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); - } - - public WebvttCue(long startTime, long endTime, CharSequence text, Alignment textAlignment, - float line, @Cue.LineType int lineType, @Cue.AnchorType int lineAnchor, float position, - @Cue.AnchorType int positionAnchor, float width) { + private WebvttCue( + long startTime, + long endTime, + CharSequence text, + @Nullable Alignment textAlignment, + float line, + @Cue.LineType int lineType, + @Cue.AnchorType int lineAnchor, + float position, + @Cue.AnchorType int positionAnchor, + float width) { super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width); this.startTime = startTime; this.endTime = endTime; @@ -52,30 +57,81 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; * @return Whether this cue should be placed in the default position. */ public boolean isNormalCue() { - return (line == DIMEN_UNSET && position == DIMEN_UNSET); + return (line == DIMEN_UNSET && position == DEFAULT_POSITION); } - /** - * Builder for WebVTT cues. - */ + /** Builder for WebVTT cues. */ @SuppressWarnings("hiding") - public static final class Builder { + public static class Builder { + + /** + * Valid values for {@link #setTextAlignment(int)}. + * + *

We use a custom list (and not {@link Alignment} directly) in order to include both {@code + * START}/{@code LEFT} and {@code END}/{@code RIGHT}. The distinction is important for {@link + * #derivePosition(int)}. + * + *

These correspond to the valid values for the 'align' cue setting in the WebVTT spec. + */ + @Documented + @Retention(SOURCE) + @IntDef({ + TEXT_ALIGNMENT_START, + TEXT_ALIGNMENT_CENTER, + TEXT_ALIGNMENT_END, + TEXT_ALIGNMENT_LEFT, + TEXT_ALIGNMENT_RIGHT + }) + public @interface TextAlignment {} + /** + * See WebVTT's align:start. + */ + public static final int TEXT_ALIGNMENT_START = 1; + + /** + * See WebVTT's align:center. + */ + public static final int TEXT_ALIGNMENT_CENTER = 2; + + /** + * See WebVTT's align:end. + */ + public static final int TEXT_ALIGNMENT_END = 3; + + /** + * See WebVTT's align:left. + */ + public static final int TEXT_ALIGNMENT_LEFT = 4; + + /** + * See WebVTT's align:right. + */ + public static final int TEXT_ALIGNMENT_RIGHT = 5; private static final String TAG = "WebvttCueBuilder"; private long startTime; private long endTime; - private SpannableStringBuilder text; - private Alignment textAlignment; + @Nullable private CharSequence text; + @TextAlignment private int textAlignment; private float line; - private int lineType; - private int lineAnchor; + // Equivalent to WebVTT's snap-to-lines flag: + // https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag + @LineType private int lineType; + @AnchorType private int lineAnchor; private float position; - private int positionAnchor; + @AnchorType private int positionAnchor; private float width; // Initialization methods + // Calling reset() is forbidden because `this` isn't initialized. This can be safely + // suppressed because reset() only assigns fields, it doesn't read any. + @SuppressWarnings("nullness:method.invocation.invalid") public Builder() { reset(); } @@ -84,23 +140,45 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; startTime = 0; endTime = 0; text = null; - textAlignment = null; + // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment + textAlignment = TEXT_ALIGNMENT_CENTER; line = Cue.DIMEN_UNSET; - lineType = Cue.TYPE_UNSET; - lineAnchor = Cue.TYPE_UNSET; + // Defaults to NUMBER (true): https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag + lineType = Cue.LINE_TYPE_NUMBER; + // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-line-alignment + lineAnchor = Cue.ANCHOR_TYPE_START; position = Cue.DIMEN_UNSET; positionAnchor = Cue.TYPE_UNSET; - width = Cue.DIMEN_UNSET; + // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-size + width = 1.0f; } // Construction methods. public WebvttCue build() { - if (position != Cue.DIMEN_UNSET && positionAnchor == Cue.TYPE_UNSET) { - derivePositionAnchorFromAlignment(); + line = computeLine(line, lineType); + + if (position == Cue.DIMEN_UNSET) { + position = derivePosition(textAlignment); } - return new WebvttCue(startTime, endTime, text, textAlignment, line, lineType, lineAnchor, - position, positionAnchor, width); + + if (positionAnchor == Cue.TYPE_UNSET) { + positionAnchor = derivePositionAnchor(textAlignment); + } + + width = Math.min(width, deriveMaxSize(positionAnchor, position)); + + return new WebvttCue( + startTime, + endTime, + Assertions.checkNotNull(text), + convertTextAlignment(textAlignment), + line, + lineType, + lineAnchor, + position, + positionAnchor, + width); } public Builder setStartTime(long time) { @@ -113,12 +191,12 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; return this; } - public Builder setText(SpannableStringBuilder aText) { - text = aText; + public Builder setText(CharSequence text) { + this.text = text; return this; } - public Builder setTextAlignment(Alignment textAlignment) { + public Builder setTextAlignment(@TextAlignment int textAlignment) { this.textAlignment = textAlignment; return this; } @@ -128,12 +206,12 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; return this; } - public Builder setLineType(int lineType) { + public Builder setLineType(@LineType int lineType) { this.lineType = lineType; return this; } - public Builder setLineAnchor(int lineAnchor) { + public Builder setLineAnchor(@AnchorType int lineAnchor) { this.lineAnchor = lineAnchor; return this; } @@ -143,7 +221,7 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; return this; } - public Builder setPositionAnchor(int positionAnchor) { + public Builder setPositionAnchor(@AnchorType int positionAnchor) { this.positionAnchor = positionAnchor; return this; } @@ -153,29 +231,89 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; return this; } - private Builder derivePositionAnchorFromAlignment() { - if (textAlignment == null) { - positionAnchor = Cue.TYPE_UNSET; + // https://www.w3.org/TR/webvtt1/#webvtt-cue-line + private static float computeLine(float line, @LineType int lineType) { + if (line != Cue.DIMEN_UNSET + && lineType == Cue.LINE_TYPE_FRACTION + && (line < 0.0f || line > 1.0f)) { + return 1.0f; // Step 1 + } else if (line != Cue.DIMEN_UNSET) { + // Step 2: Do nothing, line is already correct. + return line; + } else if (lineType == Cue.LINE_TYPE_FRACTION) { + return 1.0f; // Step 3 } else { - switch (textAlignment) { - case ALIGN_NORMAL: - positionAnchor = Cue.ANCHOR_TYPE_START; - break; - case ALIGN_CENTER: - positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; - break; - case ALIGN_OPPOSITE: - positionAnchor = Cue.ANCHOR_TYPE_END; - break; - default: - Log.w(TAG, "Unrecognized alignment: " + textAlignment); - positionAnchor = Cue.ANCHOR_TYPE_START; - break; - } + // Steps 4 - 10 (stacking multiple simultaneous cues) are handled by WebvttSubtitle#getCues + // and WebvttCue#isNormalCue. + return DIMEN_UNSET; } - return this; } - } + // https://www.w3.org/TR/webvtt1/#webvtt-cue-position + private static float derivePosition(@TextAlignment int textAlignment) { + switch (textAlignment) { + case TEXT_ALIGNMENT_LEFT: + return 0.0f; + case TEXT_ALIGNMENT_RIGHT: + return 1.0f; + case TEXT_ALIGNMENT_START: + case TEXT_ALIGNMENT_CENTER: + case TEXT_ALIGNMENT_END: + default: + return DEFAULT_POSITION; + } + } + // https://www.w3.org/TR/webvtt1/#webvtt-cue-position-alignment + @AnchorType + private static int derivePositionAnchor(@TextAlignment int textAlignment) { + switch (textAlignment) { + case TEXT_ALIGNMENT_LEFT: + case TEXT_ALIGNMENT_START: + return Cue.ANCHOR_TYPE_START; + case TEXT_ALIGNMENT_RIGHT: + case TEXT_ALIGNMENT_END: + return Cue.ANCHOR_TYPE_END; + case TEXT_ALIGNMENT_CENTER: + default: + return Cue.ANCHOR_TYPE_MIDDLE; + } + } + + @Nullable + private static Alignment convertTextAlignment(@TextAlignment int textAlignment) { + switch (textAlignment) { + case TEXT_ALIGNMENT_START: + case TEXT_ALIGNMENT_LEFT: + return Alignment.ALIGN_NORMAL; + case TEXT_ALIGNMENT_CENTER: + return Alignment.ALIGN_CENTER; + case TEXT_ALIGNMENT_END: + case TEXT_ALIGNMENT_RIGHT: + return Alignment.ALIGN_OPPOSITE; + default: + Log.w(TAG, "Unknown textAlignment: " + textAlignment); + return null; + } + } + + // Step 2 here: https://www.w3.org/TR/webvtt1/#processing-cue-settings + private static float deriveMaxSize(@AnchorType int positionAnchor, float position) { + switch (positionAnchor) { + case Cue.ANCHOR_TYPE_START: + return 1.0f - position; + case Cue.ANCHOR_TYPE_END: + return position; + case Cue.ANCHOR_TYPE_MIDDLE: + if (position <= 0.5f) { + return position * 2; + } else { + return (1.0f - position) * 2; + } + case Cue.TYPE_UNSET: + default: + throw new IllegalStateException(String.valueOf(positionAnchor)); + } + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index 82a21c7bdc99..b370e6779264 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -16,11 +16,11 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt; import android.graphics.Typeface; -import android.support.annotation.NonNull; -import android.text.Layout.Alignment; +import android.text.Layout; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; +import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; @@ -30,21 +30,22 @@ import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; -import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** - * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) - */ -/* package */ final class WebvttCueParser { +/** Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) */ +public final class WebvttCueParser { public static final Pattern CUE_HEADER_PATTERN = Pattern .compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$"); @@ -85,26 +86,31 @@ import java.util.regex.Pattern; * Parses the next valid WebVTT cue in a parsable array, including timestamps, settings and text. * * @param webvttData Parsable WebVTT file data. - * @param builder Builder for WebVTT Cues. - * @param styles List of styles defined by the CSS style blocks preceeding the cues. + * @param builder Builder for WebVTT Cues (output parameter). + * @param styles List of styles defined by the CSS style blocks preceding the cues. * @return Whether a valid Cue was found. */ - /* package */ boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder, - List styles) { - String firstLine = webvttData.readLine(); + public boolean parseCue( + ParsableByteArray webvttData, WebvttCue.Builder builder, List styles) { + @Nullable String firstLine = webvttData.readLine(); + if (firstLine == null) { + return false; + } Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine); if (cueHeaderMatcher.matches()) { // We have found the timestamps in the first line. No id present. return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles); - } else { - // The first line is not the timestamps, but could be the cue id. - String secondLine = webvttData.readLine(); - cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine); - if (cueHeaderMatcher.matches()) { - // We can do the rest of the parsing, including the id. - return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder, - styles); - } + } + // The first line is not the timestamps, but could be the cue id. + @Nullable String secondLine = webvttData.readLine(); + if (secondLine == null) { + return false; + } + cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine); + if (cueHeaderMatcher.matches()) { + // We can do the rest of the parsing, including the id. + return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder, + styles); } return false; } @@ -145,13 +151,13 @@ import java.util.regex.Pattern; * * @param id Id of the cue, {@code null} if it is not present. * @param markup The markup text to be parsed. - * @param styles List of styles defined by the CSS style blocks preceeding the cues. + * @param styles List of styles defined by the CSS style blocks preceding the cues. * @param builder Output builder. */ - /* package */ static void parseCueText(String id, String markup, WebvttCue.Builder builder, - List styles) { + /* package */ static void parseCueText( + @Nullable String id, String markup, WebvttCue.Builder builder, List styles) { SpannableStringBuilder spannedText = new SpannableStringBuilder(); - Stack startTagStack = new Stack<>(); + ArrayDeque startTagStack = new ArrayDeque<>(); List scratchStyleMatches = new ArrayList<>(); int pos = 0; while (pos < markup.length()) { @@ -168,8 +174,11 @@ import java.util.regex.Pattern; boolean isVoidTag = markup.charAt(pos - 2) == CHAR_SLASH; String fullTagExpression = markup.substring(ltPos + (isClosingTag ? 2 : 1), isVoidTag ? pos - 2 : pos - 1); + if (fullTagExpression.trim().isEmpty()) { + continue; + } String tagName = getTagName(fullTagExpression); - if (tagName == null || !isSupportedTag(tagName)) { + if (!isSupportedTag(tagName)) { continue; } if (isClosingTag) { @@ -217,8 +226,13 @@ import java.util.regex.Pattern; builder.setText(spannedText); } - private static boolean parseCue(String id, Matcher cueHeaderMatcher, ParsableByteArray webvttData, - WebvttCue.Builder builder, StringBuilder textBuilder, List styles) { + private static boolean parseCue( + @Nullable String id, + Matcher cueHeaderMatcher, + ParsableByteArray webvttData, + WebvttCue.Builder builder, + StringBuilder textBuilder, + List styles) { try { // Parse the cue start and end times. builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1))) @@ -232,8 +246,9 @@ import java.util.regex.Pattern; // Parse the cue text. textBuilder.setLength(0); - String line; - while ((line = webvttData.readLine()) != null && !line.isEmpty()) { + for (String line = webvttData.readLine(); + !TextUtils.isEmpty(line); + line = webvttData.readLine()) { if (textBuilder.length() > 0) { textBuilder.append("\n"); } @@ -245,14 +260,11 @@ import java.util.regex.Pattern; // Internal methods - private static void parseLineAttribute(String s, WebvttCue.Builder builder) - throws NumberFormatException { + private static void parseLineAttribute(String s, WebvttCue.Builder builder) { int commaIndex = s.indexOf(','); if (commaIndex != -1) { builder.setLineAnchor(parsePositionAnchor(s.substring(commaIndex + 1))); s = s.substring(0, commaIndex); - } else { - builder.setLineAnchor(Cue.TYPE_UNSET); } if (s.endsWith("%")) { builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION); @@ -267,18 +279,16 @@ import java.util.regex.Pattern; } } - private static void parsePositionAttribute(String s, WebvttCue.Builder builder) - throws NumberFormatException { + private static void parsePositionAttribute(String s, WebvttCue.Builder builder) { int commaIndex = s.indexOf(','); if (commaIndex != -1) { builder.setPositionAnchor(parsePositionAnchor(s.substring(commaIndex + 1))); s = s.substring(0, commaIndex); - } else { - builder.setPositionAnchor(Cue.TYPE_UNSET); } builder.setPosition(WebvttParserUtil.parsePercentage(s)); } + @Cue.AnchorType private static int parsePositionAnchor(String s) { switch (s) { case "start": @@ -294,20 +304,24 @@ import java.util.regex.Pattern; } } - private static Alignment parseTextAlignment(String s) { + @WebvttCue.Builder.TextAlignment + private static int parseTextAlignment(String s) { switch (s) { case "start": + return WebvttCue.Builder.TEXT_ALIGNMENT_START; case "left": - return Alignment.ALIGN_NORMAL; + return WebvttCue.Builder.TEXT_ALIGNMENT_LEFT; case "center": case "middle": - return Alignment.ALIGN_CENTER; + return WebvttCue.Builder.TEXT_ALIGNMENT_CENTER; case "end": + return WebvttCue.Builder.TEXT_ALIGNMENT_END; case "right": - return Alignment.ALIGN_OPPOSITE; + return WebvttCue.Builder.TEXT_ALIGNMENT_RIGHT; default: Log.w(TAG, "Invalid alignment value: " + s); - return null; + // Default value: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment + return WebvttCue.Builder.TEXT_ALIGNMENT_CENTER; } } @@ -357,8 +371,12 @@ import java.util.regex.Pattern; } } - private static void applySpansForTag(String cueId, StartTag startTag, SpannableStringBuilder text, - List styles, List scratchStyleMatches) { + private static void applySpansForTag( + @Nullable String cueId, + StartTag startTag, + SpannableStringBuilder text, + List styles, + List scratchStyleMatches) { int start = startTag.position; int end = text.length(); switch(startTag.name) { @@ -416,9 +434,10 @@ import java.util.regex.Pattern; spannedText.setSpan(new TypefaceSpan(style.getFontFamily()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } - if (style.getTextAlign() != null) { - spannedText.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + Layout.Alignment textAlign = style.getTextAlign(); + if (textAlign != null) { + spannedText.setSpan( + new AlignmentSpan.Standard(textAlign), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } switch (style.getFontSizeUnit()) { case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL: @@ -447,14 +466,15 @@ import java.util.regex.Pattern; */ private static String getTagName(String tagExpression) { tagExpression = tagExpression.trim(); - if (tagExpression.isEmpty()) { - return null; - } - return tagExpression.split("[ \\.]")[0]; + Assertions.checkArgument(!tagExpression.isEmpty()); + return Util.splitAtFirst(tagExpression, "[ \\.]")[0]; } - private static void getApplicableStyles(List declaredStyles, String id, - StartTag tag, List output) { + private static void getApplicableStyles( + List declaredStyles, + @Nullable String id, + StartTag tag, + List output) { int styleCount = declaredStyles.size(); for (int i = 0; i < styleCount; i++) { WebvttCssStyle style = declaredStyles.get(i); @@ -501,9 +521,7 @@ import java.util.regex.Pattern; public static StartTag buildStartTag(String fullTagExpression, int position) { fullTagExpression = fullTagExpression.trim(); - if (fullTagExpression.isEmpty()) { - return null; - } + Assertions.checkArgument(!fullTagExpression.isEmpty()); int voiceStartIndex = fullTagExpression.indexOf(" "); String voice; if (voiceStartIndex == -1) { @@ -512,11 +530,11 @@ import java.util.regex.Pattern; voice = fullTagExpression.substring(voiceStartIndex).trim(); fullTagExpression = fullTagExpression.substring(0, voiceStartIndex); } - String[] nameAndClasses = fullTagExpression.split("\\."); + String[] nameAndClasses = Util.split(fullTagExpression, "\\."); String name = nameAndClasses[0]; String[] classes; if (nameAndClasses.length > 1) { - classes = Arrays.copyOfRange(nameAndClasses, 1, nameAndClasses.length); + classes = Util.nullSafeArrayCopyOfRange(nameAndClasses, 1, nameAndClasses.length); } else { classes = NO_CLASSES; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java index fa817a076199..a70a49e82e8c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java @@ -16,7 +16,9 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt; import android.text.TextUtils; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle; import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleDecoderException; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; @@ -54,7 +56,7 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { } @Override - protected WebvttSubtitle decode(byte[] bytes, int length, boolean reset) + protected Subtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { parsableWebvttData.reset(bytes, length); // Initialization for consistent starting state. @@ -62,7 +64,11 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { definedStyles.clear(); // Validate the first line of the header, and skip the remainder. - WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData); + try { + WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData); + } catch (ParserException e) { + throw new SubtitleDecoderException(e); + } while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {} int event; @@ -75,10 +81,7 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { throw new SubtitleDecoderException("A style block was found after the first cue."); } parsableWebvttData.readLine(); // Consume the "STYLE" header. - WebvttCssStyle styleBlock = cssParser.parseBlock(parsableWebvttData); - if (styleBlock != null) { - definedStyles.add(styleBlock); - } + definedStyles.addAll(cssParser.parseBlock(parsableWebvttData)); } else if (event == EVENT_CUE) { if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) { subtitles.add(webvttCueBuilder.build()); @@ -105,7 +108,7 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { foundEvent = EVENT_END_OF_FILE; } else if (STYLE_START.equals(line)) { foundEvent = EVENT_STYLE_BLOCK; - } else if (COMMENT_START.startsWith(line)) { + } else if (line.startsWith(COMMENT_START)) { foundEvent = EVENT_COMMENT; } else { foundEvent = EVENT_CUE; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java index 7b94feb3bcc9..b87d014de0ac 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java @@ -15,8 +15,10 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt; -import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleDecoderException; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -25,8 +27,8 @@ import java.util.regex.Pattern; */ public final class WebvttParserUtil { - private static final Pattern COMMENT = Pattern.compile("^NOTE((\u0020|\u0009).*)?$"); - private static final Pattern HEADER = Pattern.compile("^\uFEFF?WEBVTT((\u0020|\u0009).*)?$"); + private static final Pattern COMMENT = Pattern.compile("^NOTE([ \t].*)?$"); + private static final String WEBVTT_HEADER = "WEBVTT"; private WebvttParserUtil() {} @@ -34,16 +36,26 @@ public final class WebvttParserUtil { * Reads and validates the first line of a WebVTT file. * * @param input The input from which the line should be read. - * @throws SubtitleDecoderException If the line isn't the start of a valid WebVTT file. + * @throws ParserException If the line isn't the start of a valid WebVTT file. */ - public static void validateWebvttHeaderLine(ParsableByteArray input) - throws SubtitleDecoderException { - String line = input.readLine(); - if (line == null || !HEADER.matcher(line).matches()) { - throw new SubtitleDecoderException("Expected WEBVTT. Got " + line); + public static void validateWebvttHeaderLine(ParsableByteArray input) throws ParserException { + int startPosition = input.getPosition(); + if (!isWebvttHeaderLine(input)) { + input.setPosition(startPosition); + throw new ParserException("Expected WEBVTT. Got " + input.readLine()); } } + /** + * Returns whether the given input is the first line of a WebVTT file. + * + * @param input The input from which the line should be read. + */ + public static boolean isWebvttHeaderLine(ParsableByteArray input) { + @Nullable String line = input.readLine(); + return line != null && line.startsWith(WEBVTT_HEADER); + } + /** * Parses a WebVTT timestamp. * @@ -53,12 +65,16 @@ public final class WebvttParserUtil { */ public static long parseTimestampUs(String timestamp) throws NumberFormatException { long value = 0; - String[] parts = timestamp.split("\\.", 2); - String[] subparts = parts[0].split(":"); + String[] parts = Util.splitAtFirst(timestamp, "\\."); + String[] subparts = Util.split(parts[0], ":"); for (String subpart : subparts) { - value = value * 60 + Long.parseLong(subpart); + value = (value * 60) + Long.parseLong(subpart); } - return (value * 1000 + Long.parseLong(parts[1])) * 1000; + value *= 1000; + if (parts.length == 2) { + value += Long.parseLong(parts[1]); + } + return value * 1000; } /** @@ -74,7 +90,7 @@ public final class WebvttParserUtil { } return Float.parseFloat(s.substring(0, s.length() - 1)) / 100; } - + /** * Reads lines up to and including the next WebVTT cue header. * @@ -83,8 +99,9 @@ public final class WebvttParserUtil { * reached without a cue header being found. In the case that a cue header is found, groups 1, * 2 and 3 of the returned matcher contain the start time, end time and settings list. */ + @Nullable public static Matcher findNextCueHeader(ParsableByteArray input) { - String line; + @Nullable String line; while ((line = input.readLine()) != null) { if (COMMENT.matcher(line).matches()) { // Skip until the end of the comment block. diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java index 0722b577561d..558c699eba03 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java @@ -23,7 +23,6 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; /** @@ -73,16 +72,16 @@ import java.util.List; @Override public List getCues(long timeUs) { - ArrayList list = null; + List list = new ArrayList<>(); WebvttCue firstNormalCue = null; SpannableStringBuilder normalCueTextBuilder = null; for (int i = 0; i < numCues; i++) { if ((cueTimesUs[i * 2] <= timeUs) && (timeUs < cueTimesUs[i * 2 + 1])) { - if (list == null) { - list = new ArrayList<>(); - } WebvttCue cue = cues.get(i); + // TODO(ibaker): Replace this with a closer implementation of the WebVTT spec (keeping + // individual cues, but tweaking their `line` value): + // https://www.w3.org/TR/webvtt1/#cue-computed-line if (cue.isNormalCue()) { // we want to merge all of the normal cues into a single cue to ensure they are drawn // correctly (i.e. don't overlap) and to emulate roll-up, but only if there are multiple @@ -91,9 +90,12 @@ import java.util.List; firstNormalCue = cue; } else if (normalCueTextBuilder == null) { normalCueTextBuilder = new SpannableStringBuilder(); - normalCueTextBuilder.append(firstNormalCue.text).append("\n").append(cue.text); + normalCueTextBuilder + .append(Assertions.checkNotNull(firstNormalCue.text)) + .append("\n") + .append(Assertions.checkNotNull(cue.text)); } else { - normalCueTextBuilder.append("\n").append(cue.text); + normalCueTextBuilder.append("\n").append(Assertions.checkNotNull(cue.text)); } } else { list.add(cue); @@ -102,17 +104,12 @@ import java.util.List; } if (normalCueTextBuilder != null) { // there were multiple normal cues, so create a new cue with all of the text - list.add(new WebvttCue(normalCueTextBuilder)); + list.add(new WebvttCue.Builder().setText(normalCueTextBuilder).build()); } else if (firstNormalCue != null) { // there was only a single normal cue, so just add it to the list list.add(firstNormalCue); } - - if (list != null) { - return list; - } else { - return Collections.emptyList(); - } + return list; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/package-info.java new file mode 100644 index 000000000000..e2c014d539d2 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + * + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index 26b9012e1025..33f8606e9b9b 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -15,13 +15,20 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection; -import android.os.SystemClock; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.SimpleExoPlayer; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A bandwidth based adaptive {@link TrackSelection}, whose selected track is updated to be the one @@ -29,79 +36,272 @@ import java.util.List; */ public class AdaptiveTrackSelection extends BaseTrackSelection { - /** - * Factory for {@link AdaptiveTrackSelection} instances. - */ - public static final class Factory implements TrackSelection.Factory { + /** Factory for {@link AdaptiveTrackSelection} instances. */ + public static class Factory implements TrackSelection.Factory { - private final BandwidthMeter bandwidthMeter; - private final int maxInitialBitrate; + @Nullable private final BandwidthMeter bandwidthMeter; private final int minDurationForQualityIncreaseMs; private final int maxDurationForQualityDecreaseMs; private final int minDurationToRetainAfterDiscardMs; private final float bandwidthFraction; + private final float bufferedFractionToLiveEdgeForQualityIncrease; + private final long minTimeBetweenBufferReevaluationMs; + private final Clock clock; - /** - * @param bandwidthMeter Provides an estimate of the currently available bandwidth. - */ - public Factory(BandwidthMeter bandwidthMeter) { - this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, + /** Creates an adaptive track selection factory with default parameters. */ + public Factory() { + this( DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, - DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION); + DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + DEFAULT_BANDWIDTH_FRACTION, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); } /** - * @param bandwidthMeter Provides an estimate of the currently available bandwidth. - * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed - * when a bandwidth estimate is unavailable. - * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for - * the selected track to switch to one of higher quality. - * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for - * the selected track to switch to one of lower quality. + * @deprecated Use {@link #Factory()} instead. Custom bandwidth meter should be directly passed + * to the player in {@link SimpleExoPlayer.Builder}. + */ + @Deprecated + @SuppressWarnings("deprecation") + public Factory(BandwidthMeter bandwidthMeter) { + this( + bandwidthMeter, + DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, + DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, + DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + DEFAULT_BANDWIDTH_FRACTION, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); + } + + /** + * Creates an adaptive track selection factory. + * + * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the + * selected track to switch to one of higher quality. + * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the + * selected track to switch to one of lower quality. * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher * quality, the selection may indicate that media already buffered at the lower quality can * be discarded to speed up the switch. This is the minimum duration of media that must be * retained at the lower quality. * @param bandwidthFraction The fraction of the available bandwidth that the selection should - * consider available for use. Setting to a value less than 1 is recommended to account - * for inaccuracies in the bandwidth estimator. + * consider available for use. Setting to a value less than 1 is recommended to account for + * inaccuracies in the bandwidth estimator. */ - public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate, - int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, - int minDurationToRetainAfterDiscardMs, float bandwidthFraction) { + public Factory( + int minDurationForQualityIncreaseMs, + int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, + float bandwidthFraction) { + this( + minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, + bandwidthFraction, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); + } + + /** + * @deprecated Use {@link #Factory(int, int, int, float)} instead. Custom bandwidth meter should + * be directly passed to the player in {@link SimpleExoPlayer.Builder}. + */ + @Deprecated + @SuppressWarnings("deprecation") + public Factory( + BandwidthMeter bandwidthMeter, + int minDurationForQualityIncreaseMs, + int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, + float bandwidthFraction) { + this( + bandwidthMeter, + minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, + bandwidthFraction, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); + } + + /** + * Creates an adaptive track selection factory. + * + * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the + * selected track to switch to one of higher quality. + * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the + * selected track to switch to one of lower quality. + * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher + * quality, the selection may indicate that media already buffered at the lower quality can + * be discarded to speed up the switch. This is the minimum duration of media that must be + * retained at the lower quality. + * @param bandwidthFraction The fraction of the available bandwidth that the selection should + * consider available for use. Setting to a value less than 1 is recommended to account for + * inaccuracies in the bandwidth estimator. + * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the + * duration from current playback position to the live edge that has to be buffered before + * the selected track can be switched to one of higher quality. This parameter is only + * applied when the playback position is closer to the live edge than {@code + * minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher + * quality from happening. + * @param minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its + * buffer and discard some chunks of lower quality to improve the playback quality if + * network conditions have changed. This is the minimum duration between 2 consecutive + * buffer reevaluation calls. + * @param clock A {@link Clock}. + */ + @SuppressWarnings("deprecation") + public Factory( + int minDurationForQualityIncreaseMs, + int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, + float bandwidthFraction, + float bufferedFractionToLiveEdgeForQualityIncrease, + long minTimeBetweenBufferReevaluationMs, + Clock clock) { + this( + /* bandwidthMeter= */ null, + minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, + bandwidthFraction, + bufferedFractionToLiveEdgeForQualityIncrease, + minTimeBetweenBufferReevaluationMs, + clock); + } + + /** + * @deprecated Use {@link #Factory(int, int, int, float, float, long, Clock)} instead. Custom + * bandwidth meter should be directly passed to the player in {@link + * SimpleExoPlayer.Builder}. + */ + @Deprecated + public Factory( + @Nullable BandwidthMeter bandwidthMeter, + int minDurationForQualityIncreaseMs, + int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, + float bandwidthFraction, + float bufferedFractionToLiveEdgeForQualityIncrease, + long minTimeBetweenBufferReevaluationMs, + Clock clock) { this.bandwidthMeter = bandwidthMeter; - this.maxInitialBitrate = maxInitialBitrate; this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs; this.maxDurationForQualityDecreaseMs = maxDurationForQualityDecreaseMs; this.minDurationToRetainAfterDiscardMs = minDurationToRetainAfterDiscardMs; this.bandwidthFraction = bandwidthFraction; + this.bufferedFractionToLiveEdgeForQualityIncrease = + bufferedFractionToLiveEdgeForQualityIncrease; + this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs; + this.clock = clock; } @Override - public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... tracks) { - return new AdaptiveTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate, - minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, - minDurationToRetainAfterDiscardMs, bandwidthFraction); + public final @NullableType TrackSelection[] createTrackSelections( + @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { + if (this.bandwidthMeter != null) { + bandwidthMeter = this.bandwidthMeter; + } + TrackSelection[] selections = new TrackSelection[definitions.length]; + int totalFixedBandwidth = 0; + for (int i = 0; i < definitions.length; i++) { + Definition definition = definitions[i]; + if (definition != null && definition.tracks.length == 1) { + // Make fixed selections first to know their total bandwidth. + selections[i] = + new FixedTrackSelection( + definition.group, definition.tracks[0], definition.reason, definition.data); + int trackBitrate = definition.group.getFormat(definition.tracks[0]).bitrate; + if (trackBitrate != Format.NO_VALUE) { + totalFixedBandwidth += trackBitrate; + } + } + } + List adaptiveSelections = new ArrayList<>(); + for (int i = 0; i < definitions.length; i++) { + Definition definition = definitions[i]; + if (definition != null && definition.tracks.length > 1) { + AdaptiveTrackSelection adaptiveSelection = + createAdaptiveTrackSelection( + definition.group, bandwidthMeter, definition.tracks, totalFixedBandwidth); + adaptiveSelections.add(adaptiveSelection); + selections[i] = adaptiveSelection; + } + } + if (adaptiveSelections.size() > 1) { + long[][] adaptiveTrackBitrates = new long[adaptiveSelections.size()][]; + for (int i = 0; i < adaptiveSelections.size(); i++) { + AdaptiveTrackSelection adaptiveSelection = adaptiveSelections.get(i); + adaptiveTrackBitrates[i] = new long[adaptiveSelection.length()]; + for (int j = 0; j < adaptiveSelection.length(); j++) { + adaptiveTrackBitrates[i][j] = + adaptiveSelection.getFormat(adaptiveSelection.length() - j - 1).bitrate; + } + } + long[][][] bandwidthCheckpoints = getAllocationCheckpoints(adaptiveTrackBitrates); + for (int i = 0; i < adaptiveSelections.size(); i++) { + adaptiveSelections + .get(i) + .experimental_setBandwidthAllocationCheckpoints(bandwidthCheckpoints[i]); + } + } + return selections; } + /** + * Creates a single adaptive selection for the given group, bandwidth meter and tracks. + * + * @param group The {@link TrackGroup}. + * @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks. + * @param tracks The indices of the selected tracks in the track group. + * @param totalFixedTrackBandwidth The total bandwidth used by all non-adaptive tracks, in bits + * per second. + * @return An {@link AdaptiveTrackSelection} for the specified tracks. + */ + protected AdaptiveTrackSelection createAdaptiveTrackSelection( + TrackGroup group, + BandwidthMeter bandwidthMeter, + int[] tracks, + int totalFixedTrackBandwidth) { + return new AdaptiveTrackSelection( + group, + tracks, + new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, totalFixedTrackBandwidth), + minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, + bufferedFractionToLiveEdgeForQualityIncrease, + minTimeBetweenBufferReevaluationMs, + clock); + } } - public static final int DEFAULT_MAX_INITIAL_BITRATE = 800000; public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000; public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000; public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000; - public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f; + public static final float DEFAULT_BANDWIDTH_FRACTION = 0.7f; + public static final float DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE = 0.75f; + public static final long DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 2000; - private final BandwidthMeter bandwidthMeter; - private final int maxInitialBitrate; + private final BandwidthProvider bandwidthProvider; private final long minDurationForQualityIncreaseUs; private final long maxDurationForQualityDecreaseUs; private final long minDurationToRetainAfterDiscardUs; - private final float bandwidthFraction; + private final float bufferedFractionToLiveEdgeForQualityIncrease; + private final long minTimeBetweenBufferReevaluationMs; + private final Clock clock; + private float playbackSpeed; private int selectedIndex; private int reason; + private long lastBufferEvaluationMs; /** * @param group The {@link TrackGroup}. @@ -111,10 +311,18 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { */ public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter) { - this (group, tracks, bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, + this( + group, + tracks, + bandwidthMeter, + /* reservedBandwidth= */ 0, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, - DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION); + DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + DEFAULT_BANDWIDTH_FRACTION, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); } /** @@ -122,55 +330,134 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be * empty. May be in any order. * @param bandwidthMeter Provides an estimate of the currently available bandwidth. - * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed when a - * bandwidth estimate is unavailable. + * @param reservedBandwidth The reserved bandwidth, which shouldn't be considered available for + * use, in bits per second. * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the * selected track to switch to one of higher quality. * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the * selected track to switch to one of lower quality. * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher - * quality, the selection may indicate that media already buffered at the lower quality can - * be discarded to speed up the switch. This is the minimum duration of media that must be + * quality, the selection may indicate that media already buffered at the lower quality can be + * discarded to speed up the switch. This is the minimum duration of media that must be * retained at the lower quality. * @param bandwidthFraction The fraction of the available bandwidth that the selection should - * consider available for use. Setting to a value less than 1 is recommended to account - * for inaccuracies in the bandwidth estimator. + * consider available for use. Setting to a value less than 1 is recommended to account for + * inaccuracies in the bandwidth estimator. + * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the + * duration from current playback position to the live edge that has to be buffered before the + * selected track can be switched to one of higher quality. This parameter is only applied + * when the playback position is closer to the live edge than {@code + * minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher + * quality from happening. + * @param minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its + * buffer and discard some chunks of lower quality to improve the playback quality if network + * condition has changed. This is the minimum duration between 2 consecutive buffer + * reevaluation calls. */ - public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, - int maxInitialBitrate, long minDurationForQualityIncreaseMs, - long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, - float bandwidthFraction) { + public AdaptiveTrackSelection( + TrackGroup group, + int[] tracks, + BandwidthMeter bandwidthMeter, + long reservedBandwidth, + long minDurationForQualityIncreaseMs, + long maxDurationForQualityDecreaseMs, + long minDurationToRetainAfterDiscardMs, + float bandwidthFraction, + float bufferedFractionToLiveEdgeForQualityIncrease, + long minTimeBetweenBufferReevaluationMs, + Clock clock) { + this( + group, + tracks, + new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, reservedBandwidth), + minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, + bufferedFractionToLiveEdgeForQualityIncrease, + minTimeBetweenBufferReevaluationMs, + clock); + } + + private AdaptiveTrackSelection( + TrackGroup group, + int[] tracks, + BandwidthProvider bandwidthProvider, + long minDurationForQualityIncreaseMs, + long maxDurationForQualityDecreaseMs, + long minDurationToRetainAfterDiscardMs, + float bufferedFractionToLiveEdgeForQualityIncrease, + long minTimeBetweenBufferReevaluationMs, + Clock clock) { super(group, tracks); - this.bandwidthMeter = bandwidthMeter; - this.maxInitialBitrate = maxInitialBitrate; + this.bandwidthProvider = bandwidthProvider; this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L; this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L; this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L; - this.bandwidthFraction = bandwidthFraction; - selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE); - reason = C.SELECTION_REASON_INITIAL; + this.bufferedFractionToLiveEdgeForQualityIncrease = + bufferedFractionToLiveEdgeForQualityIncrease; + this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs; + this.clock = clock; + playbackSpeed = 1f; + reason = C.SELECTION_REASON_UNKNOWN; + lastBufferEvaluationMs = C.TIME_UNSET; + } + + /** + * Sets checkpoints to determine the allocation bandwidth based on the total bandwidth. + * + * @param allocationCheckpoints List of checkpoints. Each element must be a long[2], with [0] + * being the total bandwidth and [1] being the allocated bandwidth. + */ + public void experimental_setBandwidthAllocationCheckpoints(long[][] allocationCheckpoints) { + ((DefaultBandwidthProvider) bandwidthProvider) + .experimental_setBandwidthAllocationCheckpoints(allocationCheckpoints); } @Override - public void updateSelectedTrack(long bufferedDurationUs) { - long nowMs = SystemClock.elapsedRealtime(); - // Get the current and ideal selections. + public void enable() { + lastBufferEvaluationMs = C.TIME_UNSET; + } + + @Override + public void onPlaybackSpeed(float playbackSpeed) { + this.playbackSpeed = playbackSpeed; + } + + @Override + public void updateSelectedTrack( + long playbackPositionUs, + long bufferedDurationUs, + long availableDurationUs, + List queue, + MediaChunkIterator[] mediaChunkIterators) { + long nowMs = clock.elapsedRealtime(); + + // Make initial selection + if (reason == C.SELECTION_REASON_UNKNOWN) { + reason = C.SELECTION_REASON_INITIAL; + selectedIndex = determineIdealSelectedIndex(nowMs); + return; + } + + // Stash the current selection, then make a new one. int currentSelectedIndex = selectedIndex; - Format currentFormat = getSelectedFormat(); - int idealSelectedIndex = determineIdealSelectedIndex(nowMs); - Format idealFormat = getFormat(idealSelectedIndex); - // Assume we can switch to the ideal selection. - selectedIndex = idealSelectedIndex; - // Revert back to the current selection if conditions are not suitable for switching. - if (currentFormat != null && !isBlacklisted(selectedIndex, nowMs)) { - if (idealFormat.bitrate > currentFormat.bitrate - && bufferedDurationUs < minDurationForQualityIncreaseUs) { - // The ideal track is a higher quality, but we have insufficient buffer to safely switch + selectedIndex = determineIdealSelectedIndex(nowMs); + if (selectedIndex == currentSelectedIndex) { + return; + } + + if (!isBlacklisted(currentSelectedIndex, nowMs)) { + // Revert back to the current selection if conditions are not suitable for switching. + Format currentFormat = getFormat(currentSelectedIndex); + Format selectedFormat = getFormat(selectedIndex); + if (selectedFormat.bitrate > currentFormat.bitrate + && bufferedDurationUs < minDurationForQualityIncreaseUs(availableDurationUs)) { + // The selected track is a higher quality, but we have insufficient buffer to safely switch // up. Defer switching up for now. selectedIndex = currentSelectedIndex; - } else if (idealFormat.bitrate < currentFormat.bitrate + } else if (selectedFormat.bitrate < currentFormat.bitrate && bufferedDurationUs >= maxDurationForQualityDecreaseUs) { - // The ideal track is a lower quality, but we have sufficient buffer to defer switching + // The selected track is a lower quality, but we have sufficient buffer to defer switching // down for now. selectedIndex = currentSelectedIndex; } @@ -192,21 +479,33 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { } @Override + @Nullable public Object getSelectionData() { return null; } @Override public int evaluateQueueSize(long playbackPositionUs, List queue) { + long nowMs = clock.elapsedRealtime(); + if (!shouldEvaluateQueueSize(nowMs)) { + return queue.size(); + } + + lastBufferEvaluationMs = nowMs; if (queue.isEmpty()) { return 0; } + int queueSize = queue.size(); - long bufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs; - if (bufferedDurationUs < minDurationToRetainAfterDiscardUs) { + MediaChunk lastChunk = queue.get(queueSize - 1); + long playoutBufferedDurationBeforeLastChunkUs = + Util.getPlayoutDurationForMediaDuration( + lastChunk.startTimeUs - playbackPositionUs, playbackSpeed); + long minDurationToRetainAfterDiscardUs = getMinDurationToRetainAfterDiscardUs(); + if (playoutBufferedDurationBeforeLastChunkUs < minDurationToRetainAfterDiscardUs) { return queueSize; } - int idealSelectedIndex = determineIdealSelectedIndex(SystemClock.elapsedRealtime()); + int idealSelectedIndex = determineIdealSelectedIndex(nowMs); Format idealFormat = getFormat(idealSelectedIndex); // If the chunks contain video, discard from the first SD chunk beyond // minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal @@ -214,8 +513,10 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { for (int i = 0; i < queueSize; i++) { MediaChunk chunk = queue.get(i); Format format = chunk.trackFormat; - long durationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs; - if (durationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs + long mediaDurationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs; + long playoutDurationBeforeThisChunkUs = + Util.getPlayoutDurationForMediaDuration(mediaDurationBeforeThisChunkUs, playbackSpeed); + if (playoutDurationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs && format.bitrate < idealFormat.bitrate && format.height != Format.NO_VALUE && format.height < 720 && format.width != Format.NO_VALUE && format.width < 1280 @@ -226,21 +527,57 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { return queueSize; } + /** + * Called when updating the selected track to determine whether a candidate track can be selected. + * + * @param format The {@link Format} of the candidate track. + * @param trackBitrate The estimated bitrate of the track. May differ from {@link Format#bitrate} + * if a more accurate estimate of the current track bitrate is available. + * @param playbackSpeed The current playback speed. + * @param effectiveBitrate The bitrate available to this selection. + * @return Whether this {@link Format} can be selected. + */ + @SuppressWarnings("unused") + protected boolean canSelectFormat( + Format format, int trackBitrate, float playbackSpeed, long effectiveBitrate) { + return Math.round(trackBitrate * playbackSpeed) <= effectiveBitrate; + } + + /** + * Called from {@link #evaluateQueueSize(long, List)} to determine whether an evaluation should be + * performed. + * + * @param nowMs The current value of {@link Clock#elapsedRealtime()}. + * @return Whether an evaluation should be performed. + */ + protected boolean shouldEvaluateQueueSize(long nowMs) { + return lastBufferEvaluationMs == C.TIME_UNSET + || nowMs - lastBufferEvaluationMs >= minTimeBetweenBufferReevaluationMs; + } + + /** + * Called from {@link #evaluateQueueSize(long, List)} to determine the minimum duration of buffer + * to retain after discarding chunks. + * + * @return The minimum duration of buffer to retain after discarding chunks, in microseconds. + */ + protected long getMinDurationToRetainAfterDiscardUs() { + return minDurationToRetainAfterDiscardUs; + } + /** * Computes the ideal selected index ignoring buffer health. * - * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}, or - * {@link Long#MIN_VALUE} to ignore blacklisting. + * @param nowMs The current time in the timebase of {@link Clock#elapsedRealtime()}, or {@link + * Long#MIN_VALUE} to ignore blacklisting. */ private int determineIdealSelectedIndex(long nowMs) { - long bitrateEstimate = bandwidthMeter.getBitrateEstimate(); - long effectiveBitrate = bitrateEstimate == BandwidthMeter.NO_ESTIMATE - ? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction); + long effectiveBitrate = bandwidthProvider.getAllocatedBandwidth(); int lowestBitrateNonBlacklistedIndex = 0; for (int i = 0; i < length; i++) { if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) { Format format = getFormat(i); - if (format.bitrate <= effectiveBitrate) { + if (canSelectFormat(format, format.bitrate, playbackSpeed, effectiveBitrate)) { return i; } else { lowestBitrateNonBlacklistedIndex = i; @@ -250,4 +587,175 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { return lowestBitrateNonBlacklistedIndex; } + private long minDurationForQualityIncreaseUs(long availableDurationUs) { + boolean isAvailableDurationTooShort = availableDurationUs != C.TIME_UNSET + && availableDurationUs <= minDurationForQualityIncreaseUs; + return isAvailableDurationTooShort + ? (long) (availableDurationUs * bufferedFractionToLiveEdgeForQualityIncrease) + : minDurationForQualityIncreaseUs; + } + + /** Provides the allocated bandwidth. */ + private interface BandwidthProvider { + + /** Returns the allocated bitrate. */ + long getAllocatedBandwidth(); + } + + private static final class DefaultBandwidthProvider implements BandwidthProvider { + + private final BandwidthMeter bandwidthMeter; + private final float bandwidthFraction; + private final long reservedBandwidth; + + @Nullable private long[][] allocationCheckpoints; + + /* package */ + // the constructor does not initialize fields: allocationCheckpoints + @SuppressWarnings("nullness:initialization.fields.uninitialized") + DefaultBandwidthProvider( + BandwidthMeter bandwidthMeter, float bandwidthFraction, long reservedBandwidth) { + this.bandwidthMeter = bandwidthMeter; + this.bandwidthFraction = bandwidthFraction; + this.reservedBandwidth = reservedBandwidth; + } + + // unboxing a possibly-null reference allocationCheckpoints[nextIndex][0] + @SuppressWarnings("nullness:unboxing.of.nullable") + @Override + public long getAllocatedBandwidth() { + long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction); + long allocatableBandwidth = Math.max(0L, totalBandwidth - reservedBandwidth); + if (allocationCheckpoints == null) { + return allocatableBandwidth; + } + int nextIndex = 1; + while (nextIndex < allocationCheckpoints.length - 1 + && allocationCheckpoints[nextIndex][0] < allocatableBandwidth) { + nextIndex++; + } + long[] previous = allocationCheckpoints[nextIndex - 1]; + long[] next = allocationCheckpoints[nextIndex]; + float fractionBetweenCheckpoints = + (float) (allocatableBandwidth - previous[0]) / (next[0] - previous[0]); + return previous[1] + (long) (fractionBetweenCheckpoints * (next[1] - previous[1])); + } + + /* package */ void experimental_setBandwidthAllocationCheckpoints( + long[][] allocationCheckpoints) { + Assertions.checkArgument(allocationCheckpoints.length >= 2); + this.allocationCheckpoints = allocationCheckpoints; + } + } + + /** + * Returns allocation checkpoints for allocating bandwidth between multiple adaptive track + * selections. + * + * @param trackBitrates Array of [selectionIndex][trackIndex] -> trackBitrate. + * @return Array of allocation checkpoints [selectionIndex][checkpointIndex][2] with [0]=total + * bandwidth at checkpoint and [1]=allocated bandwidth at checkpoint. + */ + private static long[][][] getAllocationCheckpoints(long[][] trackBitrates) { + // Algorithm: + // 1. Use log bitrates to treat all resolution update steps equally. + // 2. Distribute switch points for each selection equally in the same [0.0-1.0] range. + // 3. Switch up one format at a time in the order of the switch points. + double[][] logBitrates = getLogArrayValues(trackBitrates); + double[][] switchPoints = getSwitchPoints(logBitrates); + + // There will be (count(switch point) + 3) checkpoints: + // [0] = all zero, [1] = minimum bitrates, [2-(end-1)] = up-switch points, + // [end] = extra point to set slope for additional bitrate. + int checkpointCount = countArrayElements(switchPoints) + 3; + long[][][] checkpoints = new long[logBitrates.length][checkpointCount][2]; + int[] currentSelection = new int[logBitrates.length]; + setCheckpointValues(checkpoints, /* checkpointIndex= */ 1, trackBitrates, currentSelection); + for (int checkpointIndex = 2; checkpointIndex < checkpointCount - 1; checkpointIndex++) { + int nextUpdateIndex = 0; + double nextUpdateSwitchPoint = Double.MAX_VALUE; + for (int i = 0; i < logBitrates.length; i++) { + if (currentSelection[i] + 1 == logBitrates[i].length) { + continue; + } + double switchPoint = switchPoints[i][currentSelection[i]]; + if (switchPoint < nextUpdateSwitchPoint) { + nextUpdateSwitchPoint = switchPoint; + nextUpdateIndex = i; + } + } + currentSelection[nextUpdateIndex]++; + setCheckpointValues(checkpoints, checkpointIndex, trackBitrates, currentSelection); + } + for (long[][] points : checkpoints) { + points[checkpointCount - 1][0] = 2 * points[checkpointCount - 2][0]; + points[checkpointCount - 1][1] = 2 * points[checkpointCount - 2][1]; + } + return checkpoints; + } + + /** Converts all input values to Math.log(value). */ + private static double[][] getLogArrayValues(long[][] values) { + double[][] logValues = new double[values.length][]; + for (int i = 0; i < values.length; i++) { + logValues[i] = new double[values[i].length]; + for (int j = 0; j < values[i].length; j++) { + logValues[i][j] = values[i][j] == Format.NO_VALUE ? 0 : Math.log(values[i][j]); + } + } + return logValues; + } + + /** + * Returns idealized switch points for each switch between consecutive track selection bitrates. + * + * @param logBitrates Log bitrates with [selectionCount][formatCount]. + * @return Linearly distributed switch points in the range of [0.0-1.0]. + */ + private static double[][] getSwitchPoints(double[][] logBitrates) { + double[][] switchPoints = new double[logBitrates.length][]; + for (int i = 0; i < logBitrates.length; i++) { + switchPoints[i] = new double[logBitrates[i].length - 1]; + if (switchPoints[i].length == 0) { + continue; + } + double totalBitrateDiff = logBitrates[i][logBitrates[i].length - 1] - logBitrates[i][0]; + for (int j = 0; j < logBitrates[i].length - 1; j++) { + double switchBitrate = 0.5 * (logBitrates[i][j] + logBitrates[i][j + 1]); + switchPoints[i][j] = + totalBitrateDiff == 0.0 ? 1.0 : (switchBitrate - logBitrates[i][0]) / totalBitrateDiff; + } + } + return switchPoints; + } + + /** Returns total number of elements in a 2D array. */ + private static int countArrayElements(double[][] array) { + int count = 0; + for (double[] subArray : array) { + count += subArray.length; + } + return count; + } + + /** + * Sets checkpoint bitrates. + * + * @param checkpoints Output checkpoints with [selectionIndex][checkpointIndex][2] where [0]=Total + * bitrate and [1]=Allocated bitrate. + * @param checkpointIndex The checkpoint index. + * @param trackBitrates The track bitrates with [selectionIndex][trackIndex]. + * @param selectedTracks The indices of selected tracks for each selection for this checkpoint. + */ + private static void setCheckpointValues( + long[][][] checkpoints, int checkpointIndex, long[][] trackBitrates, int[] selectedTracks) { + long totalBitrate = 0; + for (int i = 0; i < checkpoints.length; i++) { + checkpoints[i][checkpointIndex][1] = trackBitrates[i][selectedTracks[i]]; + totalBitrate += checkpoints[i][checkpointIndex][1]; + } + for (long[][] points : checkpoints) { + points[checkpointIndex][0] = totalBitrate; + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java index 9dc6cf2004a9..d7e94cb56179 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -16,11 +16,13 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection; import android.os.SystemClock; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -78,6 +80,16 @@ public abstract class BaseTrackSelection implements TrackSelection { blacklistUntilTimes = new long[length]; } + @Override + public void enable() { + // Do nothing. + } + + @Override + public void disable() { + // Do nothing. + } + @Override public final TrackGroup getTrackGroup() { return group; @@ -99,6 +111,7 @@ public abstract class BaseTrackSelection implements TrackSelection { } @Override + @SuppressWarnings("ReferenceEquality") public final int indexOf(Format format) { for (int i = 0; i < length; i++) { if (formats[i] == format) { @@ -128,6 +141,11 @@ public abstract class BaseTrackSelection implements TrackSelection { return tracks[getSelectedIndex()]; } + @Override + public void onPlaybackSpeed(float playbackSpeed) { + // Do nothing. + } + @Override public int evaluateQueueSize(long playbackPositionUs, List queue) { return queue.size(); @@ -143,7 +161,10 @@ public abstract class BaseTrackSelection implements TrackSelection { if (!canBlacklist) { return false; } - blacklistUntilTimes[index] = Math.max(blacklistUntilTimes[index], nowMs + blacklistDurationMs); + blacklistUntilTimes[index] = + Math.max( + blacklistUntilTimes[index], + Util.addWithOverflowDefault(nowMs, blacklistDurationMs, Long.MAX_VALUE)); return true; } @@ -167,8 +188,10 @@ public abstract class BaseTrackSelection implements TrackSelection { return hashCode; } + // Track groups are compared by identity not value, as distinct groups may have the same value. @Override - public boolean equals(Object obj) { + @SuppressWarnings({"ReferenceEquality", "EqualsGetClass"}) + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java new file mode 100644 index 000000000000..735889bfaa76 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java @@ -0,0 +1,494 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection; + +import android.util.Pair; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.DefaultLoadControl; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.LoadControl; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection.Definition; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultAllocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock; +import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** + * Builder for a {@link TrackSelection.Factory} and {@link LoadControl} that implement buffer size + * based track adaptation. + */ +public final class BufferSizeAdaptationBuilder { + + /** Dynamic filter for formats, which is applied when selecting a new track. */ + public interface DynamicFormatFilter { + + /** Filter which allows all formats. */ + DynamicFormatFilter NO_FILTER = (format, trackBitrate, isInitialSelection) -> true; + + /** + * Called when updating the selected track to determine whether a candidate track is allowed. If + * no format is allowed or eligible, the lowest quality format will be used. + * + * @param format The {@link Format} of the candidate track. + * @param trackBitrate The estimated bitrate of the track. May differ from {@link + * Format#bitrate} if a more accurate estimate of the current track bitrate is available. + * @param isInitialSelection Whether this is for the initial track selection. + */ + boolean isFormatAllowed(Format format, int trackBitrate, boolean isInitialSelection); + } + + /** + * The default minimum duration of media that the player will attempt to ensure is buffered at all + * times, in milliseconds. + */ + public static final int DEFAULT_MIN_BUFFER_MS = 15000; + + /** + * The default maximum duration of media that the player will attempt to buffer, in milliseconds. + */ + public static final int DEFAULT_MAX_BUFFER_MS = 50000; + + /** + * The default duration of media that must be buffered for playback to start or resume following a + * user action such as a seek, in milliseconds. + */ + public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = + DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS; + + /** + * The default duration of media that must be buffered for playback to resume after a rebuffer, in + * milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action. + */ + public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = + DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; + + /** + * The default offset the current duration of buffered media must deviate from the ideal duration + * of buffered media for the currently selected format, before the selected format is changed. + */ + public static final int DEFAULT_HYSTERESIS_BUFFER_MS = 5000; + + /** + * During start-up phase, the default fraction of the available bandwidth that the selection + * should consider available for use. Setting to a value less than 1 is recommended to account for + * inaccuracies in the bandwidth estimator. + */ + public static final float DEFAULT_START_UP_BANDWIDTH_FRACTION = + AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION; + + /** + * During start-up phase, the default minimum duration of buffered media required for the selected + * track to switch to one of higher quality based on measured bandwidth. + */ + public static final int DEFAULT_START_UP_MIN_BUFFER_FOR_QUALITY_INCREASE_MS = + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS; + + @Nullable private DefaultAllocator allocator; + private Clock clock; + private int minBufferMs; + private int maxBufferMs; + private int bufferForPlaybackMs; + private int bufferForPlaybackAfterRebufferMs; + private int hysteresisBufferMs; + private float startUpBandwidthFraction; + private int startUpMinBufferForQualityIncreaseMs; + private DynamicFormatFilter dynamicFormatFilter; + private boolean buildCalled; + + /** Creates builder with default values. */ + public BufferSizeAdaptationBuilder() { + clock = Clock.DEFAULT; + minBufferMs = DEFAULT_MIN_BUFFER_MS; + maxBufferMs = DEFAULT_MAX_BUFFER_MS; + bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS; + bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; + hysteresisBufferMs = DEFAULT_HYSTERESIS_BUFFER_MS; + startUpBandwidthFraction = DEFAULT_START_UP_BANDWIDTH_FRACTION; + startUpMinBufferForQualityIncreaseMs = DEFAULT_START_UP_MIN_BUFFER_FOR_QUALITY_INCREASE_MS; + dynamicFormatFilter = DynamicFormatFilter.NO_FILTER; + } + + /** + * Set the clock to use. Should only be set for testing purposes. + * + * @param clock The {@link Clock}. + * @return This builder, for convenience. + * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. + */ + public BufferSizeAdaptationBuilder setClock(Clock clock) { + Assertions.checkState(!buildCalled); + this.clock = clock; + return this; + } + + /** + * Sets the {@link DefaultAllocator} used by the loader. + * + * @param allocator The {@link DefaultAllocator}. + * @return This builder, for convenience. + * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. + */ + public BufferSizeAdaptationBuilder setAllocator(DefaultAllocator allocator) { + Assertions.checkState(!buildCalled); + this.allocator = allocator; + return this; + } + + /** + * Sets the buffer duration parameters. + * + * @param minBufferMs The minimum duration of media that the player will attempt to ensure is + * buffered at all times, in milliseconds. + * @param maxBufferMs The maximum duration of media that the player will attempt to buffer, in + * milliseconds. + * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or + * resume following a user action such as a seek, in milliseconds. + * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for + * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by + * buffer depletion rather than a user action. + * @return This builder, for convenience. + * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. + */ + public BufferSizeAdaptationBuilder setBufferDurationsMs( + int minBufferMs, + int maxBufferMs, + int bufferForPlaybackMs, + int bufferForPlaybackAfterRebufferMs) { + Assertions.checkState(!buildCalled); + this.minBufferMs = minBufferMs; + this.maxBufferMs = maxBufferMs; + this.bufferForPlaybackMs = bufferForPlaybackMs; + this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs; + return this; + } + + /** + * Sets the hysteresis buffer used to prevent repeated format switching. + * + * @param hysteresisBufferMs The offset the current duration of buffered media must deviate from + * the ideal duration of buffered media for the currently selected format, before the selected + * format is changed. This value must be smaller than {@code maxBufferMs - minBufferMs}. + * @return This builder, for convenience. + * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. + */ + public BufferSizeAdaptationBuilder setHysteresisBufferMs(int hysteresisBufferMs) { + Assertions.checkState(!buildCalled); + this.hysteresisBufferMs = hysteresisBufferMs; + return this; + } + + /** + * Sets track selection parameters used during the start-up phase before the selection can be made + * purely on based on buffer size. During the start-up phase the selection is based on the current + * bandwidth estimate. + * + * @param bandwidthFraction The fraction of the available bandwidth that the selection should + * consider available for use. Setting to a value less than 1 is recommended to account for + * inaccuracies in the bandwidth estimator. + * @param minBufferForQualityIncreaseMs The minimum duration of buffered media required for the + * selected track to switch to one of higher quality. + * @return This builder, for convenience. + * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. + */ + public BufferSizeAdaptationBuilder setStartUpTrackSelectionParameters( + float bandwidthFraction, int minBufferForQualityIncreaseMs) { + Assertions.checkState(!buildCalled); + this.startUpBandwidthFraction = bandwidthFraction; + this.startUpMinBufferForQualityIncreaseMs = minBufferForQualityIncreaseMs; + return this; + } + + /** + * Sets the {@link DynamicFormatFilter} to use when updating the selected track. + * + * @param dynamicFormatFilter The {@link DynamicFormatFilter}. + * @return This builder, for convenience. + * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. + */ + public BufferSizeAdaptationBuilder setDynamicFormatFilter( + DynamicFormatFilter dynamicFormatFilter) { + Assertions.checkState(!buildCalled); + this.dynamicFormatFilter = dynamicFormatFilter; + return this; + } + + /** + * Builds player components for buffer size based track adaptation. + * + * @return A pair of a {@link TrackSelection.Factory} and a {@link LoadControl}, which should be + * used to construct the player. + */ + public Pair buildPlayerComponents() { + Assertions.checkArgument(hysteresisBufferMs < maxBufferMs - minBufferMs); + Assertions.checkState(!buildCalled); + buildCalled = true; + + DefaultLoadControl.Builder loadControlBuilder = + new DefaultLoadControl.Builder() + .setTargetBufferBytes(/* targetBufferBytes = */ Integer.MAX_VALUE) + .setBufferDurationsMs( + /* minBufferMs= */ maxBufferMs, + maxBufferMs, + bufferForPlaybackMs, + bufferForPlaybackAfterRebufferMs); + if (allocator != null) { + loadControlBuilder.setAllocator(allocator); + } + + TrackSelection.Factory trackSelectionFactory = + new TrackSelection.Factory() { + @Override + public @NullableType TrackSelection[] createTrackSelections( + @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { + return TrackSelectionUtil.createTrackSelectionsForDefinitions( + definitions, + definition -> + new BufferSizeAdaptiveTrackSelection( + definition.group, + definition.tracks, + bandwidthMeter, + minBufferMs, + maxBufferMs, + hysteresisBufferMs, + startUpBandwidthFraction, + startUpMinBufferForQualityIncreaseMs, + dynamicFormatFilter, + clock)); + } + }; + + return Pair.create(trackSelectionFactory, loadControlBuilder.createDefaultLoadControl()); + } + + private static final class BufferSizeAdaptiveTrackSelection extends BaseTrackSelection { + + private static final int BITRATE_BLACKLISTED = Format.NO_VALUE; + + private final BandwidthMeter bandwidthMeter; + private final Clock clock; + private final DynamicFormatFilter dynamicFormatFilter; + private final int[] formatBitrates; + private final long minBufferUs; + private final long maxBufferUs; + private final long hysteresisBufferUs; + private final float startUpBandwidthFraction; + private final long startUpMinBufferForQualityIncreaseUs; + private final int minBitrate; + private final int maxBitrate; + private final double bitrateToBufferFunctionSlope; + private final double bitrateToBufferFunctionIntercept; + + private boolean isInSteadyState; + private int selectedIndex; + private int selectionReason; + private float playbackSpeed; + + private BufferSizeAdaptiveTrackSelection( + TrackGroup trackGroup, + int[] tracks, + BandwidthMeter bandwidthMeter, + int minBufferMs, + int maxBufferMs, + int hysteresisBufferMs, + float startUpBandwidthFraction, + int startUpMinBufferForQualityIncreaseMs, + DynamicFormatFilter dynamicFormatFilter, + Clock clock) { + super(trackGroup, tracks); + this.bandwidthMeter = bandwidthMeter; + this.minBufferUs = C.msToUs(minBufferMs); + this.maxBufferUs = C.msToUs(maxBufferMs); + this.hysteresisBufferUs = C.msToUs(hysteresisBufferMs); + this.startUpBandwidthFraction = startUpBandwidthFraction; + this.startUpMinBufferForQualityIncreaseUs = C.msToUs(startUpMinBufferForQualityIncreaseMs); + this.dynamicFormatFilter = dynamicFormatFilter; + this.clock = clock; + + formatBitrates = new int[length]; + maxBitrate = getFormat(/* index= */ 0).bitrate; + minBitrate = getFormat(/* index= */ length - 1).bitrate; + selectionReason = C.SELECTION_REASON_UNKNOWN; + playbackSpeed = 1.0f; + + // We use a log-linear function to map from bitrate to buffer size: + // buffer = slope * ln(bitrate) + intercept, + // with buffer(minBitrate) = minBuffer and buffer(maxBitrate) = maxBuffer - hysteresisBuffer. + bitrateToBufferFunctionSlope = + (maxBufferUs - hysteresisBufferUs - minBufferUs) + / Math.log((double) maxBitrate / minBitrate); + bitrateToBufferFunctionIntercept = + minBufferUs - bitrateToBufferFunctionSlope * Math.log(minBitrate); + } + + @Override + public void onPlaybackSpeed(float playbackSpeed) { + this.playbackSpeed = playbackSpeed; + } + + @Override + public void onDiscontinuity() { + isInSteadyState = false; + } + + @Override + public int getSelectedIndex() { + return selectedIndex; + } + + @Override + public int getSelectionReason() { + return selectionReason; + } + + @Override + @Nullable + public Object getSelectionData() { + return null; + } + + @Override + public void updateSelectedTrack( + long playbackPositionUs, + long bufferedDurationUs, + long availableDurationUs, + List queue, + MediaChunkIterator[] mediaChunkIterators) { + updateFormatBitrates(/* nowMs= */ clock.elapsedRealtime()); + + // Make initial selection + if (selectionReason == C.SELECTION_REASON_UNKNOWN) { + selectionReason = C.SELECTION_REASON_INITIAL; + selectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ true); + return; + } + + long bufferUs = getCurrentPeriodBufferedDurationUs(playbackPositionUs, bufferedDurationUs); + int oldSelectedIndex = selectedIndex; + if (isInSteadyState) { + selectIndexSteadyState(bufferUs); + } else { + selectIndexStartUpPhase(bufferUs); + } + if (selectedIndex != oldSelectedIndex) { + selectionReason = C.SELECTION_REASON_ADAPTIVE; + } + } + + // Steady state. + + private void selectIndexSteadyState(long bufferUs) { + if (isOutsideHysteresis(bufferUs)) { + selectedIndex = selectIdealIndexUsingBufferSize(bufferUs); + } + } + + private boolean isOutsideHysteresis(long bufferUs) { + if (formatBitrates[selectedIndex] == BITRATE_BLACKLISTED) { + return true; + } + long targetBufferForCurrentBitrateUs = + getTargetBufferForBitrateUs(formatBitrates[selectedIndex]); + long bufferDiffUs = bufferUs - targetBufferForCurrentBitrateUs; + return Math.abs(bufferDiffUs) > hysteresisBufferUs; + } + + private int selectIdealIndexUsingBufferSize(long bufferUs) { + int lowestBitrateNonBlacklistedIndex = 0; + for (int i = 0; i < formatBitrates.length; i++) { + if (formatBitrates[i] != BITRATE_BLACKLISTED) { + if (getTargetBufferForBitrateUs(formatBitrates[i]) <= bufferUs + && dynamicFormatFilter.isFormatAllowed( + getFormat(i), formatBitrates[i], /* isInitialSelection= */ false)) { + return i; + } + lowestBitrateNonBlacklistedIndex = i; + } + } + return lowestBitrateNonBlacklistedIndex; + } + + // Startup. + + private void selectIndexStartUpPhase(long bufferUs) { + int startUpSelectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ false); + int steadyStateSelectedIndex = selectIdealIndexUsingBufferSize(bufferUs); + if (steadyStateSelectedIndex <= selectedIndex) { + // Switch to steady state if we have enough buffer to maintain current selection. + selectedIndex = steadyStateSelectedIndex; + isInSteadyState = true; + } else { + if (bufferUs < startUpMinBufferForQualityIncreaseUs + && startUpSelectedIndex < selectedIndex + && formatBitrates[selectedIndex] != BITRATE_BLACKLISTED) { + // Switching up from a non-blacklisted track is only allowed if we have enough buffer. + return; + } + selectedIndex = startUpSelectedIndex; + } + } + + private int selectIdealIndexUsingBandwidth(boolean isInitialSelection) { + long effectiveBitrate = + (long) (bandwidthMeter.getBitrateEstimate() * startUpBandwidthFraction); + int lowestBitrateNonBlacklistedIndex = 0; + for (int i = 0; i < formatBitrates.length; i++) { + if (formatBitrates[i] != BITRATE_BLACKLISTED) { + if (Math.round(formatBitrates[i] * playbackSpeed) <= effectiveBitrate + && dynamicFormatFilter.isFormatAllowed( + getFormat(i), formatBitrates[i], isInitialSelection)) { + return i; + } + lowestBitrateNonBlacklistedIndex = i; + } + } + return lowestBitrateNonBlacklistedIndex; + } + + // Utility methods. + + private void updateFormatBitrates(long nowMs) { + for (int i = 0; i < length; i++) { + if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) { + formatBitrates[i] = getFormat(i).bitrate; + } else { + formatBitrates[i] = BITRATE_BLACKLISTED; + } + } + } + + private long getTargetBufferForBitrateUs(int bitrate) { + if (bitrate <= minBitrate) { + return minBufferUs; + } + if (bitrate >= maxBitrate) { + return maxBufferUs - hysteresisBufferUs; + } + return (int) + (bitrateToBufferFunctionSlope * Math.log(bitrate) + bitrateToBufferFunctionIntercept); + } + + private static long getCurrentPeriodBufferedDurationUs( + long playbackPositionUs, long bufferedDurationUs) { + return playbackPositionUs >= 0 ? bufferedDurationUs : playbackPositionUs + bufferedDurationUs; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 98f27132bce0..549e5991b967 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -17,314 +17,1113 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection; import android.content.Context; import android.graphics.Point; +import android.os.Parcel; +import android.os.Parcelable; import android.text.TextUtils; +import android.util.Pair; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Renderer; import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities.Capabilities; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities.FormatSupport; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererConfiguration; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import org.checkerframework.checker.initialization.qual.UnderInitialization; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** - * A {@link MappingTrackSelector} that allows configuration of common parameters. It is safe to call - * the methods of this class from the application thread. See {@link Parameters#Parameters()} for - * default selection parameters. + * A default {@link TrackSelector} suitable for most use cases. Track selections are made according + * to configurable {@link Parameters}, which can be set by calling {@link + * #setParameters(Parameters)}. + * + *

Modifying parameters

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

Selecting specific tracks

+ * + * Track selection overrides can be used to select specific tracks. To specify an override for a + * renderer, it's first necessary to obtain the tracks that have been mapped to it: + * + *
{@code
+ * MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
+ * TrackGroupArray rendererTrackGroups = mappedTrackInfo == null ? null
+ *     : mappedTrackInfo.getTrackGroups(rendererIndex);
+ * }
+ * + * If {@code rendererTrackGroups} is null then there aren't any currently mapped tracks, and so + * setting an override isn't possible. Note that a {@link Player.EventListener} registered on the + * player can be used to determine when the current tracks (and therefore the mapping) changes. If + * {@code rendererTrackGroups} is non-null then an override can be set. The next step is to query + * the properties of the available tracks to determine the {@code groupIndex} and the {@code + * trackIndices} within the group it that should be selected. The override can then be specified + * using {@link ParametersBuilder#setSelectionOverride}: + * + *
{@code
+ * SelectionOverride selectionOverride = new SelectionOverride(groupIndex, trackIndices);
+ * trackSelector.setParameters(
+ *     trackSelector
+ *         .buildUponParameters()
+ *         .setSelectionOverride(rendererIndex, rendererTrackGroups, selectionOverride));
+ * }
+ * + *

Constraint based track selection

+ * + * Whilst track selection overrides make it possible to select specific tracks, the recommended way + * of controlling which tracks are selected is by specifying constraints. For example consider the + * case of wanting to restrict video track selections to SD, and preferring German audio tracks. + * Track selection overrides could be used to select specific tracks meeting these criteria, however + * a simpler and more flexible approach is to specify these constraints directly: + * + *
{@code
+ * trackSelector.setParameters(
+ *     trackSelector
+ *         .buildUponParameters()
+ *         .setMaxVideoSizeSd()
+ *         .setPreferredAudioLanguage("deu"));
+ * }
+ * + * There are several benefits to using constraint based track selection instead of specific track + * overrides: + * + *
    + *
  • You can specify constraints before knowing what tracks the media provides. This can + * simplify track selection code (e.g. you don't have to listen for changes in the available + * tracks before configuring the selector). + *
  • Constraints can be applied consistently across all periods in a complex piece of media, + * even if those periods contain different tracks. In contrast, a specific track override is + * only applied to periods whose tracks match those for which the override was set. + *
+ * + *

Disabling renderers

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

Tunneling

+ * + * Tunneled playback can be enabled in cases where the combination of renderers and selected tracks + * support it. Tunneled playback is enabled by passing an audio session ID to {@link + * ParametersBuilder#setTunnelingAudioSessionId(int)}. */ public class DefaultTrackSelector extends MappingTrackSelector { /** - * Holder for available configurations for the {@link DefaultTrackSelector}. + * A builder for {@link Parameters}. See the {@link Parameters} documentation for explanations of + * the parameters that can be configured using this builder. */ - public static final class Parameters { + public static final class ParametersBuilder extends TrackSelectionParameters.Builder { - // Audio. - public final String preferredAudioLanguage; + // Video + private int maxVideoWidth; + private int maxVideoHeight; + private int maxVideoFrameRate; + private int maxVideoBitrate; + private boolean exceedVideoConstraintsIfNecessary; + private boolean allowVideoMixedMimeTypeAdaptiveness; + private boolean allowVideoNonSeamlessAdaptiveness; + private int viewportWidth; + private int viewportHeight; + private boolean viewportOrientationMayChange; + // Audio + private int maxAudioChannelCount; + private int maxAudioBitrate; + private boolean exceedAudioConstraintsIfNecessary; + private boolean allowAudioMixedMimeTypeAdaptiveness; + private boolean allowAudioMixedSampleRateAdaptiveness; + private boolean allowAudioMixedChannelCountAdaptiveness; + // General + private boolean forceLowestBitrate; + private boolean forceHighestSupportedBitrate; + private boolean exceedRendererCapabilitiesIfNecessary; + private int tunnelingAudioSessionId; - // Text. - public final String preferredTextLanguage; - - // Video. - public final boolean allowMixedMimeAdaptiveness; - public final boolean allowNonSeamlessAdaptiveness; - public final int maxVideoWidth; - public final int maxVideoHeight; - public final int maxVideoBitrate; - public final boolean exceedVideoConstraintsIfNecessary; - public final boolean exceedRendererCapabilitiesIfNecessary; - public final int viewportWidth; - public final int viewportHeight; - public final boolean orientationMayChange; + private final SparseArray> + selectionOverrides; + private final SparseBooleanArray rendererDisabledFlags; /** - * Constructor with default selection parameters: - *
    - *
  • No preferred audio language is set.
  • - *
  • No preferred text language is set.
  • - *
  • Adaptation between different mime types is not allowed.
  • - *
  • Non seamless adaptation is allowed.
  • - *
  • No max limit for video width/height.
  • - *
  • No max video bitrate.
  • - *
  • Video constraints are exceeded if no supported selection can be made otherwise.
  • - *
  • Renderer capabilities are exceeded if no supported selection can be made.
  • - *
  • No viewport width/height constraints are set.
  • - *
+ * @deprecated {@link Context} constraints will not be set using this constructor. Use {@link + * #ParametersBuilder(Context)} instead. */ - public Parameters() { - this(null, null, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, true, - true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); + @Deprecated + @SuppressWarnings({"deprecation"}) + public ParametersBuilder() { + super(); + setInitialValuesWithoutContext(); + selectionOverrides = new SparseArray<>(); + rendererDisabledFlags = new SparseBooleanArray(); } /** - * @param preferredAudioLanguage The preferred language for audio, as well as for forced text - * tracks as defined by RFC 5646. {@code null} to select the default track, or first track - * if there's no default. - * @param preferredTextLanguage The preferred language for text tracks as defined by RFC 5646. - * {@code null} to select the default track, or first track if there's no default. - * @param allowMixedMimeAdaptiveness Whether to allow selections to contain mixed mime types. - * @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed. - * @param maxVideoWidth Maximum allowed video width. - * @param maxVideoHeight Maximum allowed video height. - * @param maxVideoBitrate Maximum allowed video bitrate. - * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no - * selection can be made otherwise. - * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no - * selection can be made otherwise. - * @param viewportWidth Viewport width in pixels. - * @param viewportHeight Viewport height in pixels. - * @param orientationMayChange Whether orientation may change during playback. + * Creates a builder with default initial values. + * + * @param context Any context. */ - public Parameters(String preferredAudioLanguage, String preferredTextLanguage, - boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, - int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, - boolean exceedVideoConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary, - int viewportWidth, int viewportHeight, boolean orientationMayChange) { - this.preferredAudioLanguage = preferredAudioLanguage; - this.preferredTextLanguage = preferredTextLanguage; - this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; - this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness; + + public ParametersBuilder(Context context) { + super(context); + setInitialValuesWithoutContext(); + selectionOverrides = new SparseArray<>(); + rendererDisabledFlags = new SparseBooleanArray(); + setViewportSizeToPhysicalDisplaySize(context, /* viewportOrientationMayChange= */ true); + } + + /** + * @param initialValues The {@link Parameters} from which the initial values of the builder are + * obtained. + */ + private ParametersBuilder(Parameters initialValues) { + super(initialValues); + // Video + maxVideoWidth = initialValues.maxVideoWidth; + maxVideoHeight = initialValues.maxVideoHeight; + maxVideoFrameRate = initialValues.maxVideoFrameRate; + maxVideoBitrate = initialValues.maxVideoBitrate; + exceedVideoConstraintsIfNecessary = initialValues.exceedVideoConstraintsIfNecessary; + allowVideoMixedMimeTypeAdaptiveness = initialValues.allowVideoMixedMimeTypeAdaptiveness; + allowVideoNonSeamlessAdaptiveness = initialValues.allowVideoNonSeamlessAdaptiveness; + viewportWidth = initialValues.viewportWidth; + viewportHeight = initialValues.viewportHeight; + viewportOrientationMayChange = initialValues.viewportOrientationMayChange; + // Audio + maxAudioChannelCount = initialValues.maxAudioChannelCount; + maxAudioBitrate = initialValues.maxAudioBitrate; + exceedAudioConstraintsIfNecessary = initialValues.exceedAudioConstraintsIfNecessary; + allowAudioMixedMimeTypeAdaptiveness = initialValues.allowAudioMixedMimeTypeAdaptiveness; + allowAudioMixedSampleRateAdaptiveness = initialValues.allowAudioMixedSampleRateAdaptiveness; + allowAudioMixedChannelCountAdaptiveness = + initialValues.allowAudioMixedChannelCountAdaptiveness; + // General + forceLowestBitrate = initialValues.forceLowestBitrate; + forceHighestSupportedBitrate = initialValues.forceHighestSupportedBitrate; + exceedRendererCapabilitiesIfNecessary = initialValues.exceedRendererCapabilitiesIfNecessary; + tunnelingAudioSessionId = initialValues.tunnelingAudioSessionId; + // Overrides + selectionOverrides = cloneSelectionOverrides(initialValues.selectionOverrides); + rendererDisabledFlags = initialValues.rendererDisabledFlags.clone(); + } + + // Video + + /** + * Equivalent to {@link #setMaxVideoSize setMaxVideoSize(1279, 719)}. + * + * @return This builder. + */ + public ParametersBuilder setMaxVideoSizeSd() { + return setMaxVideoSize(1279, 719); + } + + /** + * Equivalent to {@link #setMaxVideoSize setMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE)}. + * + * @return This builder. + */ + public ParametersBuilder clearVideoSizeConstraints() { + return setMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + /** + * Sets the maximum allowed video width and height. + * + * @param maxVideoWidth Maximum allowed video width in pixels. + * @param maxVideoHeight Maximum allowed video height in pixels. + * @return This builder. + */ + public ParametersBuilder setMaxVideoSize(int maxVideoWidth, int maxVideoHeight) { this.maxVideoWidth = maxVideoWidth; this.maxVideoHeight = maxVideoHeight; + return this; + } + + /** + * Sets the maximum allowed video frame rate. + * + * @param maxVideoFrameRate Maximum allowed video frame rate in hertz. + * @return This builder. + */ + public ParametersBuilder setMaxVideoFrameRate(int maxVideoFrameRate) { + this.maxVideoFrameRate = maxVideoFrameRate; + return this; + } + + /** + * Sets the maximum allowed video bitrate. + * + * @param maxVideoBitrate Maximum allowed video bitrate in bits per second. + * @return This builder. + */ + public ParametersBuilder setMaxVideoBitrate(int maxVideoBitrate) { this.maxVideoBitrate = maxVideoBitrate; - this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary; - this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary; - this.viewportWidth = viewportWidth; - this.viewportHeight = viewportHeight; - this.orientationMayChange = orientationMayChange; + return this; } /** - * Returns a {@link Parameters} instance with the provided preferred language for audio and - * forced text tracks. - * - * @param preferredAudioLanguage The preferred language as defined by RFC 5646. {@code null} to - * select the default track, or first track if there's no default. - * @return A {@link Parameters} instance with the provided preferred language for audio and - * forced text tracks. - */ - public Parameters withPreferredAudioLanguage(String preferredAudioLanguage) { - preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage); - if (TextUtils.equals(preferredAudioLanguage, this.preferredAudioLanguage)) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); - } - - /** - * Returns a {@link Parameters} instance with the provided preferred language for text tracks. - * - * @param preferredTextLanguage The preferred language as defined by RFC 5646. {@code null} to - * select the default track, or no track if there's no default. - * @return A {@link Parameters} instance with the provided preferred language for text tracks. - */ - public Parameters withPreferredTextLanguage(String preferredTextLanguage) { - preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage); - if (TextUtils.equals(preferredTextLanguage, this.preferredTextLanguage)) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); - } - - /** - * Returns a {@link Parameters} instance with the provided mixed mime adaptiveness allowance. - * - * @param allowMixedMimeAdaptiveness Whether to allow selections to contain mixed mime types. - * @return A {@link Parameters} instance with the provided mixed mime adaptiveness allowance. - */ - public Parameters withAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiveness) { - if (allowMixedMimeAdaptiveness == this.allowMixedMimeAdaptiveness) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); - } - - /** - * Returns a {@link Parameters} instance with the provided seamless adaptiveness allowance. - * - * @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed. - * @return A {@link Parameters} instance with the provided seamless adaptiveness allowance. - */ - public Parameters withAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdaptiveness) { - if (allowNonSeamlessAdaptiveness == this.allowNonSeamlessAdaptiveness) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); - } - - /** - * Returns a {@link Parameters} instance with the provided max video size. - * - * @param maxVideoWidth The max video width. - * @param maxVideoHeight The max video width. - * @return A {@link Parameters} instance with the provided max video size. - */ - public Parameters withMaxVideoSize(int maxVideoWidth, int maxVideoHeight) { - if (maxVideoWidth == this.maxVideoWidth && maxVideoHeight == this.maxVideoHeight) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); - } - - /** - * Returns a {@link Parameters} instance with the provided max video bitrate. - * - * @param maxVideoBitrate The max video bitrate. - * @return A {@link Parameters} instance with the provided max video bitrate. - */ - public Parameters withMaxVideoBitrate(int maxVideoBitrate) { - if (maxVideoBitrate == this.maxVideoBitrate) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); - } - - /** - * Equivalent to {@code withMaxVideoSize(1279, 719)}. - * - * @return A {@link Parameters} instance with maximum standard definition as maximum video size. - */ - public Parameters withMaxVideoSizeSd() { - return withMaxVideoSize(1279, 719); - } - - /** - * Equivalent to {@code withMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE)}. - * - * @return A {@link Parameters} instance without video size constraints. - */ - public Parameters withoutVideoSizeConstraints() { - return withMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE); - } - - /** - * Returns a {@link Parameters} instance with the provided - * {@code exceedVideoConstraintsIfNecessary} value. + * Sets whether to exceed the {@link #setMaxVideoSize(int, int)} and {@link + * #setMaxAudioBitrate(int)} constraints when no selection can be made otherwise. * * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no * selection can be made otherwise. - * @return A {@link Parameters} instance with the provided - * {@code exceedVideoConstraintsIfNecessary} value. + * @return This builder. */ - public Parameters withExceedVideoConstraintsIfNecessary( + public ParametersBuilder setExceedVideoConstraintsIfNecessary( boolean exceedVideoConstraintsIfNecessary) { - if (exceedVideoConstraintsIfNecessary == this.exceedVideoConstraintsIfNecessary) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary; + return this; } /** - * Returns a {@link Parameters} instance with the provided - * {@code exceedRendererCapabilitiesIfNecessary} value. + * Sets whether to allow adaptive video selections containing mixed MIME types. * - * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no - * selection can be made otherwise. - * @return A {@link Parameters} instance with the provided - * {@code exceedRendererCapabilitiesIfNecessary} value. + *

Adaptations between different MIME types may not be completely seamless, in which case + * {@link #setAllowVideoNonSeamlessAdaptiveness(boolean)} also needs to be {@code true} for + * mixed MIME type selections to be made. + * + * @param allowVideoMixedMimeTypeAdaptiveness Whether to allow adaptive video selections + * containing mixed MIME types. + * @return This builder. */ - public Parameters withExceedRendererCapabilitiesIfNecessary( - boolean exceedRendererCapabilitiesIfNecessary) { - if (exceedRendererCapabilitiesIfNecessary == this.exceedRendererCapabilitiesIfNecessary) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + public ParametersBuilder setAllowVideoMixedMimeTypeAdaptiveness( + boolean allowVideoMixedMimeTypeAdaptiveness) { + this.allowVideoMixedMimeTypeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness; + return this; } /** - * Returns a {@link Parameters} instance with the provided viewport size. + * Sets whether to allow adaptive video selections where adaptation may not be completely + * seamless. + * + * @param allowVideoNonSeamlessAdaptiveness Whether to allow adaptive video selections where + * adaptation may not be completely seamless. + * @return This builder. + */ + public ParametersBuilder setAllowVideoNonSeamlessAdaptiveness( + boolean allowVideoNonSeamlessAdaptiveness) { + this.allowVideoNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness; + return this; + } + + /** + * Equivalent to calling {@link #setViewportSize(int, int, boolean)} with the viewport size + * obtained from {@link Util#getCurrentDisplayModeSize(Context)}. + * + * @param context Any context. + * @param viewportOrientationMayChange Whether the viewport orientation may change during + * playback. + * @return This builder. + */ + public ParametersBuilder setViewportSizeToPhysicalDisplaySize( + Context context, boolean viewportOrientationMayChange) { + // Assume the viewport is fullscreen. + Point viewportSize = Util.getCurrentDisplayModeSize(context); + return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange); + } + + /** + * Equivalent to {@link #setViewportSize setViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, + * true)}. + * + * @return This builder. + */ + public ParametersBuilder clearViewportSizeConstraints() { + return setViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true); + } + + /** + * Sets the viewport size to constrain adaptive video selections so that only tracks suitable + * for the viewport are selected. * * @param viewportWidth Viewport width in pixels. * @param viewportHeight Viewport height in pixels. - * @param orientationMayChange Whether orientation may change during playback. - * @return A {@link Parameters} instance with the provided viewport size. + * @param viewportOrientationMayChange Whether the viewport orientation may change during + * playback. + * @return This builder. */ - public Parameters withViewportSize(int viewportWidth, int viewportHeight, - boolean orientationMayChange) { - if (viewportWidth == this.viewportWidth && viewportHeight == this.viewportHeight - && orientationMayChange == this.orientationMayChange) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + public ParametersBuilder setViewportSize( + int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { + this.viewportWidth = viewportWidth; + this.viewportHeight = viewportHeight; + this.viewportOrientationMayChange = viewportOrientationMayChange; + return this; + } + + // Audio + + @Override + public ParametersBuilder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) { + super.setPreferredAudioLanguage(preferredAudioLanguage); + return this; } /** - * Returns a {@link Parameters} instance where the viewport size is obtained from the provided - * {@link Context}. + * Sets the maximum allowed audio channel count. * - * @param context The context to obtain the viewport size from. - * @param orientationMayChange Whether orientation may change during playback. - * @return A {@link Parameters} instance where the viewport size is obtained from the provided - * {@link Context}. + * @param maxAudioChannelCount Maximum allowed audio channel count. + * @return This builder. */ - public Parameters withViewportSizeFromContext(Context context, boolean orientationMayChange) { - // Assume the viewport is fullscreen. - Point viewportSize = Util.getPhysicalDisplaySize(context); - return withViewportSize(viewportSize.x, viewportSize.y, orientationMayChange); + public ParametersBuilder setMaxAudioChannelCount(int maxAudioChannelCount) { + this.maxAudioChannelCount = maxAudioChannelCount; + return this; } /** - * Equivalent to {@code withViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true)}. + * Sets the maximum allowed audio bitrate. * - * @return A {@link Parameters} instance without viewport size constraints. + * @param maxAudioBitrate Maximum allowed audio bitrate in bits per second. + * @return This builder. */ - public Parameters withoutViewportSizeConstraints() { - return withViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true); + public ParametersBuilder setMaxAudioBitrate(int maxAudioBitrate) { + this.maxAudioBitrate = maxAudioBitrate; + return this; + } + + /** + * Sets whether to exceed the {@link #setMaxAudioChannelCount(int)} and {@link + * #setMaxAudioBitrate(int)} constraints when no selection can be made otherwise. + * + * @param exceedAudioConstraintsIfNecessary Whether to exceed audio constraints when no + * selection can be made otherwise. + * @return This builder. + */ + public ParametersBuilder setExceedAudioConstraintsIfNecessary( + boolean exceedAudioConstraintsIfNecessary) { + this.exceedAudioConstraintsIfNecessary = exceedAudioConstraintsIfNecessary; + return this; + } + + /** + * Sets whether to allow adaptive audio selections containing mixed MIME types. + * + *

Adaptations between different MIME types may not be completely seamless. + * + * @param allowAudioMixedMimeTypeAdaptiveness Whether to allow adaptive audio selections + * containing mixed MIME types. + * @return This builder. + */ + public ParametersBuilder setAllowAudioMixedMimeTypeAdaptiveness( + boolean allowAudioMixedMimeTypeAdaptiveness) { + this.allowAudioMixedMimeTypeAdaptiveness = allowAudioMixedMimeTypeAdaptiveness; + return this; + } + + /** + * Sets whether to allow adaptive audio selections containing mixed sample rates. + * + *

Adaptations between different sample rates may not be completely seamless. + * + * @param allowAudioMixedSampleRateAdaptiveness Whether to allow adaptive audio selections + * containing mixed sample rates. + * @return This builder. + */ + public ParametersBuilder setAllowAudioMixedSampleRateAdaptiveness( + boolean allowAudioMixedSampleRateAdaptiveness) { + this.allowAudioMixedSampleRateAdaptiveness = allowAudioMixedSampleRateAdaptiveness; + return this; + } + + /** + * Sets whether to allow adaptive audio selections containing mixed channel counts. + * + *

Adaptations between different channel counts may not be completely seamless. + * + * @param allowAudioMixedChannelCountAdaptiveness Whether to allow adaptive audio selections + * containing mixed channel counts. + * @return This builder. + */ + public ParametersBuilder setAllowAudioMixedChannelCountAdaptiveness( + boolean allowAudioMixedChannelCountAdaptiveness) { + this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness; + return this; + } + + // Text + + @Override + public ParametersBuilder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings( + Context context) { + super.setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(context); + return this; } @Override - public boolean equals(Object obj) { + public ParametersBuilder setPreferredTextLanguage(@Nullable String preferredTextLanguage) { + super.setPreferredTextLanguage(preferredTextLanguage); + return this; + } + + @Override + public ParametersBuilder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags) { + super.setPreferredTextRoleFlags(preferredTextRoleFlags); + return this; + } + + @Override + public ParametersBuilder setSelectUndeterminedTextLanguage( + boolean selectUndeterminedTextLanguage) { + super.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage); + return this; + } + + @Override + public ParametersBuilder setDisabledTextTrackSelectionFlags( + @C.SelectionFlags int disabledTextTrackSelectionFlags) { + super.setDisabledTextTrackSelectionFlags(disabledTextTrackSelectionFlags); + return this; + } + + // General + + /** + * Sets whether to force selection of the single lowest bitrate audio and video tracks that + * comply with all other constraints. + * + * @param forceLowestBitrate Whether to force selection of the single lowest bitrate audio and + * video tracks. + * @return This builder. + */ + public ParametersBuilder setForceLowestBitrate(boolean forceLowestBitrate) { + this.forceLowestBitrate = forceLowestBitrate; + return this; + } + + /** + * Sets whether to force selection of the highest bitrate audio and video tracks that comply + * with all other constraints. + * + * @param forceHighestSupportedBitrate Whether to force selection of the highest bitrate audio + * and video tracks. + * @return This builder. + */ + public ParametersBuilder setForceHighestSupportedBitrate(boolean forceHighestSupportedBitrate) { + this.forceHighestSupportedBitrate = forceHighestSupportedBitrate; + return this; + } + + /** + * @deprecated Use {@link #setAllowVideoMixedMimeTypeAdaptiveness(boolean)} and {@link + * #setAllowAudioMixedMimeTypeAdaptiveness(boolean)}. + */ + @Deprecated + public ParametersBuilder setAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiveness) { + setAllowAudioMixedMimeTypeAdaptiveness(allowMixedMimeAdaptiveness); + setAllowVideoMixedMimeTypeAdaptiveness(allowMixedMimeAdaptiveness); + return this; + } + + /** @deprecated Use {@link #setAllowVideoNonSeamlessAdaptiveness(boolean)} */ + @Deprecated + public ParametersBuilder setAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdaptiveness) { + return setAllowVideoNonSeamlessAdaptiveness(allowNonSeamlessAdaptiveness); + } + + /** + * Sets whether to exceed renderer capabilities when no selection can be made otherwise. + * + *

This parameter applies when all of the tracks available for a renderer exceed the + * renderer's reported capabilities. If the parameter is {@code true} then the lowest quality + * track will still be selected. Playback may succeed if the renderer has under-reported its + * true capabilities. If {@code false} then no track will be selected. + * + * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no + * selection can be made otherwise. + * @return This builder. + */ + public ParametersBuilder setExceedRendererCapabilitiesIfNecessary( + boolean exceedRendererCapabilitiesIfNecessary) { + this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary; + return this; + } + + /** + * Sets the audio session id to use when tunneling. + * + *

Enables or disables tunneling. To enable tunneling, pass an audio session id to use when + * in tunneling mode. Session ids can be generated using {@link + * C#generateAudioSessionIdV21(Context)}. To disable tunneling pass {@link + * C#AUDIO_SESSION_ID_UNSET}. Tunneling will only be activated if it's both enabled and + * supported by the audio and video renderers for the selected tracks. + * + * @param tunnelingAudioSessionId The audio session id to use when tunneling, or {@link + * C#AUDIO_SESSION_ID_UNSET} to disable tunneling. + * @return This builder. + */ + public ParametersBuilder setTunnelingAudioSessionId(int tunnelingAudioSessionId) { + this.tunnelingAudioSessionId = tunnelingAudioSessionId; + return this; + } + + // Overrides + + /** + * Sets whether the renderer at the specified index is disabled. Disabling a renderer prevents + * the selector from selecting any tracks for it. + * + * @param rendererIndex The renderer index. + * @param disabled Whether the renderer is disabled. + * @return This builder. + */ + public final ParametersBuilder setRendererDisabled(int rendererIndex, boolean disabled) { + if (rendererDisabledFlags.get(rendererIndex) == disabled) { + // The disabled flag is unchanged. + return this; + } + // Only true values are placed in the array to make it easier to check for equality. + if (disabled) { + rendererDisabledFlags.put(rendererIndex, true); + } else { + rendererDisabledFlags.delete(rendererIndex); + } + return this; + } + + /** + * Overrides the track selection for the renderer at the specified index. + * + *

When the {@link TrackGroupArray} mapped to the renderer matches the one provided, the + * override is applied. When the {@link TrackGroupArray} does not match, the override has no + * effect. The override replaces any previous override for the specified {@link TrackGroupArray} + * for the specified {@link Renderer}. + * + *

Passing a {@code null} override will cause the renderer to be disabled when the {@link + * TrackGroupArray} mapped to it matches the one provided. When the {@link TrackGroupArray} does + * not match a {@code null} override has no effect. Hence a {@code null} override differs from + * disabling the renderer using {@link #setRendererDisabled(int, boolean)} because the renderer + * is disabled conditionally on the {@link TrackGroupArray} mapped to it, where-as {@link + * #setRendererDisabled(int, boolean)} disables the renderer unconditionally. + * + *

To remove overrides use {@link #clearSelectionOverride(int, TrackGroupArray)}, {@link + * #clearSelectionOverrides(int)} or {@link #clearSelectionOverrides()}. + * + * @param rendererIndex The renderer index. + * @param groups The {@link TrackGroupArray} for which the override should be applied. + * @param override The override. + * @return This builder. + */ + public final ParametersBuilder setSelectionOverride( + int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) { + Map overrides = + selectionOverrides.get(rendererIndex); + if (overrides == null) { + overrides = new HashMap<>(); + selectionOverrides.put(rendererIndex, overrides); + } + if (overrides.containsKey(groups) && Util.areEqual(overrides.get(groups), override)) { + // The override is unchanged. + return this; + } + overrides.put(groups, override); + return this; + } + + /** + * Clears a track selection override for the specified renderer and {@link TrackGroupArray}. + * + * @param rendererIndex The renderer index. + * @param groups The {@link TrackGroupArray} for which the override should be cleared. + * @return This builder. + */ + public final ParametersBuilder clearSelectionOverride( + int rendererIndex, TrackGroupArray groups) { + Map overrides = + selectionOverrides.get(rendererIndex); + if (overrides == null || !overrides.containsKey(groups)) { + // Nothing to clear. + return this; + } + overrides.remove(groups); + if (overrides.isEmpty()) { + selectionOverrides.remove(rendererIndex); + } + return this; + } + + /** + * Clears all track selection overrides for the specified renderer. + * + * @param rendererIndex The renderer index. + * @return This builder. + */ + public final ParametersBuilder clearSelectionOverrides(int rendererIndex) { + Map overrides = + selectionOverrides.get(rendererIndex); + if (overrides == null || overrides.isEmpty()) { + // Nothing to clear. + return this; + } + selectionOverrides.remove(rendererIndex); + return this; + } + + /** + * Clears all track selection overrides for all renderers. + * + * @return This builder. + */ + public final ParametersBuilder clearSelectionOverrides() { + if (selectionOverrides.size() == 0) { + // Nothing to clear. + return this; + } + selectionOverrides.clear(); + return this; + } + + /** + * Builds a {@link Parameters} instance with the selected values. + */ + public Parameters build() { + return new Parameters( + // Video + maxVideoWidth, + maxVideoHeight, + maxVideoFrameRate, + maxVideoBitrate, + exceedVideoConstraintsIfNecessary, + allowVideoMixedMimeTypeAdaptiveness, + allowVideoNonSeamlessAdaptiveness, + viewportWidth, + viewportHeight, + viewportOrientationMayChange, + // Audio + preferredAudioLanguage, + maxAudioChannelCount, + maxAudioBitrate, + exceedAudioConstraintsIfNecessary, + allowAudioMixedMimeTypeAdaptiveness, + allowAudioMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness, + // Text + preferredTextLanguage, + preferredTextRoleFlags, + selectUndeterminedTextLanguage, + disabledTextTrackSelectionFlags, + // General + forceLowestBitrate, + forceHighestSupportedBitrate, + exceedRendererCapabilitiesIfNecessary, + tunnelingAudioSessionId, + selectionOverrides, + rendererDisabledFlags); + } + + private void setInitialValuesWithoutContext(@UnderInitialization ParametersBuilder this) { + // Video + maxVideoWidth = Integer.MAX_VALUE; + maxVideoHeight = Integer.MAX_VALUE; + maxVideoFrameRate = Integer.MAX_VALUE; + maxVideoBitrate = Integer.MAX_VALUE; + exceedVideoConstraintsIfNecessary = true; + allowVideoMixedMimeTypeAdaptiveness = false; + allowVideoNonSeamlessAdaptiveness = true; + viewportWidth = Integer.MAX_VALUE; + viewportHeight = Integer.MAX_VALUE; + viewportOrientationMayChange = true; + // Audio + maxAudioChannelCount = Integer.MAX_VALUE; + maxAudioBitrate = Integer.MAX_VALUE; + exceedAudioConstraintsIfNecessary = true; + allowAudioMixedMimeTypeAdaptiveness = false; + allowAudioMixedSampleRateAdaptiveness = false; + allowAudioMixedChannelCountAdaptiveness = false; + // General + forceLowestBitrate = false; + forceHighestSupportedBitrate = false; + exceedRendererCapabilitiesIfNecessary = true; + tunnelingAudioSessionId = C.AUDIO_SESSION_ID_UNSET; + } + + private static SparseArray> + cloneSelectionOverrides( + SparseArray> selectionOverrides) { + SparseArray> clone = + new SparseArray<>(); + for (int i = 0; i < selectionOverrides.size(); i++) { + clone.put(selectionOverrides.keyAt(i), new HashMap<>(selectionOverrides.valueAt(i))); + } + return clone; + } + } + + /** + * Extends {@link TrackSelectionParameters} by adding fields that are specific to {@link + * DefaultTrackSelector}. + */ + public static final class Parameters extends TrackSelectionParameters { + + /** + * An instance with default values, except those obtained from the {@link Context}. + * + *

If possible, use {@link #getDefaults(Context)} instead. + * + *

This instance will not have the following settings: + * + *

    + *
  • {@link ParametersBuilder#setViewportSizeToPhysicalDisplaySize(Context, boolean) + * Viewport constraints} configured for the primary display. + *
  • {@link + * ParametersBuilder#setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(Context) + * Preferred text language and role flags} configured to the accessibility settings of + * {@link android.view.accessibility.CaptioningManager}. + *
+ */ + @SuppressWarnings("deprecation") + public static final Parameters DEFAULT_WITHOUT_CONTEXT = new ParametersBuilder().build(); + + /** + * @deprecated This instance does not have {@link Context} constraints configured. Use {@link + * #getDefaults(Context)} instead. + */ + @Deprecated public static final Parameters DEFAULT_WITHOUT_VIEWPORT = DEFAULT_WITHOUT_CONTEXT; + + /** + * @deprecated This instance does not have {@link Context} constraints configured. Use {@link + * #getDefaults(Context)} instead. + */ + @Deprecated + public static final Parameters DEFAULT = DEFAULT_WITHOUT_CONTEXT; + + /** Returns an instance configured with default values. */ + public static Parameters getDefaults(Context context) { + return new ParametersBuilder(context).build(); + } + + // Video + /** + * Maximum allowed video width in pixels. The default value is {@link Integer#MAX_VALUE} (i.e. + * no constraint). + * + *

To constrain adaptive video track selections to be suitable for a given viewport (the + * region of the display within which video will be played), use ({@link #viewportWidth}, {@link + * #viewportHeight} and {@link #viewportOrientationMayChange}) instead. + */ + public final int maxVideoWidth; + /** + * Maximum allowed video height in pixels. The default value is {@link Integer#MAX_VALUE} (i.e. + * no constraint). + * + *

To constrain adaptive video track selections to be suitable for a given viewport (the + * region of the display within which video will be played), use ({@link #viewportWidth}, {@link + * #viewportHeight} and {@link #viewportOrientationMayChange}) instead. + */ + public final int maxVideoHeight; + /** + * Maximum allowed video frame rate in hertz. The default value is {@link Integer#MAX_VALUE} + * (i.e. no constraint). + */ + public final int maxVideoFrameRate; + /** + * Maximum allowed video bitrate in bits per second. The default value is {@link + * Integer#MAX_VALUE} (i.e. no constraint). + */ + public final int maxVideoBitrate; + /** + * Whether to exceed the {@link #maxVideoWidth}, {@link #maxVideoHeight} and {@link + * #maxVideoBitrate} constraints when no selection can be made otherwise. The default value is + * {@code true}. + */ + public final boolean exceedVideoConstraintsIfNecessary; + /** + * Whether to allow adaptive video selections containing mixed MIME types. Adaptations between + * different MIME types may not be completely seamless, in which case {@link + * #allowVideoNonSeamlessAdaptiveness} also needs to be {@code true} for mixed MIME type + * selections to be made. The default value is {@code false}. + */ + public final boolean allowVideoMixedMimeTypeAdaptiveness; + /** + * Whether to allow adaptive video selections where adaptation may not be completely seamless. + * The default value is {@code true}. + */ + public final boolean allowVideoNonSeamlessAdaptiveness; + /** + * Viewport width in pixels. Constrains video track selections for adaptive content so that only + * tracks suitable for the viewport are selected. The default value is the physical width of the + * primary display, in pixels. + */ + public final int viewportWidth; + /** + * Viewport height in pixels. Constrains video track selections for adaptive content so that + * only tracks suitable for the viewport are selected. The default value is the physical height + * of the primary display, in pixels. + */ + public final int viewportHeight; + /** + * Whether the viewport orientation may change during playback. Constrains video track + * selections for adaptive content so that only tracks suitable for the viewport are selected. + * The default value is {@code true}. + */ + public final boolean viewportOrientationMayChange; + // Audio + /** + * Maximum allowed audio channel count. The default value is {@link Integer#MAX_VALUE} (i.e. no + * constraint). + */ + public final int maxAudioChannelCount; + /** + * Maximum allowed audio bitrate in bits per second. The default value is {@link + * Integer#MAX_VALUE} (i.e. no constraint). + */ + public final int maxAudioBitrate; + /** + * Whether to exceed the {@link #maxAudioChannelCount} and {@link #maxAudioBitrate} constraints + * when no selection can be made otherwise. The default value is {@code true}. + */ + public final boolean exceedAudioConstraintsIfNecessary; + /** + * Whether to allow adaptive audio selections containing mixed MIME types. Adaptations between + * different MIME types may not be completely seamless. The default value is {@code false}. + */ + public final boolean allowAudioMixedMimeTypeAdaptiveness; + /** + * Whether to allow adaptive audio selections containing mixed sample rates. Adaptations between + * different sample rates may not be completely seamless. The default value is {@code false}. + */ + public final boolean allowAudioMixedSampleRateAdaptiveness; + /** + * Whether to allow adaptive audio selections containing mixed channel counts. Adaptations + * between different channel counts may not be completely seamless. The default value is {@code + * false}. + */ + public final boolean allowAudioMixedChannelCountAdaptiveness; + + // General + /** + * Whether to force selection of the single lowest bitrate audio and video tracks that comply + * with all other constraints. The default value is {@code false}. + */ + public final boolean forceLowestBitrate; + /** + * Whether to force selection of the highest bitrate audio and video tracks that comply with all + * other constraints. The default value is {@code false}. + */ + public final boolean forceHighestSupportedBitrate; + /** + * @deprecated Use {@link #allowVideoMixedMimeTypeAdaptiveness} and {@link + * #allowAudioMixedMimeTypeAdaptiveness}. + */ + @Deprecated public final boolean allowMixedMimeAdaptiveness; + /** @deprecated Use {@link #allowVideoNonSeamlessAdaptiveness}. */ + @Deprecated public final boolean allowNonSeamlessAdaptiveness; + /** + * Whether to exceed renderer capabilities when no selection can be made otherwise. + * + *

This parameter applies when all of the tracks available for a renderer exceed the + * renderer's reported capabilities. If the parameter is {@code true} then the lowest quality + * track will still be selected. Playback may succeed if the renderer has under-reported its + * true capabilities. If {@code false} then no track will be selected. The default value is + * {@code true}. + */ + public final boolean exceedRendererCapabilitiesIfNecessary; + /** + * The audio session id to use when tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling + * is disabled. The default value is {@link C#AUDIO_SESSION_ID_UNSET} (i.e. tunneling is + * disabled). + */ + public final int tunnelingAudioSessionId; + + // Overrides + private final SparseArray> + selectionOverrides; + private final SparseBooleanArray rendererDisabledFlags; + + /* package */ Parameters( + // Video + int maxVideoWidth, + int maxVideoHeight, + int maxVideoFrameRate, + int maxVideoBitrate, + boolean exceedVideoConstraintsIfNecessary, + boolean allowVideoMixedMimeTypeAdaptiveness, + boolean allowVideoNonSeamlessAdaptiveness, + int viewportWidth, + int viewportHeight, + boolean viewportOrientationMayChange, + // Audio + @Nullable String preferredAudioLanguage, + int maxAudioChannelCount, + int maxAudioBitrate, + boolean exceedAudioConstraintsIfNecessary, + boolean allowAudioMixedMimeTypeAdaptiveness, + boolean allowAudioMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness, + // Text + @Nullable String preferredTextLanguage, + @C.RoleFlags int preferredTextRoleFlags, + boolean selectUndeterminedTextLanguage, + @C.SelectionFlags int disabledTextTrackSelectionFlags, + // General + boolean forceLowestBitrate, + boolean forceHighestSupportedBitrate, + boolean exceedRendererCapabilitiesIfNecessary, + int tunnelingAudioSessionId, + // Overrides + SparseArray> selectionOverrides, + SparseBooleanArray rendererDisabledFlags) { + super( + preferredAudioLanguage, + preferredTextLanguage, + preferredTextRoleFlags, + selectUndeterminedTextLanguage, + disabledTextTrackSelectionFlags); + // Video + this.maxVideoWidth = maxVideoWidth; + this.maxVideoHeight = maxVideoHeight; + this.maxVideoFrameRate = maxVideoFrameRate; + this.maxVideoBitrate = maxVideoBitrate; + this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary; + this.allowVideoMixedMimeTypeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness; + this.allowVideoNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness; + this.viewportWidth = viewportWidth; + this.viewportHeight = viewportHeight; + this.viewportOrientationMayChange = viewportOrientationMayChange; + // Audio + this.maxAudioChannelCount = maxAudioChannelCount; + this.maxAudioBitrate = maxAudioBitrate; + this.exceedAudioConstraintsIfNecessary = exceedAudioConstraintsIfNecessary; + this.allowAudioMixedMimeTypeAdaptiveness = allowAudioMixedMimeTypeAdaptiveness; + this.allowAudioMixedSampleRateAdaptiveness = allowAudioMixedSampleRateAdaptiveness; + this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness; + // General + this.forceLowestBitrate = forceLowestBitrate; + this.forceHighestSupportedBitrate = forceHighestSupportedBitrate; + this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary; + this.tunnelingAudioSessionId = tunnelingAudioSessionId; + // Deprecated fields. + this.allowMixedMimeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness; + this.allowNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness; + // Overrides + this.selectionOverrides = selectionOverrides; + this.rendererDisabledFlags = rendererDisabledFlags; + } + + /* package */ + Parameters(Parcel in) { + super(in); + // Video + this.maxVideoWidth = in.readInt(); + this.maxVideoHeight = in.readInt(); + this.maxVideoFrameRate = in.readInt(); + this.maxVideoBitrate = in.readInt(); + this.exceedVideoConstraintsIfNecessary = Util.readBoolean(in); + this.allowVideoMixedMimeTypeAdaptiveness = Util.readBoolean(in); + this.allowVideoNonSeamlessAdaptiveness = Util.readBoolean(in); + this.viewportWidth = in.readInt(); + this.viewportHeight = in.readInt(); + this.viewportOrientationMayChange = Util.readBoolean(in); + // Audio + this.maxAudioChannelCount = in.readInt(); + this.maxAudioBitrate = in.readInt(); + this.exceedAudioConstraintsIfNecessary = Util.readBoolean(in); + this.allowAudioMixedMimeTypeAdaptiveness = Util.readBoolean(in); + this.allowAudioMixedSampleRateAdaptiveness = Util.readBoolean(in); + this.allowAudioMixedChannelCountAdaptiveness = Util.readBoolean(in); + // General + this.forceLowestBitrate = Util.readBoolean(in); + this.forceHighestSupportedBitrate = Util.readBoolean(in); + this.exceedRendererCapabilitiesIfNecessary = Util.readBoolean(in); + this.tunnelingAudioSessionId = in.readInt(); + // Overrides + this.selectionOverrides = readSelectionOverrides(in); + this.rendererDisabledFlags = Util.castNonNull(in.readSparseBooleanArray()); + // Deprecated fields. + this.allowMixedMimeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness; + this.allowNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness; + } + + /** + * Returns whether the renderer is disabled. + * + * @param rendererIndex The renderer index. + * @return Whether the renderer is disabled. + */ + public final boolean getRendererDisabled(int rendererIndex) { + return rendererDisabledFlags.get(rendererIndex); + } + + /** + * Returns whether there is an override for the specified renderer and {@link TrackGroupArray}. + * + * @param rendererIndex The renderer index. + * @param groups The {@link TrackGroupArray}. + * @return Whether there is an override. + */ + public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { + Map overrides = + selectionOverrides.get(rendererIndex); + return overrides != null && overrides.containsKey(groups); + } + + /** + * Returns the override for the specified renderer and {@link TrackGroupArray}. + * + * @param rendererIndex The renderer index. + * @param groups The {@link TrackGroupArray}. + * @return The override, or null if no override exists. + */ + @Nullable + public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { + Map overrides = + selectionOverrides.get(rendererIndex); + return overrides != null ? overrides.get(groups) : null; + } + + /** Creates a new {@link ParametersBuilder}, copying the initial values from this instance. */ + @Override + public ParametersBuilder buildUpon() { + return new ParametersBuilder(this); + } + + @Override + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -332,35 +1131,315 @@ public class DefaultTrackSelector extends MappingTrackSelector { return false; } Parameters other = (Parameters) obj; - return allowMixedMimeAdaptiveness == other.allowMixedMimeAdaptiveness - && allowNonSeamlessAdaptiveness == other.allowNonSeamlessAdaptiveness - && maxVideoWidth == other.maxVideoWidth && maxVideoHeight == other.maxVideoHeight - && exceedVideoConstraintsIfNecessary == other.exceedVideoConstraintsIfNecessary - && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary - && orientationMayChange == other.orientationMayChange - && viewportWidth == other.viewportWidth && viewportHeight == other.viewportHeight + return super.equals(obj) + // Video + && maxVideoWidth == other.maxVideoWidth + && maxVideoHeight == other.maxVideoHeight + && maxVideoFrameRate == other.maxVideoFrameRate && maxVideoBitrate == other.maxVideoBitrate - && TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) - && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage); + && exceedVideoConstraintsIfNecessary == other.exceedVideoConstraintsIfNecessary + && allowVideoMixedMimeTypeAdaptiveness == other.allowVideoMixedMimeTypeAdaptiveness + && allowVideoNonSeamlessAdaptiveness == other.allowVideoNonSeamlessAdaptiveness + && viewportOrientationMayChange == other.viewportOrientationMayChange + && viewportWidth == other.viewportWidth + && viewportHeight == other.viewportHeight + // Audio + && maxAudioChannelCount == other.maxAudioChannelCount + && maxAudioBitrate == other.maxAudioBitrate + && exceedAudioConstraintsIfNecessary == other.exceedAudioConstraintsIfNecessary + && allowAudioMixedMimeTypeAdaptiveness == other.allowAudioMixedMimeTypeAdaptiveness + && allowAudioMixedSampleRateAdaptiveness == other.allowAudioMixedSampleRateAdaptiveness + && allowAudioMixedChannelCountAdaptiveness + == other.allowAudioMixedChannelCountAdaptiveness + // General + && forceLowestBitrate == other.forceLowestBitrate + && forceHighestSupportedBitrate == other.forceHighestSupportedBitrate + && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary + && tunnelingAudioSessionId == other.tunnelingAudioSessionId + // Overrides + && areRendererDisabledFlagsEqual(rendererDisabledFlags, other.rendererDisabledFlags) + && areSelectionOverridesEqual(selectionOverrides, other.selectionOverrides); } @Override public int hashCode() { - int result = preferredAudioLanguage.hashCode(); - result = 31 * result + preferredTextLanguage.hashCode(); - result = 31 * result + (allowMixedMimeAdaptiveness ? 1 : 0); - result = 31 * result + (allowNonSeamlessAdaptiveness ? 1 : 0); + int result = super.hashCode(); + // Video result = 31 * result + maxVideoWidth; result = 31 * result + maxVideoHeight; + result = 31 * result + maxVideoFrameRate; result = 31 * result + maxVideoBitrate; result = 31 * result + (exceedVideoConstraintsIfNecessary ? 1 : 0); - result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0); - result = 31 * result + (orientationMayChange ? 1 : 0); + result = 31 * result + (allowVideoMixedMimeTypeAdaptiveness ? 1 : 0); + result = 31 * result + (allowVideoNonSeamlessAdaptiveness ? 1 : 0); + result = 31 * result + (viewportOrientationMayChange ? 1 : 0); result = 31 * result + viewportWidth; result = 31 * result + viewportHeight; + // Audio + result = 31 * result + maxAudioChannelCount; + result = 31 * result + maxAudioBitrate; + result = 31 * result + (exceedAudioConstraintsIfNecessary ? 1 : 0); + result = 31 * result + (allowAudioMixedMimeTypeAdaptiveness ? 1 : 0); + result = 31 * result + (allowAudioMixedSampleRateAdaptiveness ? 1 : 0); + result = 31 * result + (allowAudioMixedChannelCountAdaptiveness ? 1 : 0); + // General + result = 31 * result + (forceLowestBitrate ? 1 : 0); + result = 31 * result + (forceHighestSupportedBitrate ? 1 : 0); + result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0); + result = 31 * result + tunnelingAudioSessionId; + // Overrides (omitted from hashCode). return result; } + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + // Video + dest.writeInt(maxVideoWidth); + dest.writeInt(maxVideoHeight); + dest.writeInt(maxVideoFrameRate); + dest.writeInt(maxVideoBitrate); + Util.writeBoolean(dest, exceedVideoConstraintsIfNecessary); + Util.writeBoolean(dest, allowVideoMixedMimeTypeAdaptiveness); + Util.writeBoolean(dest, allowVideoNonSeamlessAdaptiveness); + dest.writeInt(viewportWidth); + dest.writeInt(viewportHeight); + Util.writeBoolean(dest, viewportOrientationMayChange); + // Audio + dest.writeInt(maxAudioChannelCount); + dest.writeInt(maxAudioBitrate); + Util.writeBoolean(dest, exceedAudioConstraintsIfNecessary); + Util.writeBoolean(dest, allowAudioMixedMimeTypeAdaptiveness); + Util.writeBoolean(dest, allowAudioMixedSampleRateAdaptiveness); + Util.writeBoolean(dest, allowAudioMixedChannelCountAdaptiveness); + // General + Util.writeBoolean(dest, forceLowestBitrate); + Util.writeBoolean(dest, forceHighestSupportedBitrate); + Util.writeBoolean(dest, exceedRendererCapabilitiesIfNecessary); + dest.writeInt(tunnelingAudioSessionId); + // Overrides + writeSelectionOverridesToParcel(dest, selectionOverrides); + dest.writeSparseBooleanArray(rendererDisabledFlags); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public Parameters createFromParcel(Parcel in) { + return new Parameters(in); + } + + @Override + public Parameters[] newArray(int size) { + return new Parameters[size]; + } + }; + + // Static utility methods. + + private static SparseArray> + readSelectionOverrides(Parcel in) { + int renderersWithOverridesCount = in.readInt(); + SparseArray> selectionOverrides = + new SparseArray<>(renderersWithOverridesCount); + for (int i = 0; i < renderersWithOverridesCount; i++) { + int rendererIndex = in.readInt(); + int overrideCount = in.readInt(); + Map overrides = + new HashMap<>(overrideCount); + for (int j = 0; j < overrideCount; j++) { + TrackGroupArray trackGroups = + Assertions.checkNotNull(in.readParcelable(TrackGroupArray.class.getClassLoader())); + @Nullable + SelectionOverride override = in.readParcelable(SelectionOverride.class.getClassLoader()); + overrides.put(trackGroups, override); + } + selectionOverrides.put(rendererIndex, overrides); + } + return selectionOverrides; + } + + private static void writeSelectionOverridesToParcel( + Parcel dest, + SparseArray> selectionOverrides) { + int renderersWithOverridesCount = selectionOverrides.size(); + dest.writeInt(renderersWithOverridesCount); + for (int i = 0; i < renderersWithOverridesCount; i++) { + int rendererIndex = selectionOverrides.keyAt(i); + Map overrides = + selectionOverrides.valueAt(i); + int overrideCount = overrides.size(); + dest.writeInt(rendererIndex); + dest.writeInt(overrideCount); + for (Map.Entry override : + overrides.entrySet()) { + dest.writeParcelable(override.getKey(), /* parcelableFlags= */ 0); + dest.writeParcelable(override.getValue(), /* parcelableFlags= */ 0); + } + } + } + + private static boolean areRendererDisabledFlagsEqual( + SparseBooleanArray first, SparseBooleanArray second) { + int firstSize = first.size(); + if (second.size() != firstSize) { + return false; + } + // Only true values are put into rendererDisabledFlags, so we don't need to compare values. + for (int indexInFirst = 0; indexInFirst < firstSize; indexInFirst++) { + if (second.indexOfKey(first.keyAt(indexInFirst)) < 0) { + return false; + } + } + return true; + } + + private static boolean areSelectionOverridesEqual( + SparseArray> first, + SparseArray> second) { + int firstSize = first.size(); + if (second.size() != firstSize) { + return false; + } + for (int indexInFirst = 0; indexInFirst < firstSize; indexInFirst++) { + int indexInSecond = second.indexOfKey(first.keyAt(indexInFirst)); + if (indexInSecond < 0 + || !areSelectionOverridesEqual( + first.valueAt(indexInFirst), second.valueAt(indexInSecond))) { + return false; + } + } + return true; + } + + private static boolean areSelectionOverridesEqual( + Map first, + Map second) { + int firstSize = first.size(); + if (second.size() != firstSize) { + return false; + } + for (Map.Entry firstEntry : + first.entrySet()) { + TrackGroupArray key = firstEntry.getKey(); + if (!second.containsKey(key) || !Util.areEqual(firstEntry.getValue(), second.get(key))) { + return false; + } + } + return true; + } + } + + /** A track selection override. */ + public static final class SelectionOverride implements Parcelable { + + public final int groupIndex; + public final int[] tracks; + public final int length; + public final int reason; + public final int data; + + /** + * @param groupIndex The overriding track group index. + * @param tracks The overriding track indices within the track group. + */ + public SelectionOverride(int groupIndex, int... tracks) { + this(groupIndex, tracks, C.SELECTION_REASON_MANUAL, /* data= */ 0); + } + + /** + * @param groupIndex The overriding track group index. + * @param tracks The overriding track indices within the track group. + * @param reason The reason for the override. One of the {@link C} SELECTION_REASON_ constants. + * @param data Optional data associated with this override. + */ + public SelectionOverride(int groupIndex, int[] tracks, int reason, int data) { + this.groupIndex = groupIndex; + this.tracks = Arrays.copyOf(tracks, tracks.length); + this.length = tracks.length; + this.reason = reason; + this.data = data; + Arrays.sort(this.tracks); + } + + /* package */ SelectionOverride(Parcel in) { + groupIndex = in.readInt(); + length = in.readByte(); + tracks = new int[length]; + in.readIntArray(tracks); + reason = in.readInt(); + data = in.readInt(); + } + + /** Returns whether this override contains the specified track index. */ + public boolean containsTrack(int track) { + for (int overrideTrack : tracks) { + if (overrideTrack == track) { + return true; + } + } + return false; + } + + @Override + public int hashCode() { + int hash = 31 * groupIndex + Arrays.hashCode(tracks); + hash = 31 * hash + reason; + return 31 * hash + data; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SelectionOverride other = (SelectionOverride) obj; + return groupIndex == other.groupIndex + && Arrays.equals(tracks, other.tracks) + && reason == other.reason + && data == other.data; + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(groupIndex); + dest.writeInt(tracks.length); + dest.writeIntArray(tracks); + dest.writeInt(reason); + dest.writeInt(data); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public SelectionOverride createFromParcel(Parcel in) { + return new SelectionOverride(in); + } + + @Override + public SelectionOverride[] newArray(int size) { + return new SelectionOverride[size]; + } + }; } /** @@ -372,153 +1451,435 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static final int[] NO_TRACKS = new int[0]; private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000; - private final TrackSelection.Factory adaptiveTrackSelectionFactory; - private final AtomicReference paramsReference; + private final TrackSelection.Factory trackSelectionFactory; + private final AtomicReference parametersReference; - /** - * Constructs an instance that does not support adaptive tracks. - */ + private boolean allowMultipleAdaptiveSelections; + + /** @deprecated Use {@link #DefaultTrackSelector(Context)} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public DefaultTrackSelector() { - this(null); + this(new AdaptiveTrackSelection.Factory()); } /** - * Constructs an instance that uses a factory to create adaptive track selections. - * - * @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null if - * the selector should not support adaptive tracks. + * @deprecated Use {@link #DefaultTrackSelector(Context)} instead. The bandwidth meter should be + * passed directly to the player in {@link + * com.google.android.exoplayer2.SimpleExoPlayer.Builder}. */ - public DefaultTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) { - this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory; - paramsReference = new AtomicReference<>(new Parameters()); + @Deprecated + @SuppressWarnings("deprecation") + public DefaultTrackSelector(BandwidthMeter bandwidthMeter) { + this(new AdaptiveTrackSelection.Factory(bandwidthMeter)); + } + + /** @deprecated Use {@link #DefaultTrackSelector(Context, TrackSelection.Factory)}. */ + @Deprecated + public DefaultTrackSelector(TrackSelection.Factory trackSelectionFactory) { + this(Parameters.DEFAULT_WITHOUT_CONTEXT, trackSelectionFactory); + } + + /** @param context Any {@link Context}. */ + public DefaultTrackSelector(Context context) { + this(context, new AdaptiveTrackSelection.Factory()); + } + + /** + * @param context Any {@link Context}. + * @param trackSelectionFactory A factory for {@link TrackSelection}s. + */ + public DefaultTrackSelector(Context context, TrackSelection.Factory trackSelectionFactory) { + this(Parameters.getDefaults(context), trackSelectionFactory); + } + + /** + * @param parameters Initial {@link Parameters}. + * @param trackSelectionFactory A factory for {@link TrackSelection}s. + */ + public DefaultTrackSelector(Parameters parameters, TrackSelection.Factory trackSelectionFactory) { + this.trackSelectionFactory = trackSelectionFactory; + parametersReference = new AtomicReference<>(parameters); } /** * Atomically sets the provided parameters for track selection. * - * @param params The parameters for track selection. + * @param parameters The parameters for track selection. */ - public void setParameters(Parameters params) { - Assertions.checkNotNull(params); - if (!paramsReference.getAndSet(params).equals(params)) { + public void setParameters(Parameters parameters) { + Assertions.checkNotNull(parameters); + if (!parametersReference.getAndSet(parameters).equals(parameters)) { invalidate(); } } + /** + * Atomically sets the provided parameters for track selection. + * + * @param parametersBuilder A builder from which to obtain the parameters for track selection. + */ + public void setParameters(ParametersBuilder parametersBuilder) { + setParameters(parametersBuilder.build()); + } + /** * Gets the current selection parameters. * * @return The current selection parameters. */ public Parameters getParameters() { - return paramsReference.get(); + return parametersReference.get(); + } + + /** Returns a new {@link ParametersBuilder} initialized with the current selection parameters. */ + public ParametersBuilder buildUponParameters() { + return getParameters().buildUpon(); + } + + /** @deprecated Use {@link ParametersBuilder#setRendererDisabled(int, boolean)}. */ + @Deprecated + public final void setRendererDisabled(int rendererIndex, boolean disabled) { + setParameters(buildUponParameters().setRendererDisabled(rendererIndex, disabled)); + } + + /** @deprecated Use {@link Parameters#getRendererDisabled(int)}. */ + @Deprecated + public final boolean getRendererDisabled(int rendererIndex) { + return getParameters().getRendererDisabled(rendererIndex); + } + + /** + * @deprecated Use {@link ParametersBuilder#setSelectionOverride(int, TrackGroupArray, + * SelectionOverride)}. + */ + @Deprecated + public final void setSelectionOverride( + int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) { + setParameters(buildUponParameters().setSelectionOverride(rendererIndex, groups, override)); + } + + /** @deprecated Use {@link Parameters#hasSelectionOverride(int, TrackGroupArray)}. */ + @Deprecated + public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { + return getParameters().hasSelectionOverride(rendererIndex, groups); + } + + /** @deprecated Use {@link Parameters#getSelectionOverride(int, TrackGroupArray)}. */ + @Deprecated + @Nullable + public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { + return getParameters().getSelectionOverride(rendererIndex, groups); + } + + /** @deprecated Use {@link ParametersBuilder#clearSelectionOverride(int, TrackGroupArray)}. */ + @Deprecated + public final void clearSelectionOverride(int rendererIndex, TrackGroupArray groups) { + setParameters(buildUponParameters().clearSelectionOverride(rendererIndex, groups)); + } + + /** @deprecated Use {@link ParametersBuilder#clearSelectionOverrides(int)}. */ + @Deprecated + public final void clearSelectionOverrides(int rendererIndex) { + setParameters(buildUponParameters().clearSelectionOverrides(rendererIndex)); + } + + /** @deprecated Use {@link ParametersBuilder#clearSelectionOverrides()}. */ + @Deprecated + public final void clearSelectionOverrides() { + setParameters(buildUponParameters().clearSelectionOverrides()); + } + + /** @deprecated Use {@link ParametersBuilder#setTunnelingAudioSessionId(int)}. */ + @Deprecated + public void setTunnelingAudioSessionId(int tunnelingAudioSessionId) { + setParameters(buildUponParameters().setTunnelingAudioSessionId(tunnelingAudioSessionId)); + } + + /** + * Allows the creation of multiple adaptive track selections. + * + *

This method is experimental, and will be renamed or removed in a future release. + */ + public void experimental_allowMultipleAdaptiveSelections() { + this.allowMultipleAdaptiveSelections = true; } // MappingTrackSelector implementation. @Override - protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, - TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) - throws ExoPlaybackException { - // Make a track selection for each renderer. - int rendererCount = rendererCapabilities.length; - TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount]; - Parameters params = paramsReference.get(); - boolean videoTrackAndRendererPresent = false; + protected final Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> + selectTracks( + MappedTrackInfo mappedTrackInfo, + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports) + throws ExoPlaybackException { + Parameters params = parametersReference.get(); + int rendererCount = mappedTrackInfo.getRendererCount(); + TrackSelection.@NullableType Definition[] definitions = + selectAllTracks( + mappedTrackInfo, + rendererFormatSupports, + rendererMixedMimeTypeAdaptationSupports, + params); + // Apply track disabling and overriding. for (int i = 0; i < rendererCount; i++) { - if (C.TRACK_TYPE_VIDEO == rendererCapabilities[i].getTrackType()) { - rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i], - rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth, - params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness, - params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, - params.orientationMayChange, adaptiveTrackSelectionFactory, - params.exceedVideoConstraintsIfNecessary, params.exceedRendererCapabilitiesIfNecessary); - videoTrackAndRendererPresent |= rendererTrackGroupArrays[i].length > 0; + if (params.getRendererDisabled(i)) { + definitions[i] = null; + continue; + } + TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(i); + if (params.hasSelectionOverride(i, rendererTrackGroups)) { + SelectionOverride override = params.getSelectionOverride(i, rendererTrackGroups); + definitions[i] = + override == null + ? null + : new TrackSelection.Definition( + rendererTrackGroups.get(override.groupIndex), + override.tracks, + override.reason, + override.data); } } + @NullableType + TrackSelection[] rendererTrackSelections = + trackSelectionFactory.createTrackSelections(definitions, getBandwidthMeter()); + + // Initialize the renderer configurations to the default configuration for all renderers with + // selections, and null otherwise. + @NullableType RendererConfiguration[] rendererConfigurations = + new RendererConfiguration[rendererCount]; for (int i = 0; i < rendererCount; i++) { - switch (rendererCapabilities[i].getTrackType()) { + boolean forceRendererDisabled = params.getRendererDisabled(i); + boolean rendererEnabled = + !forceRendererDisabled + && (mappedTrackInfo.getRendererType(i) == C.TRACK_TYPE_NONE + || rendererTrackSelections[i] != null); + rendererConfigurations[i] = rendererEnabled ? RendererConfiguration.DEFAULT : null; + } + + // Configure audio and video renderers to use tunneling if appropriate. + maybeConfigureRenderersForTunneling( + mappedTrackInfo, + rendererFormatSupports, + rendererConfigurations, + rendererTrackSelections, + params.tunnelingAudioSessionId); + + return Pair.create(rendererConfigurations, rendererTrackSelections); + } + + // Track selection prior to overrides and disabled flags being applied. + + /** + * Called from {@link #selectTracks(MappedTrackInfo, int[][][], int[])} to make a track selection + * for each renderer, prior to overrides and disabled flags being applied. + * + *

The implementation should not account for overrides and disabled flags. Track selections + * generated by this method will be overridden to account for these properties. + * + * @param mappedTrackInfo Mapped track information. + * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * renderer, track group and track (in that order). + * @param rendererMixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. + * @return The {@link TrackSelection.Definition}s for the renderers. A null entry indicates no + * selection was made. + * @throws ExoPlaybackException If an error occurs while selecting the tracks. + */ + protected TrackSelection.@NullableType Definition[] selectAllTracks( + MappedTrackInfo mappedTrackInfo, + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports, + Parameters params) + throws ExoPlaybackException { + int rendererCount = mappedTrackInfo.getRendererCount(); + TrackSelection.@NullableType Definition[] definitions = + new TrackSelection.Definition[rendererCount]; + + boolean seenVideoRendererWithMappedTracks = false; + boolean selectedVideoTracks = false; + for (int i = 0; i < rendererCount; i++) { + if (C.TRACK_TYPE_VIDEO == mappedTrackInfo.getRendererType(i)) { + if (!selectedVideoTracks) { + definitions[i] = + selectVideoTrack( + mappedTrackInfo.getTrackGroups(i), + rendererFormatSupports[i], + rendererMixedMimeTypeAdaptationSupports[i], + params, + /* enableAdaptiveTrackSelection= */ true); + selectedVideoTracks = definitions[i] != null; + } + seenVideoRendererWithMappedTracks |= mappedTrackInfo.getTrackGroups(i).length > 0; + } + } + + AudioTrackScore selectedAudioTrackScore = null; + String selectedAudioLanguage = null; + int selectedAudioRendererIndex = C.INDEX_UNSET; + for (int i = 0; i < rendererCount; i++) { + if (C.TRACK_TYPE_AUDIO == mappedTrackInfo.getRendererType(i)) { + boolean enableAdaptiveTrackSelection = + allowMultipleAdaptiveSelections || !seenVideoRendererWithMappedTracks; + Pair audioSelection = + selectAudioTrack( + mappedTrackInfo.getTrackGroups(i), + rendererFormatSupports[i], + rendererMixedMimeTypeAdaptationSupports[i], + params, + enableAdaptiveTrackSelection); + if (audioSelection != null + && (selectedAudioTrackScore == null + || audioSelection.second.compareTo(selectedAudioTrackScore) > 0)) { + if (selectedAudioRendererIndex != C.INDEX_UNSET) { + // We've already made a selection for another audio renderer, but it had a lower + // score. Clear the selection for that renderer. + definitions[selectedAudioRendererIndex] = null; + } + TrackSelection.Definition definition = audioSelection.first; + definitions[i] = definition; + // We assume that audio tracks in the same group have matching language. + selectedAudioLanguage = definition.group.getFormat(definition.tracks[0]).language; + selectedAudioTrackScore = audioSelection.second; + selectedAudioRendererIndex = i; + } + } + } + + TextTrackScore selectedTextTrackScore = null; + int selectedTextRendererIndex = C.INDEX_UNSET; + for (int i = 0; i < rendererCount; i++) { + int trackType = mappedTrackInfo.getRendererType(i); + switch (trackType) { case C.TRACK_TYPE_VIDEO: + case C.TRACK_TYPE_AUDIO: // Already done. Do nothing. break; - case C.TRACK_TYPE_AUDIO: - rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i], - rendererFormatSupports[i], params.preferredAudioLanguage, - params.exceedRendererCapabilitiesIfNecessary, params.allowMixedMimeAdaptiveness, - videoTrackAndRendererPresent ? null : adaptiveTrackSelectionFactory); - break; case C.TRACK_TYPE_TEXT: - rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i], - rendererFormatSupports[i], params.preferredTextLanguage, - params.preferredAudioLanguage, params.exceedRendererCapabilitiesIfNecessary); + Pair textSelection = + selectTextTrack( + mappedTrackInfo.getTrackGroups(i), + rendererFormatSupports[i], + params, + selectedAudioLanguage); + if (textSelection != null + && (selectedTextTrackScore == null + || textSelection.second.compareTo(selectedTextTrackScore) > 0)) { + if (selectedTextRendererIndex != C.INDEX_UNSET) { + // We've already made a selection for another text renderer, but it had a lower score. + // Clear the selection for that renderer. + definitions[selectedTextRendererIndex] = null; + } + definitions[i] = textSelection.first; + selectedTextTrackScore = textSelection.second; + selectedTextRendererIndex = i; + } break; default: - rendererTrackSelections[i] = selectOtherTrack(rendererCapabilities[i].getTrackType(), - rendererTrackGroupArrays[i], rendererFormatSupports[i], - params.exceedRendererCapabilitiesIfNecessary); + definitions[i] = + selectOtherTrack( + trackType, mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params); break; } } - return rendererTrackSelections; + + return definitions; } // Video track selection implementation. - protected TrackSelection selectVideoTrack(RendererCapabilities rendererCapabilities, - TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, - int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, - int viewportWidth, int viewportHeight, boolean orientationMayChange, - TrackSelection.Factory adaptiveTrackSelectionFactory, boolean exceedConstraintsIfNecessary, - boolean exceedRendererCapabilitiesIfNecessary) throws ExoPlaybackException { - TrackSelection selection = null; - if (adaptiveTrackSelectionFactory != null) { - selection = selectAdaptiveVideoTrack(rendererCapabilities, groups, formatSupport, - maxVideoWidth, maxVideoHeight, maxVideoBitrate, allowNonSeamlessAdaptiveness, - allowMixedMimeAdaptiveness, viewportWidth, viewportHeight, - orientationMayChange, adaptiveTrackSelectionFactory); + /** + * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a + * {@link TrackSelection} for a video renderer. + * + * @param groups The {@link TrackGroupArray} mapped to the renderer. + * @param formatSupports The {@link Capabilities} for each mapped track, indexed by renderer, + * track group and track (in that order). + * @param mixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. + * @param params The selector's current constraint parameters. + * @param enableAdaptiveTrackSelection Whether adaptive track selection is allowed. + * @return The {@link TrackSelection.Definition} for the renderer, or null if no selection was + * made. + * @throws ExoPlaybackException If an error occurs while selecting the tracks. + */ + @Nullable + protected TrackSelection.Definition selectVideoTrack( + TrackGroupArray groups, + @Capabilities int[][] formatSupports, + @AdaptiveSupport int mixedMimeTypeAdaptationSupports, + Parameters params, + boolean enableAdaptiveTrackSelection) + throws ExoPlaybackException { + TrackSelection.Definition definition = null; + if (!params.forceHighestSupportedBitrate + && !params.forceLowestBitrate + && enableAdaptiveTrackSelection) { + definition = + selectAdaptiveVideoTrack(groups, formatSupports, mixedMimeTypeAdaptationSupports, params); } - if (selection == null) { - selection = selectFixedVideoTrack(groups, formatSupport, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange, - exceedConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary); + if (definition == null) { + definition = selectFixedVideoTrack(groups, formatSupports, params); } - return selection; + return definition; } - private static TrackSelection selectAdaptiveVideoTrack(RendererCapabilities rendererCapabilities, - TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, - int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, - int viewportWidth, int viewportHeight, boolean orientationMayChange, - TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { - int requiredAdaptiveSupport = allowNonSeamlessAdaptiveness - ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS) - : RendererCapabilities.ADAPTIVE_SEAMLESS; - boolean allowMixedMimeTypes = allowMixedMimeAdaptiveness - && (rendererCapabilities.supportsMixedMimeTypeAdaptation() & requiredAdaptiveSupport) != 0; + @Nullable + private static TrackSelection.Definition selectAdaptiveVideoTrack( + TrackGroupArray groups, + @Capabilities int[][] formatSupport, + @AdaptiveSupport int mixedMimeTypeAdaptationSupports, + Parameters params) { + int requiredAdaptiveSupport = + params.allowVideoNonSeamlessAdaptiveness + ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS) + : RendererCapabilities.ADAPTIVE_SEAMLESS; + boolean allowMixedMimeTypes = + params.allowVideoMixedMimeTypeAdaptiveness + && (mixedMimeTypeAdaptationSupports & requiredAdaptiveSupport) != 0; for (int i = 0; i < groups.length; i++) { TrackGroup group = groups.get(i); - int[] adaptiveTracks = getAdaptiveVideoTracksForGroup(group, formatSupport[i], - allowMixedMimeTypes, requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange); + int[] adaptiveTracks = + getAdaptiveVideoTracksForGroup( + group, + formatSupport[i], + allowMixedMimeTypes, + requiredAdaptiveSupport, + params.maxVideoWidth, + params.maxVideoHeight, + params.maxVideoFrameRate, + params.maxVideoBitrate, + params.viewportWidth, + params.viewportHeight, + params.viewportOrientationMayChange); if (adaptiveTracks.length > 0) { - return adaptiveTrackSelectionFactory.createTrackSelection(group, adaptiveTracks); + return new TrackSelection.Definition(group, adaptiveTracks); } } return null; } - private static int[] getAdaptiveVideoTracksForGroup(TrackGroup group, int[] formatSupport, - boolean allowMixedMimeTypes, int requiredAdaptiveSupport, int maxVideoWidth, - int maxVideoHeight, int maxVideoBitrate, int viewportWidth, int viewportHeight, - boolean orientationMayChange) { + private static int[] getAdaptiveVideoTracksForGroup( + TrackGroup group, + @Capabilities int[] formatSupport, + boolean allowMixedMimeTypes, + int requiredAdaptiveSupport, + int maxVideoWidth, + int maxVideoHeight, + int maxVideoFrameRate, + int maxVideoBitrate, + int viewportWidth, + int viewportHeight, + boolean viewportOrientationMayChange) { if (group.length < 2) { return NO_TRACKS; } List selectedTrackIndices = getViewportFilteredTrackIndices(group, viewportWidth, - viewportHeight, orientationMayChange); + viewportHeight, viewportOrientationMayChange); if (selectedTrackIndices.size() < 2) { return NO_TRACKS; } @@ -526,15 +1887,23 @@ public class DefaultTrackSelector extends MappingTrackSelector { String selectedMimeType = null; if (!allowMixedMimeTypes) { // Select the mime type for which we have the most adaptive tracks. - HashSet seenMimeTypes = new HashSet<>(); + HashSet<@NullableType String> seenMimeTypes = new HashSet<>(); int selectedMimeTypeTrackCount = 0; for (int i = 0; i < selectedTrackIndices.size(); i++) { int trackIndex = selectedTrackIndices.get(i); String sampleMimeType = group.getFormat(trackIndex).sampleMimeType; if (seenMimeTypes.add(sampleMimeType)) { - int countForMimeType = getAdaptiveVideoTrackCountForMimeType(group, formatSupport, - requiredAdaptiveSupport, sampleMimeType, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, selectedTrackIndices); + int countForMimeType = + getAdaptiveVideoTrackCountForMimeType( + group, + formatSupport, + requiredAdaptiveSupport, + sampleMimeType, + maxVideoWidth, + maxVideoHeight, + maxVideoFrameRate, + maxVideoBitrate, + selectedTrackIndices); if (countForMimeType > selectedMimeTypeTrackCount) { selectedMimeType = sampleMimeType; selectedMimeTypeTrackCount = countForMimeType; @@ -544,20 +1913,41 @@ public class DefaultTrackSelector extends MappingTrackSelector { } // Filter by the selected mime type. - filterAdaptiveVideoTrackCountForMimeType(group, formatSupport, requiredAdaptiveSupport, - selectedMimeType, maxVideoWidth, maxVideoHeight, maxVideoBitrate, selectedTrackIndices); + filterAdaptiveVideoTrackCountForMimeType( + group, + formatSupport, + requiredAdaptiveSupport, + selectedMimeType, + maxVideoWidth, + maxVideoHeight, + maxVideoFrameRate, + maxVideoBitrate, + selectedTrackIndices); return selectedTrackIndices.size() < 2 ? NO_TRACKS : Util.toArray(selectedTrackIndices); } - private static int getAdaptiveVideoTrackCountForMimeType(TrackGroup group, int[] formatSupport, - int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, int maxVideoHeight, - int maxVideoBitrate, List selectedTrackIndices) { + private static int getAdaptiveVideoTrackCountForMimeType( + TrackGroup group, + @Capabilities int[] formatSupport, + int requiredAdaptiveSupport, + @Nullable String mimeType, + int maxVideoWidth, + int maxVideoHeight, + int maxVideoFrameRate, + int maxVideoBitrate, + List selectedTrackIndices) { int adaptiveTrackCount = 0; for (int i = 0; i < selectedTrackIndices.size(); i++) { int trackIndex = selectedTrackIndices.get(i); - if (isSupportedAdaptiveVideoTrack(group.getFormat(trackIndex), mimeType, - formatSupport[trackIndex], requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, + if (isSupportedAdaptiveVideoTrack( + group.getFormat(trackIndex), + mimeType, + formatSupport[trackIndex], + requiredAdaptiveSupport, + maxVideoWidth, + maxVideoHeight, + maxVideoFrameRate, maxVideoBitrate)) { adaptiveTrackCount++; } @@ -565,33 +1955,53 @@ public class DefaultTrackSelector extends MappingTrackSelector { return adaptiveTrackCount; } - private static void filterAdaptiveVideoTrackCountForMimeType(TrackGroup group, - int[] formatSupport, int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, - int maxVideoHeight, int maxVideoBitrate, List selectedTrackIndices) { + private static void filterAdaptiveVideoTrackCountForMimeType( + TrackGroup group, + @Capabilities int[] formatSupport, + int requiredAdaptiveSupport, + @Nullable String mimeType, + int maxVideoWidth, + int maxVideoHeight, + int maxVideoFrameRate, + int maxVideoBitrate, + List selectedTrackIndices) { for (int i = selectedTrackIndices.size() - 1; i >= 0; i--) { int trackIndex = selectedTrackIndices.get(i); - if (!isSupportedAdaptiveVideoTrack(group.getFormat(trackIndex), mimeType, - formatSupport[trackIndex], requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, + if (!isSupportedAdaptiveVideoTrack( + group.getFormat(trackIndex), + mimeType, + formatSupport[trackIndex], + requiredAdaptiveSupport, + maxVideoWidth, + maxVideoHeight, + maxVideoFrameRate, maxVideoBitrate)) { selectedTrackIndices.remove(i); } } } - private static boolean isSupportedAdaptiveVideoTrack(Format format, String mimeType, - int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight, + private static boolean isSupportedAdaptiveVideoTrack( + Format format, + @Nullable String mimeType, + @Capabilities int formatSupport, + int requiredAdaptiveSupport, + int maxVideoWidth, + int maxVideoHeight, + int maxVideoFrameRate, int maxVideoBitrate) { - return isSupported(formatSupport, false) && ((formatSupport & requiredAdaptiveSupport) != 0) + return isSupported(formatSupport, false) + && ((formatSupport & requiredAdaptiveSupport) != 0) && (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType)) && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth) && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight) + && (format.frameRate == Format.NO_VALUE || format.frameRate <= maxVideoFrameRate) && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate); } - private static TrackSelection selectFixedVideoTrack(TrackGroupArray groups, - int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, - int viewportWidth, int viewportHeight, boolean orientationMayChange, - boolean exceedConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary) { + @Nullable + private static TrackSelection.Definition selectFixedVideoTrack( + TrackGroupArray groups, @Capabilities int[][] formatSupports, Parameters params) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -600,37 +2010,47 @@ public class DefaultTrackSelector extends MappingTrackSelector { for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); List selectedTrackIndices = getViewportFilteredTrackIndices(trackGroup, - viewportWidth, viewportHeight, orientationMayChange); - int[] trackFormatSupport = formatSupport[groupIndex]; + params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange); + @Capabilities int[] trackFormatSupport = formatSupports[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { + if (isSupported(trackFormatSupport[trackIndex], + params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); - boolean isWithinConstraints = selectedTrackIndices.contains(trackIndex) - && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth) - && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight) - && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate); - if (!isWithinConstraints && !exceedConstraintsIfNecessary) { + boolean isWithinConstraints = + selectedTrackIndices.contains(trackIndex) + && (format.width == Format.NO_VALUE || format.width <= params.maxVideoWidth) + && (format.height == Format.NO_VALUE || format.height <= params.maxVideoHeight) + && (format.frameRate == Format.NO_VALUE + || format.frameRate <= params.maxVideoFrameRate) + && (format.bitrate == Format.NO_VALUE + || format.bitrate <= params.maxVideoBitrate); + if (!isWithinConstraints && !params.exceedVideoConstraintsIfNecessary) { // Track should not be selected. continue; } int trackScore = isWithinConstraints ? 2 : 1; - if (isSupported(trackFormatSupport[trackIndex], false)) { + boolean isWithinCapabilities = isSupported(trackFormatSupport[trackIndex], false); + if (isWithinCapabilities) { trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; } boolean selectTrack = trackScore > selectedTrackScore; if (trackScore == selectedTrackScore) { - // Use the pixel count as a tie breaker (or bitrate if pixel counts are tied). If we're - // within constraints prefer a higher pixel count (or bitrate), else prefer a lower - // count (or bitrate). If still tied then prefer the first track (i.e. the one that's - // already selected). - int comparisonResult; - int formatPixelCount = format.getPixelCount(); - if (formatPixelCount != selectedPixelCount) { - comparisonResult = compareFormatValues(format.getPixelCount(), selectedPixelCount); + int bitrateComparison = compareFormatValues(format.bitrate, selectedBitrate); + if (params.forceLowestBitrate && bitrateComparison != 0) { + // Use bitrate as a tie breaker, preferring the lower bitrate. + selectTrack = bitrateComparison < 0; } else { - comparisonResult = compareFormatValues(format.bitrate, selectedBitrate); + // Use the pixel count as a tie breaker (or bitrate if pixel counts are tied). If + // we're within constraints prefer a higher pixel count (or bitrate), else prefer a + // lower count (or bitrate). If still tied then prefer the first track (i.e. the one + // that's already selected). + int formatPixelCount = format.getPixelCount(); + int comparisonResult = formatPixelCount != selectedPixelCount + ? compareFormatValues(formatPixelCount, selectedPixelCount) + : compareFormatValues(format.bitrate, selectedBitrate); + selectTrack = isWithinCapabilities && isWithinConstraints + ? comparisonResult > 0 : comparisonResult < 0; } - selectTrack = isWithinConstraints ? comparisonResult > 0 : comparisonResult < 0; } if (selectTrack) { selectedGroup = trackGroup; @@ -642,41 +2062,54 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } } - return selectedGroup == null ? null - : new FixedTrackSelection(selectedGroup, selectedTrackIndex); - } - - /** - * Compares two format values for order. A known value is considered greater than - * {@link Format#NO_VALUE}. - * - * @param first The first value. - * @param second The second value. - * @return A negative integer if the first value is less than the second. Zero if they are equal. - * A positive integer if the first value is greater than the second. - */ - private static int compareFormatValues(int first, int second) { - return first == Format.NO_VALUE ? (second == Format.NO_VALUE ? 0 : -1) - : (second == Format.NO_VALUE ? 1 : (first - second)); + return selectedGroup == null + ? null + : new TrackSelection.Definition(selectedGroup, selectedTrackIndex); } // Audio track selection implementation. - protected TrackSelection selectAudioTrack(TrackGroupArray groups, int[][] formatSupport, - String preferredAudioLanguage, boolean exceedRendererCapabilitiesIfNecessary, - boolean allowMixedMimeAdaptiveness, TrackSelection.Factory adaptiveTrackSelectionFactory) { - int selectedGroupIndex = C.INDEX_UNSET; + /** + * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a + * {@link TrackSelection} for an audio renderer. + * + * @param groups The {@link TrackGroupArray} mapped to the renderer. + * @param formatSupports The {@link Capabilities} for each mapped track, indexed by renderer, + * track group and track (in that order). + * @param mixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. + * @param params The selector's current constraint parameters. + * @param enableAdaptiveTrackSelection Whether adaptive track selection is allowed. + * @return The {@link TrackSelection.Definition} and corresponding {@link AudioTrackScore}, or + * null if no selection was made. + * @throws ExoPlaybackException If an error occurs while selecting the tracks. + */ + @SuppressWarnings("unused") + @Nullable + protected Pair selectAudioTrack( + TrackGroupArray groups, + @Capabilities int[][] formatSupports, + @AdaptiveSupport int mixedMimeTypeAdaptationSupports, + Parameters params, + boolean enableAdaptiveTrackSelection) + throws ExoPlaybackException { int selectedTrackIndex = C.INDEX_UNSET; - int selectedTrackScore = 0; + int selectedGroupIndex = C.INDEX_UNSET; + AudioTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupport[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupports[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { + if (isSupported(trackFormatSupport[trackIndex], + params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); - int trackScore = getAudioTrackScore(trackFormatSupport[trackIndex], - preferredAudioLanguage, format); - if (trackScore > selectedTrackScore) { + AudioTrackScore trackScore = + new AudioTrackScore(format, params, trackFormatSupport[trackIndex]); + if (!trackScore.isWithinConstraints && !params.exceedAudioConstraintsIfNecessary) { + // Track should not be selected. + continue; + } + if (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0) { selectedGroupIndex = groupIndex; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; @@ -690,51 +2123,57 @@ public class DefaultTrackSelector extends MappingTrackSelector { } TrackGroup selectedGroup = groups.get(selectedGroupIndex); - if (adaptiveTrackSelectionFactory != null) { + + TrackSelection.Definition definition = null; + if (!params.forceHighestSupportedBitrate + && !params.forceLowestBitrate + && enableAdaptiveTrackSelection) { // If the group of the track with the highest score allows it, try to enable adaptation. - int[] adaptiveTracks = getAdaptiveAudioTracks(selectedGroup, - formatSupport[selectedGroupIndex], allowMixedMimeAdaptiveness); + int[] adaptiveTracks = + getAdaptiveAudioTracks( + selectedGroup, + formatSupports[selectedGroupIndex], + params.maxAudioBitrate, + params.allowAudioMixedMimeTypeAdaptiveness, + params.allowAudioMixedSampleRateAdaptiveness, + params.allowAudioMixedChannelCountAdaptiveness); if (adaptiveTracks.length > 0) { - return adaptiveTrackSelectionFactory.createTrackSelection(selectedGroup, - adaptiveTracks); + definition = new TrackSelection.Definition(selectedGroup, adaptiveTracks); } } - return new FixedTrackSelection(selectedGroup, selectedTrackIndex); + if (definition == null) { + // We didn't make an adaptive selection, so make a fixed one instead. + definition = new TrackSelection.Definition(selectedGroup, selectedTrackIndex); + } + + return Pair.create(definition, Assertions.checkNotNull(selectedTrackScore)); } - private static int getAudioTrackScore(int formatSupport, String preferredLanguage, - Format format) { - boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; - int trackScore; - if (formatHasLanguage(format, preferredLanguage)) { - if (isDefault) { - trackScore = 4; - } else { - trackScore = 3; - } - } else if (isDefault) { - trackScore = 2; - } else { - trackScore = 1; - } - if (isSupported(formatSupport, false)) { - trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; - } - return trackScore; - } - - private static int[] getAdaptiveAudioTracks(TrackGroup group, int[] formatSupport, - boolean allowMixedMimeTypes) { + private static int[] getAdaptiveAudioTracks( + TrackGroup group, + @Capabilities int[] formatSupport, + int maxAudioBitrate, + boolean allowMixedMimeTypeAdaptiveness, + boolean allowMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness) { int selectedConfigurationTrackCount = 0; AudioConfigurationTuple selectedConfiguration = null; HashSet seenConfigurationTuples = new HashSet<>(); for (int i = 0; i < group.length; i++) { Format format = group.getFormat(i); - AudioConfigurationTuple configuration = new AudioConfigurationTuple( - format.channelCount, format.sampleRate, - allowMixedMimeTypes ? null : format.sampleMimeType); + AudioConfigurationTuple configuration = + new AudioConfigurationTuple( + format.channelCount, format.sampleRate, format.sampleMimeType); if (seenConfigurationTuples.add(configuration)) { - int configurationCount = getAdaptiveAudioTrackCount(group, formatSupport, configuration); + int configurationCount = + getAdaptiveAudioTrackCount( + group, + formatSupport, + configuration, + maxAudioBitrate, + allowMixedMimeTypeAdaptiveness, + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness); if (configurationCount > selectedConfigurationTrackCount) { selectedConfiguration = configuration; selectedConfigurationTrackCount = configurationCount; @@ -743,11 +2182,19 @@ public class DefaultTrackSelector extends MappingTrackSelector { } if (selectedConfigurationTrackCount > 1) { + Assertions.checkNotNull(selectedConfiguration); int[] adaptiveIndices = new int[selectedConfigurationTrackCount]; int index = 0; for (int i = 0; i < group.length; i++) { - if (isSupportedAdaptiveAudioTrack(group.getFormat(i), formatSupport[i], - selectedConfiguration)) { + Format format = group.getFormat(i); + if (isSupportedAdaptiveAudioTrack( + format, + formatSupport[i], + selectedConfiguration, + maxAudioBitrate, + allowMixedMimeTypeAdaptiveness, + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness)) { adaptiveIndices[index++] = i; } } @@ -756,69 +2203,89 @@ public class DefaultTrackSelector extends MappingTrackSelector { return NO_TRACKS; } - private static int getAdaptiveAudioTrackCount(TrackGroup group, int[] formatSupport, - AudioConfigurationTuple configuration) { + private static int getAdaptiveAudioTrackCount( + TrackGroup group, + @Capabilities int[] formatSupport, + AudioConfigurationTuple configuration, + int maxAudioBitrate, + boolean allowMixedMimeTypeAdaptiveness, + boolean allowMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness) { int count = 0; for (int i = 0; i < group.length; i++) { - if (isSupportedAdaptiveAudioTrack(group.getFormat(i), formatSupport[i], configuration)) { + if (isSupportedAdaptiveAudioTrack( + group.getFormat(i), + formatSupport[i], + configuration, + maxAudioBitrate, + allowMixedMimeTypeAdaptiveness, + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness)) { count++; } } return count; } - private static boolean isSupportedAdaptiveAudioTrack(Format format, int formatSupport, - AudioConfigurationTuple configuration) { - return isSupported(formatSupport, false) && format.channelCount == configuration.channelCount - && format.sampleRate == configuration.sampleRate - && (configuration.mimeType == null - || TextUtils.equals(configuration.mimeType, format.sampleMimeType)); + private static boolean isSupportedAdaptiveAudioTrack( + Format format, + @Capabilities int formatSupport, + AudioConfigurationTuple configuration, + int maxAudioBitrate, + boolean allowMixedMimeTypeAdaptiveness, + boolean allowMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness) { + return isSupported(formatSupport, false) + && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxAudioBitrate) + && (allowAudioMixedChannelCountAdaptiveness + || (format.channelCount != Format.NO_VALUE + && format.channelCount == configuration.channelCount)) + && (allowMixedMimeTypeAdaptiveness + || (format.sampleMimeType != null + && TextUtils.equals(format.sampleMimeType, configuration.mimeType))) + && (allowMixedSampleRateAdaptiveness + || (format.sampleRate != Format.NO_VALUE + && format.sampleRate == configuration.sampleRate)); } // Text track selection implementation. - protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport, - String preferredTextLanguage, String preferredAudioLanguage, - boolean exceedRendererCapabilitiesIfNecessary) { + /** + * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a + * {@link TrackSelection} for a text renderer. + * + * @param groups The {@link TrackGroupArray} mapped to the renderer. + * @param formatSupport The {@link Capabilities} for each mapped track, indexed by renderer, track + * group and track (in that order). + * @param params The selector's current constraint parameters. + * @param selectedAudioLanguage The language of the selected audio track. May be null if the + * selected text track declares no language or no text track was selected. + * @return The {@link TrackSelection.Definition} and corresponding {@link TextTrackScore}, or null + * if no selection was made. + * @throws ExoPlaybackException If an error occurs while selecting the tracks. + */ + @Nullable + protected Pair selectTextTrack( + TrackGroupArray groups, + @Capabilities int[][] formatSupport, + Parameters params, + @Nullable String selectedAudioLanguage) + throws ExoPlaybackException { TrackGroup selectedGroup = null; - int selectedTrackIndex = 0; - int selectedTrackScore = 0; + int selectedTrackIndex = C.INDEX_UNSET; + TextTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupport[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { + if (isSupported(trackFormatSupport[trackIndex], + params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); - boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; - boolean isForced = (format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0; - int trackScore; - if (formatHasLanguage(format, preferredTextLanguage)) { - if (isDefault) { - trackScore = 6; - } else if (!isForced) { - // Prefer non-forced to forced if a preferred text language has been specified. Where - // both are provided the non-forced track will usually contain the forced subtitles as - // a subset. - trackScore = 5; - } else { - trackScore = 4; - } - } else if (isDefault) { - trackScore = 3; - } else if (isForced) { - if (formatHasLanguage(format, preferredAudioLanguage)) { - trackScore = 2; - } else { - trackScore = 1; - } - } else { - // Track should not be selected. - continue; - } - if (isSupported(trackFormatSupport[trackIndex], false)) { - trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; - } - if (trackScore > selectedTrackScore) { + TextTrackScore trackScore = + new TextTrackScore( + format, params, trackFormatSupport[trackIndex], selectedAudioLanguage); + if (trackScore.isWithinConstraints + && (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0)) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; @@ -826,22 +2293,40 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } } - return selectedGroup == null ? null - : new FixedTrackSelection(selectedGroup, selectedTrackIndex); + return selectedGroup == null + ? null + : Pair.create( + new TrackSelection.Definition(selectedGroup, selectedTrackIndex), + Assertions.checkNotNull(selectedTrackScore)); } // General track selection methods. - protected TrackSelection selectOtherTrack(int trackType, TrackGroupArray groups, - int[][] formatSupport, boolean exceedRendererCapabilitiesIfNecessary) { + /** + * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a + * {@link TrackSelection} for a renderer whose type is neither video, audio or text. + * + * @param trackType The type of the renderer. + * @param groups The {@link TrackGroupArray} mapped to the renderer. + * @param formatSupport The {@link Capabilities} for each mapped track, indexed by renderer, track + * group and track (in that order). + * @param params The selector's current constraint parameters. + * @return The {@link TrackSelection} for the renderer, or null if no selection was made. + * @throws ExoPlaybackException If an error occurs while selecting the tracks. + */ + @Nullable + protected TrackSelection.Definition selectOtherTrack( + int trackType, TrackGroupArray groups, @Capabilities int[][] formatSupport, Parameters params) + throws ExoPlaybackException { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupport[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { + if (isSupported(trackFormatSupport[trackIndex], + params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; int trackScore = isDefault ? 2 : 1; @@ -856,21 +2341,184 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } } - return selectedGroup == null ? null - : new FixedTrackSelection(selectedGroup, selectedTrackIndex); + return selectedGroup == null + ? null + : new TrackSelection.Definition(selectedGroup, selectedTrackIndex); } - protected static boolean isSupported(int formatSupport, boolean allowExceedsCapabilities) { - int maskedSupport = formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK; + // Utility methods. + + /** + * Determines whether tunneling should be enabled, replacing {@link RendererConfiguration}s in + * {@code rendererConfigurations} with configurations that enable tunneling on the appropriate + * renderers if so. + * + * @param mappedTrackInfo Mapped track information. + * @param renderererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * renderer, track group and track (in that order). + * @param rendererConfigurations The renderer configurations. Configurations may be replaced with + * ones that enable tunneling as a result of this call. + * @param trackSelections The renderer track selections. + * @param tunnelingAudioSessionId The audio session id to use when tunneling, or {@link + * C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled. + */ + private static void maybeConfigureRenderersForTunneling( + MappedTrackInfo mappedTrackInfo, + @Capabilities int[][][] renderererFormatSupports, + @NullableType RendererConfiguration[] rendererConfigurations, + @NullableType TrackSelection[] trackSelections, + int tunnelingAudioSessionId) { + if (tunnelingAudioSessionId == C.AUDIO_SESSION_ID_UNSET) { + return; + } + // Check whether we can enable tunneling. To enable tunneling we require exactly one audio and + // one video renderer to support tunneling and have a selection. + int tunnelingAudioRendererIndex = -1; + int tunnelingVideoRendererIndex = -1; + boolean enableTunneling = true; + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + int rendererType = mappedTrackInfo.getRendererType(i); + TrackSelection trackSelection = trackSelections[i]; + if ((rendererType == C.TRACK_TYPE_AUDIO || rendererType == C.TRACK_TYPE_VIDEO) + && trackSelection != null) { + if (rendererSupportsTunneling( + renderererFormatSupports[i], mappedTrackInfo.getTrackGroups(i), trackSelection)) { + if (rendererType == C.TRACK_TYPE_AUDIO) { + if (tunnelingAudioRendererIndex != -1) { + enableTunneling = false; + break; + } else { + tunnelingAudioRendererIndex = i; + } + } else { + if (tunnelingVideoRendererIndex != -1) { + enableTunneling = false; + break; + } else { + tunnelingVideoRendererIndex = i; + } + } + } + } + } + enableTunneling &= tunnelingAudioRendererIndex != -1 && tunnelingVideoRendererIndex != -1; + if (enableTunneling) { + RendererConfiguration tunnelingRendererConfiguration = + new RendererConfiguration(tunnelingAudioSessionId); + rendererConfigurations[tunnelingAudioRendererIndex] = tunnelingRendererConfiguration; + rendererConfigurations[tunnelingVideoRendererIndex] = tunnelingRendererConfiguration; + } + } + + /** + * Returns whether a renderer supports tunneling for a {@link TrackSelection}. + * + * @param formatSupports The {@link Capabilities} for each track, indexed by group index and track + * index (in that order). + * @param trackGroups The {@link TrackGroupArray}s for the renderer. + * @param selection The track selection. + * @return Whether the renderer supports tunneling for the {@link TrackSelection}. + */ + private static boolean rendererSupportsTunneling( + @Capabilities int[][] formatSupports, TrackGroupArray trackGroups, TrackSelection selection) { + if (selection == null) { + return false; + } + int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); + for (int i = 0; i < selection.length(); i++) { + @Capabilities + int trackFormatSupport = formatSupports[trackGroupIndex][selection.getIndexInTrackGroup(i)]; + if (RendererCapabilities.getTunnelingSupport(trackFormatSupport) + != RendererCapabilities.TUNNELING_SUPPORTED) { + return false; + } + } + return true; + } + + /** + * Compares two format values for order. A known value is considered greater than {@link + * Format#NO_VALUE}. + * + * @param first The first value. + * @param second The second value. + * @return A negative integer if the first value is less than the second. Zero if they are equal. + * A positive integer if the first value is greater than the second. + */ + private static int compareFormatValues(int first, int second) { + return first == Format.NO_VALUE + ? (second == Format.NO_VALUE ? 0 : -1) + : (second == Format.NO_VALUE ? 1 : (first - second)); + } + + /** + * Returns true if the {@link FormatSupport} in the given {@link Capabilities} is {@link + * RendererCapabilities#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set and the + * format support is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * + * @param formatSupport {@link Capabilities}. + * @param allowExceedsCapabilities Whether to return true if {@link FormatSupport} is {@link + * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * @return True if {@link FormatSupport} is {@link RendererCapabilities#FORMAT_HANDLED}, or if + * {@code allowExceedsCapabilities} is set and the format support is {@link + * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + */ + protected static boolean isSupported( + @Capabilities int formatSupport, boolean allowExceedsCapabilities) { + @FormatSupport int maskedSupport = RendererCapabilities.getFormatSupport(formatSupport); return maskedSupport == RendererCapabilities.FORMAT_HANDLED || (allowExceedsCapabilities && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } - protected static boolean formatHasLanguage(Format format, String language) { - return TextUtils.equals(language, Util.normalizeLanguageCode(format.language)); + /** + * Normalizes the input string to null if it does not define a language, or returns it otherwise. + * + * @param language The string. + * @return The string, optionally normalized to null if it does not define a language. + */ + @Nullable + protected static String normalizeUndeterminedLanguageToNull(@Nullable String language) { + return TextUtils.isEmpty(language) || TextUtils.equals(language, C.LANGUAGE_UNDETERMINED) + ? null + : language; } - // Viewport size util methods. + /** + * Returns a score for how well a language specified in a {@link Format} matches a given language. + * + * @param format The {@link Format}. + * @param language The language, or null. + * @param allowUndeterminedFormatLanguage Whether matches with an empty or undetermined format + * language tag are allowed. + * @return A score of 4 if the languages match fully, a score of 3 if the languages match partly, + * a score of 2 if the languages don't match but belong to the same main language, a score of + * 1 if the format language is undetermined and such a match is allowed, and a score of 0 if + * the languages don't match at all. + */ + protected static int getFormatLanguageScore( + Format format, @Nullable String language, boolean allowUndeterminedFormatLanguage) { + if (!TextUtils.isEmpty(language) && language.equals(format.language)) { + // Full literal match of non-empty languages, including matches of an explicit "und" query. + return 4; + } + language = normalizeUndeterminedLanguageToNull(language); + String formatLanguage = normalizeUndeterminedLanguageToNull(format.language); + if (formatLanguage == null || language == null) { + // At least one of the languages is undetermined. + return allowUndeterminedFormatLanguage && formatLanguage == null ? 1 : 0; + } + if (formatLanguage.startsWith(language) || language.startsWith(formatLanguage)) { + // Partial match where one language is a subset of the other (e.g. "zh-hans" and "zh-hans-hk") + return 3; + } + String formatMainLanguage = Util.splitAtFirst(formatLanguage, "-")[0]; + String queryMainLanguage = Util.splitAtFirst(language, "-")[0]; + if (formatMainLanguage.equals(queryMainLanguage)) { + // Partial match where only the main language tag is the same (e.g. "fr-fr" and "fr-ca") + return 2; + } + return 0; + } private static List getViewportFilteredTrackIndices(TrackGroup group, int viewportWidth, int viewportHeight, boolean orientationMayChange) { @@ -941,20 +2589,136 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } + /** + * Compares two integers in a safe way avoiding potential overflow. + * + * @param first The first value. + * @param second The second value. + * @return A negative integer if the first value is less than the second. Zero if they are equal. + * A positive integer if the first value is greater than the second. + */ + private static int compareInts(int first, int second) { + return first > second ? 1 : (second > first ? -1 : 0); + } + + /** Represents how well an audio track matches the selection {@link Parameters}. */ + protected static final class AudioTrackScore implements Comparable { + + /** + * Whether the provided format is within the parameter constraints. If {@code false}, the format + * should not be selected. + */ + public final boolean isWithinConstraints; + + @Nullable private final String language; + private final Parameters parameters; + private final boolean isWithinRendererCapabilities; + private final int preferredLanguageScore; + private final int localeLanguageMatchIndex; + private final int localeLanguageScore; + private final boolean isDefaultSelectionFlag; + private final int channelCount; + private final int sampleRate; + private final int bitrate; + + public AudioTrackScore(Format format, Parameters parameters, @Capabilities int formatSupport) { + this.parameters = parameters; + this.language = normalizeUndeterminedLanguageToNull(format.language); + isWithinRendererCapabilities = isSupported(formatSupport, false); + preferredLanguageScore = + getFormatLanguageScore( + format, + parameters.preferredAudioLanguage, + /* allowUndeterminedFormatLanguage= */ false); + isDefaultSelectionFlag = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; + channelCount = format.channelCount; + sampleRate = format.sampleRate; + bitrate = format.bitrate; + isWithinConstraints = + (format.bitrate == Format.NO_VALUE || format.bitrate <= parameters.maxAudioBitrate) + && (format.channelCount == Format.NO_VALUE + || format.channelCount <= parameters.maxAudioChannelCount); + String[] localeLanguages = Util.getSystemLanguageCodes(); + int bestMatchIndex = Integer.MAX_VALUE; + int bestMatchScore = 0; + for (int i = 0; i < localeLanguages.length; i++) { + int score = + getFormatLanguageScore( + format, localeLanguages[i], /* allowUndeterminedFormatLanguage= */ false); + if (score > 0) { + bestMatchIndex = i; + bestMatchScore = score; + break; + } + } + localeLanguageMatchIndex = bestMatchIndex; + localeLanguageScore = bestMatchScore; + } + + /** + * Compares this score with another. + * + * @param other The other score to compare to. + * @return A positive integer if this score is better than the other. Zero if they are equal. A + * negative integer if this score is worse than the other. + */ + @Override + public int compareTo(AudioTrackScore other) { + if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { + return this.isWithinRendererCapabilities ? 1 : -1; + } + if (this.preferredLanguageScore != other.preferredLanguageScore) { + return compareInts(this.preferredLanguageScore, other.preferredLanguageScore); + } + if (this.isWithinConstraints != other.isWithinConstraints) { + return this.isWithinConstraints ? 1 : -1; + } + if (parameters.forceLowestBitrate) { + int bitrateComparison = compareFormatValues(bitrate, other.bitrate); + if (bitrateComparison != 0) { + return bitrateComparison > 0 ? -1 : 1; + } + } + if (this.isDefaultSelectionFlag != other.isDefaultSelectionFlag) { + return this.isDefaultSelectionFlag ? 1 : -1; + } + if (this.localeLanguageMatchIndex != other.localeLanguageMatchIndex) { + return -compareInts(this.localeLanguageMatchIndex, other.localeLanguageMatchIndex); + } + if (this.localeLanguageScore != other.localeLanguageScore) { + return compareInts(this.localeLanguageScore, other.localeLanguageScore); + } + // If the formats are within constraints and renderer capabilities then prefer higher values + // of channel count, sample rate and bit rate in that order. Otherwise, prefer lower values. + int resultSign = isWithinConstraints && isWithinRendererCapabilities ? 1 : -1; + if (this.channelCount != other.channelCount) { + return resultSign * compareInts(this.channelCount, other.channelCount); + } + if (this.sampleRate != other.sampleRate) { + return resultSign * compareInts(this.sampleRate, other.sampleRate); + } + if (Util.areEqual(this.language, other.language)) { + // Only compare bit rates of tracks with the same or unknown language. + return resultSign * compareInts(this.bitrate, other.bitrate); + } + return 0; + } + } + private static final class AudioConfigurationTuple { public final int channelCount; public final int sampleRate; - public final String mimeType; + @Nullable public final String mimeType; - public AudioConfigurationTuple(int channelCount, int sampleRate, String mimeType) { + public AudioConfigurationTuple(int channelCount, int sampleRate, @Nullable String mimeType) { this.channelCount = channelCount; this.sampleRate = sampleRate; this.mimeType = mimeType; } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -976,4 +2740,88 @@ public class DefaultTrackSelector extends MappingTrackSelector { } + /** Represents how well a text track matches the selection {@link Parameters}. */ + protected static final class TextTrackScore implements Comparable { + + /** + * Whether the provided format is within the parameter constraints. If {@code false}, the format + * should not be selected. + */ + public final boolean isWithinConstraints; + + private final boolean isWithinRendererCapabilities; + private final boolean isDefault; + private final boolean hasPreferredIsForcedFlag; + private final int preferredLanguageScore; + private final int preferredRoleFlagsScore; + private final int selectedAudioLanguageScore; + private final boolean hasCaptionRoleFlags; + + public TextTrackScore( + Format format, + Parameters parameters, + @Capabilities int trackFormatSupport, + @Nullable String selectedAudioLanguage) { + isWithinRendererCapabilities = + isSupported(trackFormatSupport, /* allowExceedsCapabilities= */ false); + int maskedSelectionFlags = + format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags; + isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; + boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; + preferredLanguageScore = + getFormatLanguageScore( + format, parameters.preferredTextLanguage, parameters.selectUndeterminedTextLanguage); + preferredRoleFlagsScore = + Integer.bitCount(format.roleFlags & parameters.preferredTextRoleFlags); + hasCaptionRoleFlags = + (format.roleFlags & (C.ROLE_FLAG_CAPTION | C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND)) != 0; + // Prefer non-forced to forced if a preferred text language has been matched. Where both are + // provided the non-forced track will usually contain the forced subtitles as a subset. + // Otherwise, prefer a forced track. + hasPreferredIsForcedFlag = + (preferredLanguageScore > 0 && !isForced) || (preferredLanguageScore == 0 && isForced); + boolean selectedAudioLanguageUndetermined = + normalizeUndeterminedLanguageToNull(selectedAudioLanguage) == null; + selectedAudioLanguageScore = + getFormatLanguageScore(format, selectedAudioLanguage, selectedAudioLanguageUndetermined); + isWithinConstraints = + preferredLanguageScore > 0 + || (parameters.preferredTextLanguage == null && preferredRoleFlagsScore > 0) + || isDefault + || (isForced && selectedAudioLanguageScore > 0); + } + + /** + * Compares this score with another. + * + * @param other The other score to compare to. + * @return A positive integer if this score is better than the other. Zero if they are equal. A + * negative integer if this score is worse than the other. + */ + @Override + public int compareTo(TextTrackScore other) { + if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { + return this.isWithinRendererCapabilities ? 1 : -1; + } + if (this.preferredLanguageScore != other.preferredLanguageScore) { + return compareInts(this.preferredLanguageScore, other.preferredLanguageScore); + } + if (this.preferredRoleFlagsScore != other.preferredRoleFlagsScore) { + return compareInts(this.preferredRoleFlagsScore, other.preferredRoleFlagsScore); + } + if (this.isDefault != other.isDefault) { + return this.isDefault ? 1 : -1; + } + if (this.hasPreferredIsForcedFlag != other.hasPreferredIsForcedFlag) { + return this.hasPreferredIsForcedFlag ? 1 : -1; + } + if (this.selectedAudioLanguageScore != other.selectedAudioLanguageScore) { + return compareInts(this.selectedAudioLanguageScore, other.selectedAudioLanguageScore); + } + if (preferredRoleFlagsScore == 0 && this.hasCaptionRoleFlags != other.hasCaptionRoleFlags) { + return this.hasCaptionRoleFlags ? -1 : 1; + } + return 0; + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java index 6d94a2b9ef5a..824abaccfab6 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java @@ -15,9 +15,14 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup; -import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter; +import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A {@link TrackSelection} consisting of a single track. @@ -25,12 +30,16 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; public final class FixedTrackSelection extends BaseTrackSelection { /** - * Factory for {@link FixedTrackSelection} instances. + * @deprecated Don't use as adaptive track selection factory as it will throw when multiple tracks + * are selected. If you would like to disable adaptive selection in {@link + * DefaultTrackSelector}, enable the {@link + * DefaultTrackSelector.Parameters#forceHighestSupportedBitrate} flag instead. */ + @Deprecated public static final class Factory implements TrackSelection.Factory { private final int reason; - private final Object data; + @Nullable private final Object data; public Factory() { this.reason = C.SELECTION_REASON_UNKNOWN; @@ -41,21 +50,23 @@ public final class FixedTrackSelection extends BaseTrackSelection { * @param reason A reason for the track selection. * @param data Optional data associated with the track selection. */ - public Factory(int reason, Object data) { + public Factory(int reason, @Nullable Object data) { this.reason = reason; this.data = data; } @Override - public FixedTrackSelection createTrackSelection(TrackGroup group, int... tracks) { - Assertions.checkArgument(tracks.length == 1); - return new FixedTrackSelection(group, tracks[0], reason, data); + public @NullableType TrackSelection[] createTrackSelections( + @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { + return TrackSelectionUtil.createTrackSelectionsForDefinitions( + definitions, + definition -> + new FixedTrackSelection(definition.group, definition.tracks[0], reason, data)); } - } private final int reason; - private final Object data; + @Nullable private final Object data; /** * @param group The {@link TrackGroup}. Must not be null. @@ -71,14 +82,19 @@ public final class FixedTrackSelection extends BaseTrackSelection { * @param reason A reason for the track selection. * @param data Optional data associated with the track selection. */ - public FixedTrackSelection(TrackGroup group, int track, int reason, Object data) { + public FixedTrackSelection(TrackGroup group, int track, int reason, @Nullable Object data) { super(group, track); this.reason = reason; this.data = data; } @Override - public void updateSelectedTrack(long bufferedDurationUs) { + public void updateSelectedTrack( + long playbackPositionUs, + long bufferedDurationUs, + long availableDurationUs, + List queue, + MediaChunkIterator[] mediaChunkIterators) { // Do nothing. } @@ -93,6 +109,7 @@ public final class FixedTrackSelection extends BaseTrackSelection { } @Override + @Nullable public Object getSelectionData() { return data; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index c1f0ae13ff36..8ba581020b95 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -15,263 +15,362 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection; -import android.content.Context; -import android.util.SparseArray; -import android.util.SparseBooleanArray; +import android.util.Pair; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Renderer; import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities.Capabilities; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererConfiguration; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Base class for {@link TrackSelector}s that first establish a mapping between {@link TrackGroup}s - * and renderers, and then from that mapping create a {@link TrackSelection} for each renderer. + * and {@link Renderer}s, and then from that mapping create a {@link TrackSelection} for each + * renderer. */ public abstract class MappingTrackSelector extends TrackSelector { /** - * A track selection override. + * Provides mapped track information for each renderer. */ - public static final class SelectionOverride { - - public final TrackSelection.Factory factory; - public final int groupIndex; - public final int[] tracks; - public final int length; + public static final class MappedTrackInfo { /** - * @param factory A factory for creating selections from this override. - * @param groupIndex The overriding group index. - * @param tracks The overriding track indices within the group. + * Levels of renderer support. Higher numerical values indicate higher levels of support. One of + * {@link #RENDERER_SUPPORT_NO_TRACKS}, {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS}, {@link + * #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS} or {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}. */ - public SelectionOverride(TrackSelection.Factory factory, int groupIndex, int... tracks) { - this.factory = factory; - this.groupIndex = groupIndex; - this.tracks = tracks; - this.length = tracks.length; + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + RENDERER_SUPPORT_NO_TRACKS, + RENDERER_SUPPORT_UNSUPPORTED_TRACKS, + RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS, + RENDERER_SUPPORT_PLAYABLE_TRACKS + }) + @interface RendererSupport {} + /** The renderer does not have any associated tracks. */ + public static final int RENDERER_SUPPORT_NO_TRACKS = 0; + /** + * The renderer has tracks mapped to it, but all are unsupported. In other words, {@link + * #getTrackSupport(int, int, int)} returns {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} or {@link + * RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all tracks mapped to the renderer. + */ + public static final int RENDERER_SUPPORT_UNSUPPORTED_TRACKS = 1; + /** + * The renderer has tracks mapped to it and at least one is of a supported type, but all such + * tracks exceed the renderer's capabilities. In other words, {@link #getTrackSupport(int, int, + * int)} returns {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} for at least one + * track mapped to the renderer, but does not return {@link + * RendererCapabilities#FORMAT_HANDLED} for any tracks mapped to the renderer. + */ + public static final int RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS = 2; + /** + * The renderer has tracks mapped to it, and at least one such track is playable. In other + * words, {@link #getTrackSupport(int, int, int)} returns {@link + * RendererCapabilities#FORMAT_HANDLED} for at least one track mapped to the renderer. + */ + public static final int RENDERER_SUPPORT_PLAYABLE_TRACKS = 3; + + /** @deprecated Use {@link #getRendererCount()}. */ + @Deprecated public final int length; + + private final int rendererCount; + private final int[] rendererTrackTypes; + private final TrackGroupArray[] rendererTrackGroups; + @AdaptiveSupport private final int[] rendererMixedMimeTypeAdaptiveSupports; + @Capabilities private final int[][][] rendererFormatSupports; + private final TrackGroupArray unmappedTrackGroups; + + /** + * @param rendererTrackTypes The track type handled by each renderer. + * @param rendererTrackGroups The {@link TrackGroup}s mapped to each renderer. + * @param rendererMixedMimeTypeAdaptiveSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. + * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * renderer, track group and track (in that order). + * @param unmappedTrackGroups {@link TrackGroup}s not mapped to any renderer. + */ + @SuppressWarnings("deprecation") + /* package */ MappedTrackInfo( + int[] rendererTrackTypes, + TrackGroupArray[] rendererTrackGroups, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptiveSupports, + @Capabilities int[][][] rendererFormatSupports, + TrackGroupArray unmappedTrackGroups) { + this.rendererTrackTypes = rendererTrackTypes; + this.rendererTrackGroups = rendererTrackGroups; + this.rendererFormatSupports = rendererFormatSupports; + this.rendererMixedMimeTypeAdaptiveSupports = rendererMixedMimeTypeAdaptiveSupports; + this.unmappedTrackGroups = unmappedTrackGroups; + this.rendererCount = rendererTrackTypes.length; + this.length = rendererCount; + } + + /** Returns the number of renderers. */ + public int getRendererCount() { + return rendererCount; } /** - * Creates an selection from this override. + * Returns the track type that the renderer at a given index handles. * - * @param groups The groups whose selection is being overridden. - * @return The selection. + * @see Renderer#getTrackType() + * @param rendererIndex The renderer index. + * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}. */ - public TrackSelection createTrackSelection(TrackGroupArray groups) { - return factory.createTrackSelection(groups.get(groupIndex), tracks); + public int getRendererType(int rendererIndex) { + return rendererTrackTypes[rendererIndex]; } /** - * Returns whether this override contains the specified track index. + * Returns the {@link TrackGroup}s mapped to the renderer at the specified index. + * + * @param rendererIndex The renderer index. + * @return The corresponding {@link TrackGroup}s. */ - public boolean containsTrack(int track) { - for (int overrideTrack : tracks) { - if (overrideTrack == track) { - return true; + public TrackGroupArray getTrackGroups(int rendererIndex) { + return rendererTrackGroups[rendererIndex]; + } + + /** + * Returns the extent to which a renderer can play the tracks that are mapped to it. + * + * @param rendererIndex The renderer index. + * @return The {@link RendererSupport}. + */ + @RendererSupport + public int getRendererSupport(int rendererIndex) { + @RendererSupport int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; + @Capabilities int[][] rendererFormatSupport = rendererFormatSupports[rendererIndex]; + for (@Capabilities int[] trackGroupFormatSupport : rendererFormatSupport) { + for (@Capabilities int trackFormatSupport : trackGroupFormatSupport) { + int trackRendererSupport; + switch (RendererCapabilities.getFormatSupport(trackFormatSupport)) { + case RendererCapabilities.FORMAT_HANDLED: + return RENDERER_SUPPORT_PLAYABLE_TRACKS; + case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: + trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS; + break; + case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: + case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: + case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: + trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS; + break; + default: + throw new IllegalStateException(); + } + bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport); } } - return false; + return bestRendererSupport; + } + + /** @deprecated Use {@link #getTypeSupport(int)}. */ + @Deprecated + @RendererSupport + public int getTrackTypeRendererSupport(int trackType) { + return getTypeSupport(trackType); + } + + /** + * Returns the extent to which tracks of a specified type are supported. This is the best level + * of support obtained from {@link #getRendererSupport(int)} for all renderers that handle the + * specified type. If no such renderers exist then {@link #RENDERER_SUPPORT_NO_TRACKS} is + * returned. + * + * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants. + * @return The {@link RendererSupport}. + */ + @RendererSupport + public int getTypeSupport(int trackType) { + @RendererSupport int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; + for (int i = 0; i < rendererCount; i++) { + if (rendererTrackTypes[i] == trackType) { + bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i)); + } + } + return bestRendererSupport; + } + + /** @deprecated Use {@link #getTrackSupport(int, int, int)}. */ + @Deprecated + @FormatSupport + public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) { + return getTrackSupport(rendererIndex, groupIndex, trackIndex); + } + + /** + * Returns the extent to which an individual track is supported by the renderer. + * + * @param rendererIndex The renderer index. + * @param groupIndex The index of the track group to which the track belongs. + * @param trackIndex The index of the track within the track group. + * @return The {@link FormatSupport}. + */ + @FormatSupport + public int getTrackSupport(int rendererIndex, int groupIndex, int trackIndex) { + return RendererCapabilities.getFormatSupport( + rendererFormatSupports[rendererIndex][groupIndex][trackIndex]); + } + + /** + * Returns the extent to which a renderer supports adaptation between supported tracks in a + * specified {@link TrackGroup}. + * + *

Tracks for which {@link #getTrackSupport(int, int, int)} returns {@link + * RendererCapabilities#FORMAT_HANDLED} are always considered. Tracks for which {@link + * #getTrackSupport(int, int, int)} returns {@link + * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} are also considered if {@code + * includeCapabilitiesExceededTracks} is set to {@code true}. Tracks for which {@link + * #getTrackSupport(int, int, int)} returns {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or {@link + * RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered. + * + * @param rendererIndex The renderer index. + * @param groupIndex The index of the track group. + * @param includeCapabilitiesExceededTracks Whether tracks that exceed the capabilities of the + * renderer are included when determining support. + * @return The {@link AdaptiveSupport}. + */ + @AdaptiveSupport + public int getAdaptiveSupport( + int rendererIndex, int groupIndex, boolean includeCapabilitiesExceededTracks) { + int trackCount = rendererTrackGroups[rendererIndex].get(groupIndex).length; + // Iterate over the tracks in the group, recording the indices of those to consider. + int[] trackIndices = new int[trackCount]; + int trackIndexCount = 0; + for (int i = 0; i < trackCount; i++) { + @FormatSupport int fixedSupport = getTrackSupport(rendererIndex, groupIndex, i); + if (fixedSupport == RendererCapabilities.FORMAT_HANDLED + || (includeCapabilitiesExceededTracks + && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) { + trackIndices[trackIndexCount++] = i; + } + } + trackIndices = Arrays.copyOf(trackIndices, trackIndexCount); + return getAdaptiveSupport(rendererIndex, groupIndex, trackIndices); + } + + /** + * Returns the extent to which a renderer supports adaptation between specified tracks within a + * {@link TrackGroup}. + * + * @param rendererIndex The renderer index. + * @param groupIndex The index of the track group. + * @return The {@link AdaptiveSupport}. + */ + @AdaptiveSupport + public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) { + int handledTrackCount = 0; + @AdaptiveSupport int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS; + boolean multipleMimeTypes = false; + String firstSampleMimeType = null; + for (int i = 0; i < trackIndices.length; i++) { + int trackIndex = trackIndices[i]; + String sampleMimeType = + rendererTrackGroups[rendererIndex].get(groupIndex).getFormat(trackIndex).sampleMimeType; + if (handledTrackCount++ == 0) { + firstSampleMimeType = sampleMimeType; + } else { + multipleMimeTypes |= !Util.areEqual(firstSampleMimeType, sampleMimeType); + } + adaptiveSupport = + Math.min( + adaptiveSupport, + RendererCapabilities.getAdaptiveSupport( + rendererFormatSupports[rendererIndex][groupIndex][i])); + } + return multipleMimeTypes + ? Math.min(adaptiveSupport, rendererMixedMimeTypeAdaptiveSupports[rendererIndex]) + : adaptiveSupport; + } + + /** @deprecated Use {@link #getUnmappedTrackGroups()}. */ + @Deprecated + public TrackGroupArray getUnassociatedTrackGroups() { + return getUnmappedTrackGroups(); + } + + /** Returns {@link TrackGroup}s not mapped to any renderer. */ + public TrackGroupArray getUnmappedTrackGroups() { + return unmappedTrackGroups; } } - private final SparseArray> selectionOverrides; - private final SparseBooleanArray rendererDisabledFlags; - private int tunnelingAudioSessionId; - - private MappedTrackInfo currentMappedTrackInfo; - - public MappingTrackSelector() { - selectionOverrides = new SparseArray<>(); - rendererDisabledFlags = new SparseBooleanArray(); - tunnelingAudioSessionId = C.AUDIO_SESSION_ID_UNSET; - } + @Nullable private MappedTrackInfo currentMappedTrackInfo; /** - * Returns the mapping information associated with the current track selections, or null if no + * Returns the mapping information for the currently active track selection, or null if no * selection is currently active. */ - public final MappedTrackInfo getCurrentMappedTrackInfo() { + public final @Nullable MappedTrackInfo getCurrentMappedTrackInfo() { return currentMappedTrackInfo; } - /** - * Sets whether the renderer at the specified index is disabled. - * - * @param rendererIndex The renderer index. - * @param disabled Whether the renderer is disabled. - */ - public final void setRendererDisabled(int rendererIndex, boolean disabled) { - if (rendererDisabledFlags.get(rendererIndex) == disabled) { - // The disabled flag is unchanged. - return; - } - rendererDisabledFlags.put(rendererIndex, disabled); - invalidate(); - } - - /** - * Returns whether the renderer is disabled. - * - * @param rendererIndex The renderer index. - * @return Whether the renderer is disabled. - */ - public final boolean getRendererDisabled(int rendererIndex) { - return rendererDisabledFlags.get(rendererIndex); - } - - /** - * Overrides the track selection for the renderer at a specified index. - *

- * When the {@link TrackGroupArray} available to the renderer at the specified index matches the - * one provided, the override is applied. When the {@link TrackGroupArray} does not match, the - * override has no effect. The override replaces any previous override for the renderer and the - * provided {@link TrackGroupArray}. - *

- * Passing a {@code null} override will explicitly disable the renderer. To remove overrides use - * {@link #clearSelectionOverride(int, TrackGroupArray)}, {@link #clearSelectionOverrides(int)} - * or {@link #clearSelectionOverrides()}. - * - * @param rendererIndex The renderer index. - * @param groups The {@link TrackGroupArray} for which the override should be applied. - * @param override The override. - */ - public final void setSelectionOverride(int rendererIndex, TrackGroupArray groups, - SelectionOverride override) { - Map overrides = selectionOverrides.get(rendererIndex); - if (overrides == null) { - overrides = new HashMap<>(); - selectionOverrides.put(rendererIndex, overrides); - } - if (overrides.containsKey(groups) && Util.areEqual(overrides.get(groups), override)) { - // The override is unchanged. - return; - } - overrides.put(groups, override); - invalidate(); - } - - /** - * Returns whether there is an override for the specified renderer and {@link TrackGroupArray}. - * - * @param rendererIndex The renderer index. - * @param groups The {@link TrackGroupArray}. - * @return Whether there is an override. - */ - public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { - Map overrides = selectionOverrides.get(rendererIndex); - return overrides != null && overrides.containsKey(groups); - } - - /** - * Returns the override for the specified renderer and {@link TrackGroupArray}. - * - * @param rendererIndex The renderer index. - * @param groups The {@link TrackGroupArray}. - * @return The override, or null if no override exists. - */ - public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { - Map overrides = selectionOverrides.get(rendererIndex); - return overrides != null ? overrides.get(groups) : null; - } - - /** - * Clears a track selection override for the specified renderer and {@link TrackGroupArray}. - * - * @param rendererIndex The renderer index. - * @param groups The {@link TrackGroupArray} for which the override should be cleared. - */ - public final void clearSelectionOverride(int rendererIndex, TrackGroupArray groups) { - Map overrides = selectionOverrides.get(rendererIndex); - if (overrides == null || !overrides.containsKey(groups)) { - // Nothing to clear. - return; - } - overrides.remove(groups); - if (overrides.isEmpty()) { - selectionOverrides.remove(rendererIndex); - } - invalidate(); - } - - /** - * Clears all track selection override for the specified renderer. - * - * @param rendererIndex The renderer index. - */ - public final void clearSelectionOverrides(int rendererIndex) { - Map overrides = selectionOverrides.get(rendererIndex); - if (overrides == null || overrides.isEmpty()) { - // Nothing to clear. - return; - } - selectionOverrides.remove(rendererIndex); - invalidate(); - } - - /** - * Clears all track selection overrides. - */ - public final void clearSelectionOverrides() { - if (selectionOverrides.size() == 0) { - // Nothing to clear. - return; - } - selectionOverrides.clear(); - invalidate(); - } - - /** - * Enables or disables tunneling. To enable tunneling, pass an audio session id to use when in - * tunneling mode. Session ids can be generated using - * {@link C#generateAudioSessionIdV21(Context)}. To disable tunneling pass - * {@link C#AUDIO_SESSION_ID_UNSET}. Tunneling will only be activated if it's both enabled and - * supported by the audio and video renderers for the selected tracks. - * - * @param tunnelingAudioSessionId The audio session id to use when tunneling, or - * {@link C#AUDIO_SESSION_ID_UNSET} to disable tunneling. - */ - public void setTunnelingAudioSessionId(int tunnelingAudioSessionId) { - if (this.tunnelingAudioSessionId != tunnelingAudioSessionId) { - this.tunnelingAudioSessionId = tunnelingAudioSessionId; - invalidate(); - } - } - // TrackSelector implementation. @Override - public final TrackSelectorResult selectTracks(RendererCapabilities[] rendererCapabilities, - TrackGroupArray trackGroups) throws ExoPlaybackException { + public final void onSelectionActivated(Object info) { + currentMappedTrackInfo = (MappedTrackInfo) info; + } + + @Override + public final TrackSelectorResult selectTracks( + RendererCapabilities[] rendererCapabilities, + TrackGroupArray trackGroups, + MediaPeriodId periodId, + Timeline timeline) + throws ExoPlaybackException { // Structures into which data will be written during the selection. The extra item at the end // of each array is to store data associated with track groups that cannot be associated with // any renderer. int[] rendererTrackGroupCounts = new int[rendererCapabilities.length + 1]; TrackGroup[][] rendererTrackGroups = new TrackGroup[rendererCapabilities.length + 1][]; - int[][][] rendererFormatSupports = new int[rendererCapabilities.length + 1][][]; + @Capabilities int[][][] rendererFormatSupports = new int[rendererCapabilities.length + 1][][]; for (int i = 0; i < rendererTrackGroups.length; i++) { rendererTrackGroups[i] = new TrackGroup[trackGroups.length]; rendererFormatSupports[i] = new int[trackGroups.length][]; } // Determine the extent to which each renderer supports mixed mimeType adaptation. - int[] mixedMimeTypeAdaptationSupport = getMixedMimeTypeAdaptationSupport(rendererCapabilities); + @AdaptiveSupport + int[] rendererMixedMimeTypeAdaptationSupports = + getMixedMimeTypeAdaptationSupports(rendererCapabilities); // Associate each track group to a preferred renderer, and evaluate the support that the // renderer provides for each track in the group. for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { TrackGroup group = trackGroups.get(groupIndex); // Associate the group to a preferred renderer. - int rendererIndex = findRenderer(rendererCapabilities, group); + boolean preferUnassociatedRenderer = + MimeTypes.getTrackType(group.getFormat(0).sampleMimeType) == C.TRACK_TYPE_METADATA; + int rendererIndex = + findRenderer( + rendererCapabilities, group, rendererTrackGroupCounts, preferUnassociatedRenderer); // Evaluate the support that the renderer provides for each track in the group. - int[] rendererFormatSupport = rendererIndex == rendererCapabilities.length - ? new int[group.length] : getFormatSupport(rendererCapabilities[rendererIndex], group); + @Capabilities + int[] rendererFormatSupport = + rendererIndex == rendererCapabilities.length + ? new int[group.length] + : getFormatSupport(rendererCapabilities[rendererIndex], group); // Stash the results. int rendererTrackGroupCount = rendererTrackGroupCounts[rendererIndex]; rendererTrackGroups[rendererIndex][rendererTrackGroupCount] = group; @@ -284,130 +383,136 @@ public abstract class MappingTrackSelector extends TrackSelector { int[] rendererTrackTypes = new int[rendererCapabilities.length]; for (int i = 0; i < rendererCapabilities.length; i++) { int rendererTrackGroupCount = rendererTrackGroupCounts[i]; - rendererTrackGroupArrays[i] = new TrackGroupArray( - Arrays.copyOf(rendererTrackGroups[i], rendererTrackGroupCount)); - rendererFormatSupports[i] = Arrays.copyOf(rendererFormatSupports[i], rendererTrackGroupCount); + rendererTrackGroupArrays[i] = + new TrackGroupArray( + Util.nullSafeArrayCopy(rendererTrackGroups[i], rendererTrackGroupCount)); + rendererFormatSupports[i] = + Util.nullSafeArrayCopy(rendererFormatSupports[i], rendererTrackGroupCount); rendererTrackTypes[i] = rendererCapabilities[i].getTrackType(); } - // Create a track group array for track groups not associated with a renderer. - int unassociatedTrackGroupCount = rendererTrackGroupCounts[rendererCapabilities.length]; - TrackGroupArray unassociatedTrackGroupArray = new TrackGroupArray(Arrays.copyOf( - rendererTrackGroups[rendererCapabilities.length], unassociatedTrackGroupCount)); - - TrackSelection[] trackSelections = selectTracks(rendererCapabilities, rendererTrackGroupArrays, - rendererFormatSupports); - - // Apply track disabling and overriding. - for (int i = 0; i < rendererCapabilities.length; i++) { - if (rendererDisabledFlags.get(i)) { - trackSelections[i] = null; - } else { - TrackGroupArray rendererTrackGroup = rendererTrackGroupArrays[i]; - Map overrides = selectionOverrides.get(i); - SelectionOverride override = overrides == null ? null : overrides.get(rendererTrackGroup); - if (override != null) { - trackSelections[i] = override.createTrackSelection(rendererTrackGroup); - } - } - } + // Create a track group array for track groups not mapped to a renderer. + int unmappedTrackGroupCount = rendererTrackGroupCounts[rendererCapabilities.length]; + TrackGroupArray unmappedTrackGroupArray = + new TrackGroupArray( + Util.nullSafeArrayCopy( + rendererTrackGroups[rendererCapabilities.length], unmappedTrackGroupCount)); // Package up the track information and selections. - MappedTrackInfo mappedTrackInfo = new MappedTrackInfo(rendererTrackTypes, - rendererTrackGroupArrays, mixedMimeTypeAdaptationSupport, rendererFormatSupports, - unassociatedTrackGroupArray); + MappedTrackInfo mappedTrackInfo = + new MappedTrackInfo( + rendererTrackTypes, + rendererTrackGroupArrays, + rendererMixedMimeTypeAdaptationSupports, + rendererFormatSupports, + unmappedTrackGroupArray); - // Initialize the renderer configurations to the default configuration for all renderers with - // selections, and null otherwise. - RendererConfiguration[] rendererConfigurations = - new RendererConfiguration[rendererCapabilities.length]; - for (int i = 0; i < rendererCapabilities.length; i++) { - rendererConfigurations[i] = trackSelections[i] != null ? RendererConfiguration.DEFAULT : null; - } - // Configure audio and video renderers to use tunneling if appropriate. - maybeConfigureRenderersForTunneling(rendererCapabilities, rendererTrackGroupArrays, - rendererFormatSupports, rendererConfigurations, trackSelections, tunnelingAudioSessionId); - - return new TrackSelectorResult(trackGroups, new TrackSelectionArray(trackSelections), - mappedTrackInfo, rendererConfigurations); - } - - @Override - public final void onSelectionActivated(Object info) { - currentMappedTrackInfo = (MappedTrackInfo) info; + Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> result = + selectTracks( + mappedTrackInfo, rendererFormatSupports, rendererMixedMimeTypeAdaptationSupports); + return new TrackSelectorResult(result.first, result.second, mappedTrackInfo); } /** - * Given an array of renderers and a set of {@link TrackGroup}s mapped to each of them, provides a - * {@link TrackSelection} per renderer. + * Given mapped track information, returns a track selection and configuration for each renderer. * - * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which - * {@link TrackSelection}s are to be generated. - * @param rendererTrackGroupArrays An array of {@link TrackGroupArray}s where each entry - * corresponds to the renderer of equal index in {@code renderers}. - * @param rendererFormatSupports Maps every available track to a specific level of support as - * defined by the renderer {@code FORMAT_*} constants. + * @param mappedTrackInfo Mapped track information. + * @param rendererFormatSupports The {@link Capabilities} for ach mapped track, indexed by + * renderer, track group and track (in that order). + * @param rendererMixedMimeTypeAdaptationSupport The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. + * @return A pair consisting of the track selections and configurations for each renderer. A null + * configuration indicates the renderer should be disabled, in which case the track selection + * will also be null. A track selection may also be null for a non-disabled renderer if {@link + * RendererCapabilities#getTrackType()} is {@link C#TRACK_TYPE_NONE}. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected abstract TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, - TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) - throws ExoPlaybackException; + protected abstract Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> + selectTracks( + MappedTrackInfo mappedTrackInfo, + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupport) + throws ExoPlaybackException; /** - * Finds the renderer to which the provided {@link TrackGroup} should be associated. - *

- * A {@link TrackGroup} is associated to a renderer that reports - * {@link RendererCapabilities#FORMAT_HANDLED} support for one or more of the tracks in the group, - * or {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} if no such renderer exists, or - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} if again no such renderer exists. In - * the case that two or more renderers report the same level of support, the renderer with the - * lowest index is associated. - *

- * If all renderers report {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all of the - * tracks in the group, then {@code renderers.length} is returned to indicate that no association - * was made. + * Finds the renderer to which the provided {@link TrackGroup} should be mapped. + * + *

A {@link TrackGroup} is mapped to the renderer that reports the highest of (listed in + * decreasing order of support) {@link RendererCapabilities#FORMAT_HANDLED}, {@link + * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, {@link + * RendererCapabilities#FORMAT_UNSUPPORTED_DRM} and {@link + * RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE}. + * + *

In the case that two or more renderers report the same level of support, the assignment + * depends on {@code preferUnassociatedRenderer}. + * + *

    + *
  • If {@code preferUnassociatedRenderer} is false, the renderer with the lowest index is + * chosen regardless of how many other track groups are already mapped to this renderer. + *
  • If {@code preferUnassociatedRenderer} is true, the renderer with the lowest index and no + * other mapped track group is chosen, or the renderer with the lowest index if all + * available renderers have already mapped track groups. + *
+ * + *

If all renderers report {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all of the + * tracks in the group, then {@code renderers.length} is returned to indicate that the group was + * not mapped to any renderer. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderers. - * @param group The {@link TrackGroup} whose associated renderer is to be found. - * @return The index of the associated renderer, or {@code renderers.length} if no - * association was made. + * @param group The track group to map to a renderer. + * @param rendererTrackGroupCounts The number of already mapped track groups for each renderer. + * @param preferUnassociatedRenderer Whether renderers unassociated to any track group should be + * preferred. + * @return The index of the renderer to which the track group was mapped, or {@code + * renderers.length} if it was not mapped to any renderer. * @throws ExoPlaybackException If an error occurs finding a renderer. */ - private static int findRenderer(RendererCapabilities[] rendererCapabilities, TrackGroup group) + private static int findRenderer( + RendererCapabilities[] rendererCapabilities, + TrackGroup group, + int[] rendererTrackGroupCounts, + boolean preferUnassociatedRenderer) throws ExoPlaybackException { int bestRendererIndex = rendererCapabilities.length; - int bestFormatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; + @FormatSupport int bestFormatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; + boolean bestRendererIsUnassociated = true; for (int rendererIndex = 0; rendererIndex < rendererCapabilities.length; rendererIndex++) { RendererCapabilities rendererCapability = rendererCapabilities[rendererIndex]; + @FormatSupport int formatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { - int formatSupportLevel = rendererCapability.supportsFormat(group.getFormat(trackIndex)) - & RendererCapabilities.FORMAT_SUPPORT_MASK; - if (formatSupportLevel > bestFormatSupportLevel) { - bestRendererIndex = rendererIndex; - bestFormatSupportLevel = formatSupportLevel; - if (bestFormatSupportLevel == RendererCapabilities.FORMAT_HANDLED) { - // We can't do better. - return bestRendererIndex; - } - } + @FormatSupport + int trackFormatSupportLevel = + RendererCapabilities.getFormatSupport( + rendererCapability.supportsFormat(group.getFormat(trackIndex))); + formatSupportLevel = Math.max(formatSupportLevel, trackFormatSupportLevel); + } + boolean rendererIsUnassociated = rendererTrackGroupCounts[rendererIndex] == 0; + if (formatSupportLevel > bestFormatSupportLevel + || (formatSupportLevel == bestFormatSupportLevel + && preferUnassociatedRenderer + && !bestRendererIsUnassociated + && rendererIsUnassociated)) { + bestRendererIndex = rendererIndex; + bestFormatSupportLevel = formatSupportLevel; + bestRendererIsUnassociated = rendererIsUnassociated; } } return bestRendererIndex; } /** - * Calls {@link RendererCapabilities#supportsFormat} for each track in the specified - * {@link TrackGroup}, returning the results in an array. + * Calls {@link RendererCapabilities#supportsFormat} for each track in the specified {@link + * TrackGroup}, returning the results in an array. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderer. - * @param group The {@link TrackGroup} to evaluate. - * @return An array containing the result of calling - * {@link RendererCapabilities#supportsFormat} for each track in the group. + * @param group The track group to evaluate. + * @return An array containing {@link Capabilities} for each track in the group. * @throws ExoPlaybackException If an error occurs determining the format support. */ + @Capabilities private static int[] getFormatSupport(RendererCapabilities rendererCapabilities, TrackGroup group) throws ExoPlaybackException { - int[] formatSupport = new int[group.length]; + @Capabilities int[] formatSupport = new int[group.length]; for (int i = 0; i < group.length; i++) { formatSupport[i] = rendererCapabilities.supportsFormat(group.getFormat(i)); } @@ -419,315 +524,18 @@ public abstract class MappingTrackSelector extends TrackSelector { * returning the results in an array. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderers. - * @return An array containing the result of calling - * {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @return An array containing the {@link AdaptiveSupport} for mixed MIME type adaptation for the + * renderer. * @throws ExoPlaybackException If an error occurs determining the adaptation support. */ - private static int[] getMixedMimeTypeAdaptationSupport( + @AdaptiveSupport + private static int[] getMixedMimeTypeAdaptationSupports( RendererCapabilities[] rendererCapabilities) throws ExoPlaybackException { - int[] mixedMimeTypeAdaptationSupport = new int[rendererCapabilities.length]; + @AdaptiveSupport int[] mixedMimeTypeAdaptationSupport = new int[rendererCapabilities.length]; for (int i = 0; i < mixedMimeTypeAdaptationSupport.length; i++) { mixedMimeTypeAdaptationSupport[i] = rendererCapabilities[i].supportsMixedMimeTypeAdaptation(); } return mixedMimeTypeAdaptationSupport; } - /** - * Determines whether tunneling should be enabled, replacing {@link RendererConfiguration}s in - * {@code rendererConfigurations} with configurations that enable tunneling on the appropriate - * renderers if so. - * - * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which - * {@link TrackSelection}s are to be generated. - * @param rendererTrackGroupArrays An array of {@link TrackGroupArray}s where each entry - * corresponds to the renderer of equal index in {@code renderers}. - * @param rendererFormatSupports Maps every available track to a specific level of support as - * defined by the renderer {@code FORMAT_*} constants. - * @param rendererConfigurations The renderer configurations. Configurations may be replaced with - * ones that enable tunneling as a result of this call. - * @param trackSelections The renderer track selections. - * @param tunnelingAudioSessionId The audio session id to use when tunneling, or - * {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled. - */ - private static void maybeConfigureRenderersForTunneling( - RendererCapabilities[] rendererCapabilities, TrackGroupArray[] rendererTrackGroupArrays, - int[][][] rendererFormatSupports, RendererConfiguration[] rendererConfigurations, - TrackSelection[] trackSelections, int tunnelingAudioSessionId) { - if (tunnelingAudioSessionId == C.AUDIO_SESSION_ID_UNSET) { - return; - } - // Check whether we can enable tunneling. To enable tunneling we require exactly one audio and - // one video renderer to support tunneling and have a selection. - int tunnelingAudioRendererIndex = -1; - int tunnelingVideoRendererIndex = -1; - boolean enableTunneling = true; - for (int i = 0; i < rendererCapabilities.length; i++) { - int rendererType = rendererCapabilities[i].getTrackType(); - TrackSelection trackSelection = trackSelections[i]; - if ((rendererType == C.TRACK_TYPE_AUDIO || rendererType == C.TRACK_TYPE_VIDEO) - && trackSelection != null) { - if (rendererSupportsTunneling(rendererFormatSupports[i], rendererTrackGroupArrays[i], - trackSelection)) { - if (rendererType == C.TRACK_TYPE_AUDIO) { - if (tunnelingAudioRendererIndex != -1) { - enableTunneling = false; - break; - } else { - tunnelingAudioRendererIndex = i; - } - } else { - if (tunnelingVideoRendererIndex != -1) { - enableTunneling = false; - break; - } else { - tunnelingVideoRendererIndex = i; - } - } - } - } - } - enableTunneling &= tunnelingAudioRendererIndex != -1 && tunnelingVideoRendererIndex != -1; - if (enableTunneling) { - RendererConfiguration tunnelingRendererConfiguration = - new RendererConfiguration(tunnelingAudioSessionId); - rendererConfigurations[tunnelingAudioRendererIndex] = tunnelingRendererConfiguration; - rendererConfigurations[tunnelingVideoRendererIndex] = tunnelingRendererConfiguration; - } - } - - /** - * Returns whether a renderer supports tunneling for a {@link TrackSelection}. - * - * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each - * track, indexed by group index and track index (in that order). - * @param trackGroups The {@link TrackGroupArray}s for the renderer. - * @param selection The track selection. - * @return Whether the renderer supports tunneling for the {@link TrackSelection}. - */ - private static boolean rendererSupportsTunneling(int[][] formatSupport, - TrackGroupArray trackGroups, TrackSelection selection) { - if (selection == null) { - return false; - } - int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); - for (int i = 0; i < selection.length(); i++) { - int trackFormatSupport = formatSupport[trackGroupIndex][selection.getIndexInTrackGroup(i)]; - if ((trackFormatSupport & RendererCapabilities.TUNNELING_SUPPORT_MASK) - != RendererCapabilities.TUNNELING_SUPPORTED) { - return false; - } - } - return true; - } - - /** - * Provides track information for each renderer. - */ - public static final class MappedTrackInfo { - - /** - * The renderer does not have any associated tracks. - */ - public static final int RENDERER_SUPPORT_NO_TRACKS = 0; - /** - * The renderer has associated tracks, but all are of unsupported types. - */ - public static final int RENDERER_SUPPORT_UNSUPPORTED_TRACKS = 1; - /** - * The renderer has associated tracks and at least one is of a supported type, but all of the - * tracks whose types are supported exceed the renderer's capabilities. - */ - public static final int RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS = 2; - /** - * The renderer has associated tracks and can play at least one of them. - */ - public static final int RENDERER_SUPPORT_PLAYABLE_TRACKS = 3; - - /** - * The number of renderers to which tracks are mapped. - */ - public final int length; - - private final int[] rendererTrackTypes; - private final TrackGroupArray[] trackGroups; - private final int[] mixedMimeTypeAdaptiveSupport; - private final int[][][] formatSupport; - private final TrackGroupArray unassociatedTrackGroups; - - /** - * @param rendererTrackTypes The track type supported by each renderer. - * @param trackGroups The {@link TrackGroupArray}s for each renderer. - * @param mixedMimeTypeAdaptiveSupport The result of - * {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. - * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each - * track, indexed by renderer index, group index and track index (in that order). - * @param unassociatedTrackGroups Contains {@link TrackGroup}s not associated with any renderer. - */ - /* package */ MappedTrackInfo(int[] rendererTrackTypes, - TrackGroupArray[] trackGroups, int[] mixedMimeTypeAdaptiveSupport, - int[][][] formatSupport, TrackGroupArray unassociatedTrackGroups) { - this.rendererTrackTypes = rendererTrackTypes; - this.trackGroups = trackGroups; - this.formatSupport = formatSupport; - this.mixedMimeTypeAdaptiveSupport = mixedMimeTypeAdaptiveSupport; - this.unassociatedTrackGroups = unassociatedTrackGroups; - this.length = trackGroups.length; - } - - /** - * Returns the array of {@link TrackGroup}s associated to the renderer at a specified index. - * - * @param rendererIndex The renderer index. - * @return The corresponding {@link TrackGroup}s. - */ - public TrackGroupArray getTrackGroups(int rendererIndex) { - return trackGroups[rendererIndex]; - } - - /** - * Returns the extent to which a renderer can support playback of the tracks associated to it. - * - * @param rendererIndex The renderer index. - * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, - * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, - * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. - */ - public int getRendererSupport(int rendererIndex) { - int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; - int[][] rendererFormatSupport = formatSupport[rendererIndex]; - for (int i = 0; i < rendererFormatSupport.length; i++) { - for (int j = 0; j < rendererFormatSupport[i].length; j++) { - int trackRendererSupport; - switch (rendererFormatSupport[i][j] & RendererCapabilities.FORMAT_SUPPORT_MASK) { - case RendererCapabilities.FORMAT_HANDLED: - return RENDERER_SUPPORT_PLAYABLE_TRACKS; - case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: - trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS; - break; - default: - trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS; - break; - } - bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport); - } - } - return bestRendererSupport; - } - - /** - * Returns the best level of support obtained from {@link #getRendererSupport(int)} for all - * renderers of the specified track type. If no renderers exist for the specified type then - * {@link #RENDERER_SUPPORT_NO_TRACKS} is returned. - * - * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, - * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, - * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. - */ - public int getTrackTypeRendererSupport(int trackType) { - int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; - for (int i = 0; i < length; i++) { - if (rendererTrackTypes[i] == trackType) { - bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i)); - } - } - return bestRendererSupport; - } - - /** - * Returns the extent to which the format of an individual track is supported by the renderer. - * - * @param rendererIndex The renderer index. - * @param groupIndex The index of the group to which the track belongs. - * @param trackIndex The index of the track within the group. - * @return One of {@link RendererCapabilities#FORMAT_HANDLED}, - * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}. - */ - public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) { - return formatSupport[rendererIndex][groupIndex][trackIndex] - & RendererCapabilities.FORMAT_SUPPORT_MASK; - } - - /** - * Returns the extent to which the renderer supports adaptation between supported tracks in a - * specified {@link TrackGroup}. - *

- * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns - * {@link RendererCapabilities#FORMAT_HANDLED} are always considered. - * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered. - * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns - * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} are considered only if - * {@code includeCapabilitiesExceededTracks} is set to {@code true}. - * - * @param rendererIndex The renderer index. - * @param groupIndex The index of the group. - * @param includeCapabilitiesExceededTracks True if formats that exceed the capabilities of the - * renderer should be included when determining support. False otherwise. - * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, - * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and - * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. - */ - public int getAdaptiveSupport(int rendererIndex, int groupIndex, - boolean includeCapabilitiesExceededTracks) { - int trackCount = trackGroups[rendererIndex].get(groupIndex).length; - // Iterate over the tracks in the group, recording the indices of those to consider. - int[] trackIndices = new int[trackCount]; - int trackIndexCount = 0; - for (int i = 0; i < trackCount; i++) { - int fixedSupport = getTrackFormatSupport(rendererIndex, groupIndex, i); - if (fixedSupport == RendererCapabilities.FORMAT_HANDLED - || (includeCapabilitiesExceededTracks - && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) { - trackIndices[trackIndexCount++] = i; - } - } - trackIndices = Arrays.copyOf(trackIndices, trackIndexCount); - return getAdaptiveSupport(rendererIndex, groupIndex, trackIndices); - } - - /** - * Returns the extent to which the renderer supports adaptation between specified tracks within - * a {@link TrackGroup}. - * - * @param rendererIndex The renderer index. - * @param groupIndex The index of the group. - * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, - * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and - * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. - */ - public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) { - int handledTrackCount = 0; - int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS; - boolean multipleMimeTypes = false; - String firstSampleMimeType = null; - for (int i = 0; i < trackIndices.length; i++) { - int trackIndex = trackIndices[i]; - String sampleMimeType = trackGroups[rendererIndex].get(groupIndex).getFormat(trackIndex) - .sampleMimeType; - if (handledTrackCount++ == 0) { - firstSampleMimeType = sampleMimeType; - } else { - multipleMimeTypes |= !Util.areEqual(firstSampleMimeType, sampleMimeType); - } - adaptiveSupport = Math.min(adaptiveSupport, formatSupport[rendererIndex][groupIndex][i] - & RendererCapabilities.ADAPTIVE_SUPPORT_MASK); - } - return multipleMimeTypes - ? Math.min(adaptiveSupport, mixedMimeTypeAdaptiveSupport[rendererIndex]) - : adaptiveSupport; - } - - /** - * Returns the {@link TrackGroup}s not associated with any renderer. - */ - public TrackGroupArray getUnassociatedTrackGroups() { - return unassociatedTrackGroups; - } - - } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java index 17248f4406c7..75b7fc21f1bd 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java @@ -16,9 +16,15 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection; import android.os.SystemClock; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter; +import java.util.List; import java.util.Random; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A {@link TrackSelection} whose selected track is updated randomly. @@ -44,10 +50,12 @@ public final class RandomTrackSelection extends BaseTrackSelection { } @Override - public RandomTrackSelection createTrackSelection(TrackGroup group, int... tracks) { - return new RandomTrackSelection(group, tracks, random); + public @NullableType TrackSelection[] createTrackSelections( + @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { + return TrackSelectionUtil.createTrackSelectionsForDefinitions( + definitions, + definition -> new RandomTrackSelection(definition.group, definition.tracks, random)); } - } private final Random random; @@ -88,7 +96,12 @@ public final class RandomTrackSelection extends BaseTrackSelection { } @Override - public void updateSelectedTrack(long bufferedDurationUs) { + public void updateSelectedTrack( + long playbackPositionUs, + long bufferedDurationUs, + long availableDurationUs, + List queue, + MediaChunkIterator[] mediaChunkIterators) { // Count the number of non-blacklisted formats. long nowMs = SystemClock.elapsedRealtime(); int nonBlacklistedFormatCount = 0; @@ -122,6 +135,7 @@ public final class RandomTrackSelection extends BaseTrackSelection { } @Override + @Nullable public Object getSelectionData() { return null; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelection.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelection.java index 2da1fe2514ea..d2f32222fa92 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -15,38 +15,100 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter; import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** - * A track selection consisting of a static subset of selected tracks belonging to a - * {@link TrackGroup}, and a possibly varying individual selected track from the subset. - *

- * Tracks belonging to the subset are exposed in decreasing bandwidth order. The individual selected - * track may change as a result of calling {@link #updateSelectedTrack(long)}. + * A track selection consisting of a static subset of selected tracks belonging to a {@link + * TrackGroup}, and a possibly varying individual selected track from the subset. + * + *

Tracks belonging to the subset are exposed in decreasing bandwidth order. The individual + * selected track may change dynamically as a result of calling {@link #updateSelectedTrack(long, + * long, long, List, MediaChunkIterator[])} or {@link #evaluateQueueSize(long, List)}. This only + * happens between calls to {@link #enable()} and {@link #disable()}. */ public interface TrackSelection { + /** Contains of a subset of selected tracks belonging to a {@link TrackGroup}. */ + final class Definition { + /** The {@link TrackGroup} which tracks belong to. */ + public final TrackGroup group; + /** The indices of the selected tracks in {@link #group}. */ + public final int[] tracks; + /** The track selection reason. One of the {@link C} SELECTION_REASON_ constants. */ + public final int reason; + /** Optional data associated with this selection of tracks. */ + @Nullable public final Object data; + + /** + * @param group The {@link TrackGroup}. Must not be null. + * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be + * null or empty. May be in any order. + */ + public Definition(TrackGroup group, int... tracks) { + this(group, tracks, C.SELECTION_REASON_UNKNOWN, /* data= */ null); + } + + /** + * @param group The {@link TrackGroup}. Must not be null. + * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be + * @param reason The track selection reason. One of the {@link C} SELECTION_REASON_ constants. + * @param data Optional data associated with this selection of tracks. + */ + public Definition(TrackGroup group, int[] tracks, int reason, @Nullable Object data) { + this.group = group; + this.tracks = tracks; + this.reason = reason; + this.data = data; + } + } + /** * Factory for {@link TrackSelection} instances. */ interface Factory { /** - * Creates a new selection. + * Creates track selections for the provided {@link Definition Definitions}. * - * @param group The {@link TrackGroup}. Must not be null. - * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be - * null or empty. May be in any order. - * @return The created selection. + *

Implementations that create at most one adaptive track selection may use {@link + * TrackSelectionUtil#createTrackSelectionsForDefinitions}. + * + * @param definitions A {@link Definition} array. May include null values. + * @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks. + * @return The created selections. Must have the same length as {@code definitions} and may + * include null values. */ - TrackSelection createTrackSelection(TrackGroup group, int... tracks); - + @NullableType + TrackSelection[] createTrackSelections( + @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter); } + /** + * Enables the track selection. Dynamic changes via {@link #updateSelectedTrack(long, long, long, + * List, MediaChunkIterator[])} or {@link #evaluateQueueSize(long, List)} will only happen after + * this call. + * + *

This method may not be called when the track selection is already enabled. + */ + void enable(); + + /** + * Disables this track selection. No further dynamic changes via {@link #updateSelectedTrack(long, + * long, long, List, MediaChunkIterator[])} or {@link #evaluateQueueSize(long, List)} will happen + * after this call. + * + *

This method may only be called when the track selection is already enabled. + */ + void disable(); + /** * Returns the {@link TrackGroup} to which the selected tracks belong. */ @@ -76,7 +138,9 @@ public interface TrackSelection { int getIndexInTrackGroup(int index); /** - * Returns the index in the selection of the track with the specified format. + * Returns the index in the selection of the track with the specified format. The format is + * located by identity so, for example, {@code selection.indexOf(selection.getFormat(index)) == + * index} even if multiple selected tracks have formats that contain the same values. * * @param format The format. * @return The index in the selection, or {@link C#INDEX_UNSET} if the track with the specified @@ -115,19 +179,56 @@ public interface TrackSelection { */ int getSelectionReason(); - /** - * Returns optional data associated with the current track selection. - */ - Object getSelectionData(); + /** Returns optional data associated with the current track selection. */ + @Nullable Object getSelectionData(); // Adaptation. /** - * Updates the selected track. + * Called to notify the selection of the current playback speed. The playback speed may affect + * adaptive track selection. * - * @param bufferedDurationUs The duration of media currently buffered in microseconds. + * @param speed The playback speed. */ - void updateSelectedTrack(long bufferedDurationUs); + void onPlaybackSpeed(float speed); + + /** + * Called to notify the selection of a position discontinuity. + * + *

This happens when the playback position jumps, e.g., as a result of a seek being performed. + */ + default void onDiscontinuity() {} + + /** + * Updates the selected track for sources that load media in discrete {@link MediaChunk}s. + * + *

This method may only be called when the selection is enabled. + * + * @param playbackPositionUs The current playback position in microseconds. If playback of the + * period to which this track selection belongs has not yet started, the value will be the + * starting position in the period minus the duration of any media in previous periods still + * to be played. + * @param bufferedDurationUs The duration of media currently buffered from the current playback + * position, in microseconds. Note that the next load position can be calculated as {@code + * (playbackPositionUs + bufferedDurationUs)}. + * @param availableDurationUs The duration of media available for buffering from the current + * playback position, in microseconds, or {@link C#TIME_UNSET} if media can be buffered to the + * end of the current period. Note that if not set to {@link C#TIME_UNSET}, the position up to + * which media is available for buffering can be calculated as {@code (playbackPositionUs + + * availableDurationUs)}. + * @param queue The queue of already buffered {@link MediaChunk}s. Must not be modified. + * @param mediaChunkIterators An array of {@link MediaChunkIterator}s providing information about + * the sequence of upcoming media chunks for each track in the selection. All iterators start + * from the media chunk which will be loaded next if the respective track is selected. Note + * that this information may not be available for all tracks, and so some iterators may be + * empty. + */ + void updateSelectedTrack( + long playbackPositionUs, + long bufferedDurationUs, + long availableDurationUs, + List queue, + MediaChunkIterator[] mediaChunkIterators); /** * May be called periodically by sources that load media in discrete {@link MediaChunk}s and @@ -138,9 +239,12 @@ public interface TrackSelection { * An example of a case where a smaller value may be returned is if network conditions have * improved dramatically, allowing chunks to be discarded and re-buffered in a track of * significantly higher quality. Discarding chunks may allow faster switching to a higher quality - * track in this case. + * track in this case. This method may only be called when the selection is enabled. * - * @param playbackPositionUs The current playback position in microseconds. + * @param playbackPositionUs The current playback position in microseconds. If playback of the + * period to which this track selection belongs has not yet started, the value will be the + * starting position in the period minus the duration of any media in previous periods still + * to be played. * @param queue The queue of buffered {@link MediaChunk}s. Must not be modified. * @return The number of chunks to retain in the queue. */ @@ -148,10 +252,13 @@ public interface TrackSelection { /** * Attempts to blacklist the track at the specified index in the selection, making it ineligible - * for selection by calls to {@link #updateSelectedTrack(long)} for the specified period of time. - * Blacklisting will fail if all other tracks are currently blacklisted. If blacklisting the - * currently selected track, note that it will remain selected until the next call to - * {@link #updateSelectedTrack(long)}. + * for selection by calls to {@link #updateSelectedTrack(long, long, long, List, + * MediaChunkIterator[])} for the specified period of time. Blacklisting will fail if all other + * tracks are currently blacklisted. If blacklisting the currently selected track, note that it + * will remain selected until the next call to {@link #updateSelectedTrack(long, long, long, List, + * MediaChunkIterator[])}. + * + *

This method may only be called when the selection is enabled. * * @param index The index of the track in the selection. * @param blacklistDurationMs The duration of time for which the track should be blacklisted, in @@ -159,5 +266,4 @@ public interface TrackSelection { * @return Whether blacklisting was successful. */ boolean blacklist(int index, long blacklistDurationMs); - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java index 32a7348b92cd..7953ef354c7f 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java @@ -15,27 +15,23 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection; +import androidx.annotation.Nullable; import java.util.Arrays; +import org.checkerframework.checker.nullness.compatqual.NullableType; -/** - * The result of a {@link TrackSelector} operation. - */ +/** An array of {@link TrackSelection}s. */ public final class TrackSelectionArray { - /** - * The number of selections in the result. Greater than or equal to zero. - */ + /** The length of this array. */ public final int length; - private final TrackSelection[] trackSelections; + private final @NullableType TrackSelection[] trackSelections; // Lazily initialized hashcode. private int hashCode; - /** - * @param trackSelections The selections. Must not be null, but may contain null elements. - */ - public TrackSelectionArray(TrackSelection... trackSelections) { + /** @param trackSelections The selections. Must not be null, but may contain null elements. */ + public TrackSelectionArray(@NullableType TrackSelection... trackSelections) { this.trackSelections = trackSelections; this.length = trackSelections.length; } @@ -46,14 +42,13 @@ public final class TrackSelectionArray { * @param index The index of the selection. * @return The selection. */ + @Nullable public TrackSelection get(int index) { return trackSelections[index]; } - /** - * Returns the selections in a newly allocated array. - */ - public TrackSelection[] getAll() { + /** Returns the selections in a newly allocated array. */ + public @NullableType TrackSelection[] getAll() { return trackSelections.clone(); } @@ -68,7 +63,7 @@ public final class TrackSelectionArray { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java new file mode 100644 index 000000000000..b6086fa594c0 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java @@ -0,0 +1,336 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Looper; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.view.accessibility.CaptioningManager; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.Locale; + +/** Constraint parameters for track selection. */ +public class TrackSelectionParameters implements Parcelable { + + /** + * A builder for {@link TrackSelectionParameters}. See the {@link TrackSelectionParameters} + * documentation for explanations of the parameters that can be configured using this builder. + */ + public static class Builder { + + @Nullable /* package */ String preferredAudioLanguage; + @Nullable /* package */ String preferredTextLanguage; + @C.RoleFlags /* package */ int preferredTextRoleFlags; + /* package */ boolean selectUndeterminedTextLanguage; + @C.SelectionFlags /* package */ int disabledTextTrackSelectionFlags; + + /** + * Creates a builder with default initial values. + * + * @param context Any context. + */ + @SuppressWarnings({"deprecation", "initialization:method.invocation.invalid"}) + public Builder(Context context) { + this(); + setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(context); + } + + /** + * @deprecated {@link Context} constraints will not be set when using this constructor. Use + * {@link #Builder(Context)} instead. + */ + @Deprecated + public Builder() { + preferredAudioLanguage = null; + preferredTextLanguage = null; + preferredTextRoleFlags = 0; + selectUndeterminedTextLanguage = false; + disabledTextTrackSelectionFlags = 0; + } + + /** + * @param initialValues The {@link TrackSelectionParameters} from which the initial values of + * the builder are obtained. + */ + /* package */ Builder(TrackSelectionParameters initialValues) { + preferredAudioLanguage = initialValues.preferredAudioLanguage; + preferredTextLanguage = initialValues.preferredTextLanguage; + preferredTextRoleFlags = initialValues.preferredTextRoleFlags; + selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage; + disabledTextTrackSelectionFlags = initialValues.disabledTextTrackSelectionFlags; + } + + /** + * Sets the preferred language for audio and forced text tracks. + * + * @param preferredAudioLanguage Preferred audio language as an IETF BCP 47 conformant tag, or + * {@code null} to select the default track, or the first track if there's no default. + * @return This builder. + */ + public Builder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) { + this.preferredAudioLanguage = preferredAudioLanguage; + return this; + } + + /** + * Sets the preferred language and role flags for text tracks based on the accessibility + * settings of {@link CaptioningManager}. + * + *

Does nothing for API levels < 19 or when the {@link CaptioningManager} is disabled. + * + * @param context A {@link Context}. + * @return This builder. + */ + public Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings( + Context context) { + if (Util.SDK_INT >= 19) { + setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettingsV19(context); + } + return this; + } + + /** + * Sets the preferred language for text tracks. + * + * @param preferredTextLanguage Preferred text language as an IETF BCP 47 conformant tag, or + * {@code null} to select the default track if there is one, or no track otherwise. + * @return This builder. + */ + public Builder setPreferredTextLanguage(@Nullable String preferredTextLanguage) { + this.preferredTextLanguage = preferredTextLanguage; + return this; + } + + /** + * Sets the preferred {@link C.RoleFlags} for text tracks. + * + * @param preferredTextRoleFlags Preferred text role flags. + * @return This builder. + */ + public Builder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags) { + this.preferredTextRoleFlags = preferredTextRoleFlags; + return this; + } + + /** + * Sets whether a text track with undetermined language should be selected if no track with + * {@link #setPreferredTextLanguage(String)} is available, or if the preferred language is + * unset. + * + * @param selectUndeterminedTextLanguage Whether a text track with undetermined language should + * be selected if no preferred language track is available. + * @return This builder. + */ + public Builder setSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) { + this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; + return this; + } + + /** + * Sets a bitmask of selection flags that are disabled for text track selections. + * + * @param disabledTextTrackSelectionFlags A bitmask of {@link C.SelectionFlags} that are + * disabled for text track selections. + * @return This builder. + */ + public Builder setDisabledTextTrackSelectionFlags( + @C.SelectionFlags int disabledTextTrackSelectionFlags) { + this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags; + return this; + } + + /** Builds a {@link TrackSelectionParameters} instance with the selected values. */ + public TrackSelectionParameters build() { + return new TrackSelectionParameters( + // Audio + preferredAudioLanguage, + // Text + preferredTextLanguage, + preferredTextRoleFlags, + selectUndeterminedTextLanguage, + disabledTextTrackSelectionFlags); + } + + @TargetApi(19) + private void setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettingsV19( + Context context) { + if (Util.SDK_INT < 23 && Looper.myLooper() == null) { + // Android platform bug (pre-Marshmallow) that causes RuntimeExceptions when + // CaptioningService is instantiated from a non-Looper thread. See [internal: b/143779904]. + return; + } + CaptioningManager captioningManager = + (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); + if (captioningManager == null || !captioningManager.isEnabled()) { + return; + } + preferredTextRoleFlags = C.ROLE_FLAG_CAPTION | C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND; + Locale preferredLocale = captioningManager.getLocale(); + if (preferredLocale != null) { + preferredTextLanguage = Util.getLocaleLanguageTag(preferredLocale); + } + } + } + + /** + * An instance with default values, except those obtained from the {@link Context}. + * + *

If possible, use {@link #getDefaults(Context)} instead. + * + *

This instance will not have the following settings: + * + *

    + *
  • {@link Builder#setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(Context) + * Preferred text language and role flags} configured to the accessibility settings of + * {@link CaptioningManager}. + *
+ */ + @SuppressWarnings("deprecation") + public static final TrackSelectionParameters DEFAULT_WITHOUT_CONTEXT = new Builder().build(); + + /** + * @deprecated This instance is not configured using {@link Context} constraints. Use {@link + * #getDefaults(Context)} instead. + */ + @Deprecated public static final TrackSelectionParameters DEFAULT = DEFAULT_WITHOUT_CONTEXT; + + /** Returns an instance configured with default values. */ + public static TrackSelectionParameters getDefaults(Context context) { + return new Builder(context).build(); + } + + /** + * The preferred language for audio and forced text tracks as an IETF BCP 47 conformant tag. + * {@code null} selects the default track, or the first track if there's no default. The default + * value is {@code null}. + */ + @Nullable public final String preferredAudioLanguage; + /** + * The preferred language for text tracks as an IETF BCP 47 conformant tag. {@code null} selects + * the default track if there is one, or no track otherwise. The default value is {@code null}, or + * the language of the accessibility {@link CaptioningManager} if enabled. + */ + @Nullable public final String preferredTextLanguage; + /** + * The preferred {@link C.RoleFlags} for text tracks. {@code 0} selects the default track if there + * is one, or no track otherwise. The default value is {@code 0}, or {@link C#ROLE_FLAG_SUBTITLE} + * | {@link C#ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND} if the accessibility {@link CaptioningManager} + * is enabled. + */ + @C.RoleFlags public final int preferredTextRoleFlags; + /** + * Whether a text track with undetermined language should be selected if no track with {@link + * #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. The + * default value is {@code false}. + */ + public final boolean selectUndeterminedTextLanguage; + /** + * Bitmask of selection flags that are disabled for text track selections. See {@link + * C.SelectionFlags}. The default value is {@code 0} (i.e. no flags). + */ + @C.SelectionFlags public final int disabledTextTrackSelectionFlags; + + /* package */ TrackSelectionParameters( + @Nullable String preferredAudioLanguage, + @Nullable String preferredTextLanguage, + @C.RoleFlags int preferredTextRoleFlags, + boolean selectUndeterminedTextLanguage, + @C.SelectionFlags int disabledTextTrackSelectionFlags) { + // Audio + this.preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage); + // Text + this.preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage); + this.preferredTextRoleFlags = preferredTextRoleFlags; + this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; + this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags; + } + + /* package */ TrackSelectionParameters(Parcel in) { + this.preferredAudioLanguage = in.readString(); + this.preferredTextLanguage = in.readString(); + this.preferredTextRoleFlags = in.readInt(); + this.selectUndeterminedTextLanguage = Util.readBoolean(in); + this.disabledTextTrackSelectionFlags = in.readInt(); + } + + /** Creates a new {@link Builder}, copying the initial values from this instance. */ + public Builder buildUpon() { + return new Builder(this); + } + + @Override + @SuppressWarnings("EqualsGetClass") + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + TrackSelectionParameters other = (TrackSelectionParameters) obj; + return TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) + && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage) + && preferredTextRoleFlags == other.preferredTextRoleFlags + && selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage + && disabledTextTrackSelectionFlags == other.disabledTextTrackSelectionFlags; + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + (preferredAudioLanguage == null ? 0 : preferredAudioLanguage.hashCode()); + result = 31 * result + (preferredTextLanguage == null ? 0 : preferredTextLanguage.hashCode()); + result = 31 * result + preferredTextRoleFlags; + result = 31 * result + (selectUndeterminedTextLanguage ? 1 : 0); + result = 31 * result + disabledTextTrackSelectionFlags; + return result; + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(preferredAudioLanguage); + dest.writeString(preferredTextLanguage); + dest.writeInt(preferredTextRoleFlags); + Util.writeBoolean(dest, selectUndeterminedTextLanguage); + dest.writeInt(disabledTextTrackSelectionFlags); + } + + public static final Creator CREATOR = + new Creator() { + + @Override + public TrackSelectionParameters createFromParcel(Parcel in) { + return new TrackSelectionParameters(in); + } + + @Override + public TrackSelectionParameters[] newArray(int size) { + return new TrackSelectionParameters[size]; + } + }; +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java new file mode 100644 index 000000000000..b2fcf5c13cdd --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection.Definition; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** Track selection related utility methods. */ +public final class TrackSelectionUtil { + + private TrackSelectionUtil() {} + + /** Functional interface to create a single adaptive track selection. */ + public interface AdaptiveTrackSelectionFactory { + + /** + * Creates an adaptive track selection for the provided track selection definition. + * + * @param trackSelectionDefinition A {@link Definition} for the track selection. + * @return The created track selection. + */ + TrackSelection createAdaptiveTrackSelection(Definition trackSelectionDefinition); + } + + /** + * Creates track selections for an array of track selection definitions, with at most one + * multi-track adaptive selection. + * + * @param definitions The list of track selection {@link Definition definitions}. May include null + * values. + * @param adaptiveTrackSelectionFactory A factory for the multi-track adaptive track selection. + * @return The array of created track selection. For null entries in {@code definitions} returns + * null values. + */ + public static @NullableType TrackSelection[] createTrackSelectionsForDefinitions( + @NullableType Definition[] definitions, + AdaptiveTrackSelectionFactory adaptiveTrackSelectionFactory) { + TrackSelection[] selections = new TrackSelection[definitions.length]; + boolean createdAdaptiveTrackSelection = false; + for (int i = 0; i < definitions.length; i++) { + Definition definition = definitions[i]; + if (definition == null) { + continue; + } + if (definition.tracks.length > 1 && !createdAdaptiveTrackSelection) { + createdAdaptiveTrackSelection = true; + selections[i] = adaptiveTrackSelectionFactory.createAdaptiveTrackSelection(definition); + } else { + selections[i] = + new FixedTrackSelection( + definition.group, definition.tracks[0], definition.reason, definition.data); + } + } + return selections; + } + + /** + * Updates {@link DefaultTrackSelector.Parameters} with an override. + * + * @param parameters The current {@link DefaultTrackSelector.Parameters} to build upon. + * @param rendererIndex The renderer index to update. + * @param trackGroupArray The {@link TrackGroupArray} of the renderer. + * @param isDisabled Whether the renderer should be set disabled. + * @param override An optional override for the renderer. If null, no override will be set and an + * existing override for this renderer will be cleared. + * @return The updated {@link DefaultTrackSelector.Parameters}. + */ + public static DefaultTrackSelector.Parameters updateParametersWithOverride( + DefaultTrackSelector.Parameters parameters, + int rendererIndex, + TrackGroupArray trackGroupArray, + boolean isDisabled, + @Nullable SelectionOverride override) { + DefaultTrackSelector.ParametersBuilder builder = + parameters + .buildUpon() + .clearSelectionOverrides(rendererIndex) + .setRendererDisabled(rendererIndex, isDisabled); + if (override != null) { + builder.setSelectionOverride(rendererIndex, trackGroupArray, override); + } + return builder.build(); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelector.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelector.java index 9493d81747a5..878031824db5 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelector.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelector.java @@ -15,58 +15,131 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Renderer; import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererConfiguration; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; -/** Selects tracks to be consumed by available renderers. */ +/** + * The component of an {@link ExoPlayer} responsible for selecting tracks to be consumed by each of + * the player's {@link Renderer}s. The {@link DefaultTrackSelector} implementation should be + * suitable for most use cases. + * + *

Interactions with the player

+ * + * The following interactions occur between the player and its track selector during playback. + * + *
    + *
  • When the player is created it will initialize the track selector by calling {@link + * #init(InvalidationListener, BandwidthMeter)}. + *
  • When the player needs to make a track selection it will call {@link + * #selectTracks(RendererCapabilities[], TrackGroupArray, MediaPeriodId, Timeline)}. This + * typically occurs at the start of playback, when the player starts to buffer a new period of + * the media being played, and when the track selector invalidates its previous selections. + *
  • The player may perform a track selection well in advance of the selected tracks becoming + * active, where active is defined to mean that the renderers are actually consuming media + * corresponding to the selection that was made. For example when playing media containing + * multiple periods, the track selection for a period is made when the player starts to buffer + * that period. Hence if the player's buffering policy is to maintain a 30 second buffer, the + * selection will occur approximately 30 seconds in advance of it becoming active. In fact the + * selection may never become active, for example if the user seeks to some other period of + * the media during the 30 second gap. The player indicates to the track selector when a + * selection it has previously made becomes active by calling {@link + * #onSelectionActivated(Object)}. + *
  • If the track selector wishes to indicate to the player that selections it has previously + * made are invalid, it can do so by calling {@link + * InvalidationListener#onTrackSelectionsInvalidated()} on the {@link InvalidationListener} + * that was passed to {@link #init(InvalidationListener, BandwidthMeter)}. A track selector + * may wish to do this if its configuration has changed, for example if it now wishes to + * prefer audio tracks in a particular language. This will trigger the player to make new + * track selections. Note that the player will have to re-buffer in the case that the new + * track selection for the currently playing period differs from the one that was invalidated. + *
+ * + *

Renderer configuration

+ * + * The {@link TrackSelectorResult} returned by {@link #selectTracks(RendererCapabilities[], + * TrackGroupArray, MediaPeriodId, Timeline)} contains not only {@link TrackSelection}s for each + * renderer, but also {@link RendererConfiguration}s defining configuration parameters that the + * renderers should apply when consuming the corresponding media. Whilst it may seem counter- + * intuitive for a track selector to also specify renderer configuration information, in practice + * the two are tightly bound together. It may only be possible to play a certain combination tracks + * if the renderers are configured in a particular way. Equally, it may only be possible to + * configure renderers in a particular way if certain tracks are selected. Hence it makes sense to + * determine the track selection and corresponding renderer configurations in a single step. + * + *

Threading model

+ * + * All calls made by the player into the track selector are on the player's internal playback + * thread. The track selector may call {@link InvalidationListener#onTrackSelectionsInvalidated()} + * from any thread. + */ public abstract class TrackSelector { /** - * Notified when previous selections by a {@link TrackSelector} are no longer valid. + * Notified when selections previously made by a {@link TrackSelector} are no longer valid. */ public interface InvalidationListener { /** - * Called by a {@link TrackSelector} when previous selections are no longer valid. + * Called by a {@link TrackSelector} to indicate that selections it has previously made are no + * longer valid. May be called from any thread. */ void onTrackSelectionsInvalidated(); } - private InvalidationListener listener; + @Nullable private InvalidationListener listener; + @Nullable private BandwidthMeter bandwidthMeter; /** - * Initializes the selector. + * Called by the player to initialize the selector. * - * @param listener A listener for the selector. + * @param listener An invalidation listener that the selector can call to indicate that selections + * it has previously made are no longer valid. + * @param bandwidthMeter A bandwidth meter which can be used by track selections to select tracks. */ - public final void init(InvalidationListener listener) { + public final void init(InvalidationListener listener, BandwidthMeter bandwidthMeter) { this.listener = listener; + this.bandwidthMeter = bandwidthMeter; } /** - * Performs a track selection for renderers. + * Called by the player to perform a track selection. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks * are to be selected. * @param trackGroups The available track groups. + * @param periodId The {@link MediaPeriodId} of the period for which tracks are to be selected. + * @param timeline The {@link Timeline} holding the period for which tracks are to be selected. * @return A {@link TrackSelectorResult} describing the track selections. * @throws ExoPlaybackException If an error occurs selecting tracks. */ - public abstract TrackSelectorResult selectTracks(RendererCapabilities[] rendererCapabilities, - TrackGroupArray trackGroups) throws ExoPlaybackException; + public abstract TrackSelectorResult selectTracks( + RendererCapabilities[] rendererCapabilities, + TrackGroupArray trackGroups, + MediaPeriodId periodId, + Timeline timeline) + throws ExoPlaybackException; /** - * Called when a {@link TrackSelectorResult} previously generated by - * {@link #selectTracks(RendererCapabilities[], TrackGroupArray)} is activated. + * Called by the player when a {@link TrackSelectorResult} previously generated by {@link + * #selectTracks(RendererCapabilities[], TrackGroupArray, MediaPeriodId, Timeline)} is activated. * - * @param info The value of {@link TrackSelectorResult#info} in the activated result. + * @param info The value of {@link TrackSelectorResult#info} in the activated selection. */ public abstract void onSelectionActivated(Object info); /** - * Invalidates all previously generated track selections. + * Calls {@link InvalidationListener#onTrackSelectionsInvalidated()} to invalidate all previously + * generated track selections. */ protected final void invalidate() { if (listener != null) { @@ -74,4 +147,11 @@ public abstract class TrackSelector { } } + /** + * Returns a bandwidth meter which can be used by track selections to select tracks. Must only be + * called after {@link #init(InvalidationListener, BandwidthMeter)} has been called. + */ + protected final BandwidthMeter getBandwidthMeter() { + return Assertions.checkNotNull(bandwidthMeter); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index 9c809acc83b0..9c005497cc49 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -15,21 +15,25 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererConfiguration; -import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * The result of a {@link TrackSelector} operation. */ public final class TrackSelectorResult { + /** The number of selections in the result. Greater than or equal to zero. */ + public final int length; /** - * The groups provided to the {@link TrackSelector}. + * A {@link RendererConfiguration} for each renderer. A null entry indicates the corresponding + * renderer should be disabled. */ - public final TrackGroupArray groups; + public final @NullableType RendererConfiguration[] rendererConfigurations; /** - * A {@link TrackSelectionArray} containing the selection for each renderer. + * A {@link TrackSelectionArray} containing the track selection for each renderer. */ public final TrackSelectionArray selections; /** @@ -37,36 +41,38 @@ public final class TrackSelectorResult { * should the selections be activated. */ public final Object info; - /** - * A {@link RendererConfiguration} for each renderer, to be used with the selections. - */ - public final RendererConfiguration[] rendererConfigurations; /** - * @param groups The groups provided to the {@link TrackSelector}. + * @param rendererConfigurations A {@link RendererConfiguration} for each renderer. A null entry + * indicates the corresponding renderer should be disabled. * @param selections A {@link TrackSelectionArray} containing the selection for each renderer. - * @param info An opaque object that will be returned to - * {@link TrackSelector#onSelectionActivated(Object)} should the selections be activated. - * @param rendererConfigurations A {@link RendererConfiguration} for each renderer, to be used - * with the selections. + * @param info An opaque object that will be returned to {@link + * TrackSelector#onSelectionActivated(Object)} should the selection be activated. */ - public TrackSelectorResult(TrackGroupArray groups, TrackSelectionArray selections, Object info, - RendererConfiguration[] rendererConfigurations) { - this.groups = groups; - this.selections = selections; - this.info = info; + public TrackSelectorResult( + @NullableType RendererConfiguration[] rendererConfigurations, + @NullableType TrackSelection[] selections, + Object info) { this.rendererConfigurations = rendererConfigurations; + this.selections = new TrackSelectionArray(selections); + this.info = info; + length = rendererConfigurations.length; + } + + /** Returns whether the renderer at the specified index is enabled. */ + public boolean isRendererEnabled(int index) { + return rendererConfigurations[index] != null; } /** * Returns whether this result is equivalent to {@code other} for all renderers. * * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false} - * will be returned in all cases. + * will be returned. * @return Whether this result is equivalent to {@code other} for all renderers. */ - public boolean isEquivalent(TrackSelectorResult other) { - if (other == null) { + public boolean isEquivalent(@Nullable TrackSelectorResult other) { + if (other == null || other.selections.length != selections.length) { return false; } for (int i = 0; i < selections.length; i++) { @@ -83,16 +89,17 @@ public final class TrackSelectorResult { * renderer. * * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false} - * will be returned in all cases. + * will be returned. * @param index The renderer index to check for equivalence. - * @return Whether this result is equivalent to {@code other} for all renderers. + * @return Whether this result is equivalent to {@code other} for the renderer at the specified + * index. */ - public boolean isEquivalent(TrackSelectorResult other, int index) { + public boolean isEquivalent(@Nullable TrackSelectorResult other, int index) { if (other == null) { return false; } - return Util.areEqual(selections.get(index), other.selections.get(index)) - && Util.areEqual(rendererConfigurations[index], other.rendererConfigurations[index]); + return Util.areEqual(rendererConfigurations[index], other.rendererConfigurations[index]) + && Util.areEqual(selections.get(index), other.selections.get(index)); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/package-info.java new file mode 100644 index 000000000000..4a04290d0f24 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocation.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocation.java index 6162adf3fd27..87dd142e6a2f 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocation.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocation.java @@ -25,29 +25,22 @@ public final class Allocation { /** * The array containing the allocated space. The allocated space might not be at the start of the - * array, and so {@link #translateOffset(int)} method must be used when indexing into it. + * array, and so {@link #offset} must be used when indexing into it. */ public final byte[] data; - private final int offset; + /** + * The offset of the allocated space in {@link #data}. + */ + public final int offset; /** * @param data The array containing the allocated space. - * @param offset The offset of the allocated space within the array. + * @param offset The offset of the allocated space in {@code data}. */ public Allocation(byte[] data, int offset) { this.data = data; this.offset = offset; } - /** - * Translates a zero-based offset into the allocation to the corresponding {@link #data} offset. - * - * @param offset The zero-based offset to translate. - * @return The corresponding offset in {@link #data}. - */ - public int translateOffset(int offset) { - return this.offset + offset; - } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/AssetDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/AssetDataSource.java index 6bebc14d00ce..70cd1de8fe1c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/AssetDataSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/AssetDataSource.java @@ -15,18 +15,20 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.content.Context; import android.content.res.AssetManager; import android.net.Uri; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; -/** - * A {@link DataSource} for reading from a local asset. - */ -public final class AssetDataSource implements DataSource { +/** A {@link DataSource} for reading from a local asset. */ +public final class AssetDataSource extends BaseDataSource { /** * Thrown when an {@link IOException} is encountered reading a local asset. @@ -40,39 +42,29 @@ public final class AssetDataSource implements DataSource { } private final AssetManager assetManager; - private final TransferListener listener; - private Uri uri; - private InputStream inputStream; + @Nullable private Uri uri; + @Nullable private InputStream inputStream; private long bytesRemaining; private boolean opened; - /** - * @param context A context. - */ + /** @param context A context. */ public AssetDataSource(Context context) { - this(context, null); - } - - /** - * @param context A context. - * @param listener An optional listener. - */ - public AssetDataSource(Context context, TransferListener listener) { + super(/* isNetwork= */ false); this.assetManager = context.getAssets(); - this.listener = listener; } @Override public long open(DataSpec dataSpec) throws AssetDataSourceException { try { uri = dataSpec.uri; - String path = uri.getPath(); + String path = Assertions.checkNotNull(uri.getPath()); if (path.startsWith("/android_asset/")) { path = path.substring(15); } else if (path.startsWith("/")) { path = path.substring(1); } + transferInitializing(dataSpec); inputStream = assetManager.open(path, AssetManager.ACCESS_RANDOM); long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) { @@ -96,9 +88,7 @@ public final class AssetDataSource implements DataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesRemaining; } @@ -114,7 +104,7 @@ public final class AssetDataSource implements DataSource { try { int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength : (int) Math.min(bytesRemaining, readLength); - bytesRead = inputStream.read(buffer, offset, bytesToRead); + bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead); } catch (IOException e) { throw new AssetDataSourceException(e); } @@ -129,13 +119,12 @@ public final class AssetDataSource implements DataSource { if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= bytesRead; } - if (listener != null) { - listener.onBytesTransferred(this, bytesRead); - } + bytesTransferred(bytesRead); return bytesRead; } @Override + @Nullable public Uri getUri() { return uri; } @@ -153,9 +142,7 @@ public final class AssetDataSource implements DataSource { inputStream = null; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BandwidthMeter.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BandwidthMeter.java index 5d4c1421dce0..5606b4570256 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BandwidthMeter.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BandwidthMeter.java @@ -15,6 +15,9 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; +import android.os.Handler; +import androidx.annotation.Nullable; + /** * Provides estimates of the currently available bandwidth. */ @@ -26,29 +29,43 @@ public interface BandwidthMeter { interface EventListener { /** - * Called periodically to indicate that bytes have been transferred. - *

- * Note: The estimated bitrate is typically derived from more information than just - * {@code bytes} and {@code elapsedMs}. + * Called periodically to indicate that bytes have been transferred or the estimated bitrate has + * changed. * - * @param elapsedMs The time taken to transfer the bytes, in milliseconds. - * @param bytes The number of bytes transferred. - * @param bitrate The estimated bitrate in bits/sec, or {@link #NO_ESTIMATE} if an estimate is - * not available. + *

Note: The estimated bitrate is typically derived from more information than just {@code + * bytes} and {@code elapsedMs}. + * + * @param elapsedMs The time taken to transfer {@code bytesTransferred}, in milliseconds. This + * is at most the elapsed time since the last callback, but may be less if there were + * periods during which data was not being transferred. + * @param bytesTransferred The number of bytes transferred since the last callback. + * @param bitrateEstimate The estimated bitrate in bits/sec. */ - void onBandwidthSample(int elapsedMs, long bytes, long bitrate); - + void onBandwidthSample(int elapsedMs, long bytesTransferred, long bitrateEstimate); } - /** - * Indicates no bandwidth estimate is available. - */ - long NO_ESTIMATE = -1; - - /** - * Returns the estimated bandwidth in bits/sec, or {@link #NO_ESTIMATE} if an estimate is not - * available. - */ + /** Returns the estimated bitrate. */ long getBitrateEstimate(); + /** + * Returns the {@link TransferListener} that this instance uses to gather bandwidth information + * from data transfers. May be null if the implementation does not listen to data transfers. + */ + @Nullable + TransferListener getTransferListener(); + + /** + * Adds an {@link EventListener}. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + */ + void addEventListener(Handler eventHandler, EventListener eventListener); + + /** + * Removes an {@link EventListener}. + * + * @param eventListener The listener to be removed. + */ + void removeEventListener(EventListener eventListener); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BaseDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BaseDataSource.java new file mode 100644 index 000000000000..38380949274e --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BaseDataSource.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import androidx.annotation.Nullable; +import java.util.ArrayList; + +/** + * Base {@link DataSource} implementation to keep a list of {@link TransferListener}s. + * + *

Subclasses must call {@link #transferInitializing(DataSpec)}, {@link + * #transferStarted(DataSpec)}, {@link #bytesTransferred(int)}, and {@link #transferEnded()} to + * inform listeners of data transfers. + */ +public abstract class BaseDataSource implements DataSource { + + private final boolean isNetwork; + private final ArrayList listeners; + + private int listenerCount; + @Nullable private DataSpec dataSpec; + + /** + * Creates base data source. + * + * @param isNetwork Whether the data source loads data through a network. + */ + protected BaseDataSource(boolean isNetwork) { + this.isNetwork = isNetwork; + this.listeners = new ArrayList<>(/* initialCapacity= */ 1); + } + + @Override + public final void addTransferListener(TransferListener transferListener) { + if (!listeners.contains(transferListener)) { + listeners.add(transferListener); + listenerCount++; + } + } + + /** + * Notifies listeners that data transfer for the specified {@link DataSpec} is being initialized. + * + * @param dataSpec {@link DataSpec} describing the data for initializing transfer. + */ + protected final void transferInitializing(DataSpec dataSpec) { + for (int i = 0; i < listenerCount; i++) { + listeners.get(i).onTransferInitializing(/* source= */ this, dataSpec, isNetwork); + } + } + + /** + * Notifies listeners that data transfer for the specified {@link DataSpec} started. + * + * @param dataSpec {@link DataSpec} describing the data being transferred. + */ + protected final void transferStarted(DataSpec dataSpec) { + this.dataSpec = dataSpec; + for (int i = 0; i < listenerCount; i++) { + listeners.get(i).onTransferStart(/* source= */ this, dataSpec, isNetwork); + } + } + + /** + * Notifies listeners that bytes were transferred. + * + * @param bytesTransferred The number of bytes transferred since the previous call to this method + * (or if the first call, since the transfer was started). + */ + protected final void bytesTransferred(int bytesTransferred) { + DataSpec dataSpec = castNonNull(this.dataSpec); + for (int i = 0; i < listenerCount; i++) { + listeners + .get(i) + .onBytesTransferred(/* source= */ this, dataSpec, isNetwork, bytesTransferred); + } + } + + /** Notifies listeners that a transfer ended. */ + protected final void transferEnded() { + DataSpec dataSpec = castNonNull(this.dataSpec); + for (int i = 0; i < listenerCount; i++) { + listeners.get(i).onTransferEnd(/* source= */ this, dataSpec, isNetwork); + } + this.dataSpec = null; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java index 99c1f49f900b..4aa66538ff07 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java @@ -15,20 +15,24 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.ByteArrayOutputStream; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link DataSink} for writing to a byte array. */ public final class ByteArrayDataSink implements DataSink { - private ByteArrayOutputStream stream; + private @MonotonicNonNull ByteArrayOutputStream stream; @Override - public void open(DataSpec dataSpec) throws IOException { + public void open(DataSpec dataSpec) { if (dataSpec.length == C.LENGTH_UNSET) { stream = new ByteArrayOutputStream(); } else { @@ -39,18 +43,19 @@ public final class ByteArrayDataSink implements DataSink { @Override public void close() throws IOException { - stream.close(); + castNonNull(stream).close(); } @Override - public void write(byte[] buffer, int offset, int length) throws IOException { - stream.write(buffer, offset, length); + public void write(byte[] buffer, int offset, int length) { + castNonNull(stream).write(buffer, offset, length); } /** * Returns the data written to the sink since the last call to {@link #open(DataSpec)}, or null if * {@link #open(DataSpec)} has never been called. */ + @Nullable public byte[] getData() { return stream == null ? null : stream.toByteArray(); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java index 20caaac47951..0be103701dd0 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java @@ -16,25 +16,26 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; import android.net.Uri; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.IOException; -/** - * A {@link DataSource} for reading from a byte array. - */ -public final class ByteArrayDataSource implements DataSource { +/** A {@link DataSource} for reading from a byte array. */ +public final class ByteArrayDataSource extends BaseDataSource { private final byte[] data; - private Uri uri; + @Nullable private Uri uri; private int readPosition; private int bytesRemaining; + private boolean opened; /** * @param data The data to be read. */ public ByteArrayDataSource(byte[] data) { + super(/* isNetwork= */ false); Assertions.checkNotNull(data); Assertions.checkArgument(data.length > 0); this.data = data; @@ -43,6 +44,7 @@ public final class ByteArrayDataSource implements DataSource { @Override public long open(DataSpec dataSpec) throws IOException { uri = dataSpec.uri; + transferInitializing(dataSpec); readPosition = (int) dataSpec.position; bytesRemaining = (int) ((dataSpec.length == C.LENGTH_UNSET) ? (data.length - dataSpec.position) : dataSpec.length); @@ -50,11 +52,13 @@ public final class ByteArrayDataSource implements DataSource { throw new IOException("Unsatisfiable range: [" + readPosition + ", " + dataSpec.length + "], length: " + data.length); } + opened = true; + transferStarted(dataSpec); return bytesRemaining; } @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { + public int read(byte[] buffer, int offset, int readLength) { if (readLength == 0) { return 0; } else if (bytesRemaining == 0) { @@ -65,16 +69,22 @@ public final class ByteArrayDataSource implements DataSource { System.arraycopy(data, readPosition, buffer, offset, readLength); readPosition += readLength; bytesRemaining -= readLength; + bytesTransferred(readLength); return readLength; } @Override + @Nullable public Uri getUri() { return uri; } @Override - public void close() throws IOException { + public void close() { + if (opened) { + opened = false; + transferEnded(); + } uri = null; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ContentDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ContentDataSource.java index 9fb2ed7ecca1..b73d9d6375f6 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -15,20 +15,22 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.net.Uri; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import java.io.EOFException; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; +import java.nio.channels.FileChannel; -/** - * A {@link DataSource} for reading from a content URI. - */ -public final class ContentDataSource implements DataSource { +/** A {@link DataSource} for reading from a content URI. */ +public final class ContentDataSource extends BaseDataSource { /** * Thrown when an {@link IOException} is encountered reading from a content URI. @@ -42,11 +44,10 @@ public final class ContentDataSource implements DataSource { } private final ContentResolver resolver; - private final TransferListener listener; - private Uri uri; - private AssetFileDescriptor assetFileDescriptor; - private InputStream inputStream; + @Nullable private Uri uri; + @Nullable private AssetFileDescriptor assetFileDescriptor; + @Nullable private FileInputStream inputStream; private long bytesRemaining; private boolean opened; @@ -54,26 +55,28 @@ public final class ContentDataSource implements DataSource { * @param context A context. */ public ContentDataSource(Context context) { - this(context, null); - } - - /** - * @param context A context. - * @param listener An optional listener. - */ - public ContentDataSource(Context context, TransferListener listener) { + super(/* isNetwork= */ false); this.resolver = context.getContentResolver(); - this.listener = listener; } @Override public long open(DataSpec dataSpec) throws ContentDataSourceException { try { - uri = dataSpec.uri; - assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r"); - inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); - long skipped = inputStream.skip(dataSpec.position); - if (skipped < dataSpec.position) { + Uri uri = dataSpec.uri; + this.uri = uri; + + transferInitializing(dataSpec); + AssetFileDescriptor assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r"); + this.assetFileDescriptor = assetFileDescriptor; + if (assetFileDescriptor == null) { + throw new FileNotFoundException("Could not open file descriptor for: " + uri); + } + FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + this.inputStream = inputStream; + + long assetStartOffset = assetFileDescriptor.getStartOffset(); + long skipped = inputStream.skip(assetStartOffset + dataSpec.position) - assetStartOffset; + if (skipped != dataSpec.position) { // We expect the skip to be satisfied in full. If it isn't then we're probably trying to // skip beyond the end of the data. throw new EOFException(); @@ -81,12 +84,15 @@ public final class ContentDataSource implements DataSource { if (dataSpec.length != C.LENGTH_UNSET) { bytesRemaining = dataSpec.length; } else { - bytesRemaining = inputStream.available(); - if (bytesRemaining == 0) { - // FileInputStream.available() returns 0 if the remaining length cannot be determined, or - // if it's greater than Integer.MAX_VALUE. We don't know the true length in either case, - // so treat as unbounded. - bytesRemaining = C.LENGTH_UNSET; + long assetFileDescriptorLength = assetFileDescriptor.getLength(); + if (assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH) { + // The asset must extend to the end of the file. If FileInputStream.getChannel().size() + // returns 0 then the remaining length cannot be determined. + FileChannel channel = inputStream.getChannel(); + long channelSize = channel.size(); + bytesRemaining = channelSize == 0 ? C.LENGTH_UNSET : channelSize - channel.position(); + } else { + bytesRemaining = assetFileDescriptorLength - skipped; } } } catch (IOException e) { @@ -94,9 +100,7 @@ public final class ContentDataSource implements DataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesRemaining; } @@ -113,7 +117,7 @@ public final class ContentDataSource implements DataSource { try { int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength : (int) Math.min(bytesRemaining, readLength); - bytesRead = inputStream.read(buffer, offset, bytesToRead); + bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead); } catch (IOException e) { throw new ContentDataSourceException(e); } @@ -128,17 +132,17 @@ public final class ContentDataSource implements DataSource { if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= bytesRead; } - if (listener != null) { - listener.onBytesTransferred(this, bytesRead); - } + bytesTransferred(bytesRead); return bytesRead; } @Override + @Nullable public Uri getUri() { return uri; } + @SuppressWarnings("Finally") @Override public void close() throws ContentDataSourceException { uri = null; @@ -160,9 +164,7 @@ public final class ContentDataSource implements DataSource { assetFileDescriptor = null; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java new file mode 100644 index 000000000000..57420250ac89 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import android.net.Uri; +import android.util.Base64; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.net.URLDecoder; + +/** A {@link DataSource} for reading data URLs, as defined by RFC 2397. */ +public final class DataSchemeDataSource extends BaseDataSource { + + public static final String SCHEME_DATA = "data"; + + @Nullable private DataSpec dataSpec; + @Nullable private byte[] data; + private int endPosition; + private int readPosition; + + // the constructor does not initialize fields: data + @SuppressWarnings("nullness:initialization.fields.uninitialized") + public DataSchemeDataSource() { + super(/* isNetwork= */ false); + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + transferInitializing(dataSpec); + this.dataSpec = dataSpec; + readPosition = (int) dataSpec.position; + Uri uri = dataSpec.uri; + String scheme = uri.getScheme(); + if (!SCHEME_DATA.equals(scheme)) { + throw new ParserException("Unsupported scheme: " + scheme); + } + String[] uriParts = Util.split(uri.getSchemeSpecificPart(), ","); + if (uriParts.length != 2) { + throw new ParserException("Unexpected URI format: " + uri); + } + String dataString = uriParts[1]; + if (uriParts[0].contains(";base64")) { + try { + data = Base64.decode(dataString, 0); + } catch (IllegalArgumentException e) { + throw new ParserException("Error while parsing Base64 encoded string: " + dataString, e); + } + } else { + // TODO: Add support for other charsets. + data = Util.getUtf8Bytes(URLDecoder.decode(dataString, C.ASCII_NAME)); + } + endPosition = + dataSpec.length != C.LENGTH_UNSET ? (int) dataSpec.length + readPosition : data.length; + if (endPosition > data.length || readPosition > endPosition) { + data = null; + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); + } + transferStarted(dataSpec); + return (long) endPosition - readPosition; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) { + if (readLength == 0) { + return 0; + } + int remainingBytes = endPosition - readPosition; + if (remainingBytes == 0) { + return C.RESULT_END_OF_INPUT; + } + readLength = Math.min(readLength, remainingBytes); + System.arraycopy(castNonNull(data), readPosition, buffer, offset, readLength); + readPosition += readLength; + bytesTransferred(readLength); + return readLength; + } + + @Override + @Nullable + public Uri getUri() { + return dataSpec != null ? dataSpec.uri : null; + } + + @Override + public void close() { + if (data != null) { + data = null; + transferEnded(); + } + dataSpec = null; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSink.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSink.java index 853638464c08..c85ec8cfca08 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSink.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSink.java @@ -37,6 +37,9 @@ public interface DataSink { /** * Opens the sink to consume the specified data. * + *

Note: If an {@link IOException} is thrown, callers must still call {@link #close()} to + * ensure that any partial effects of the invocation are cleaned up. + * * @param dataSpec Defines the data to be consumed. * @throws IOException If an error occurs opening the sink. */ @@ -55,8 +58,10 @@ public interface DataSink { /** * Closes the sink. * + *

Note: This method must be called even if the corresponding call to {@link #open(DataSpec)} + * threw an {@link IOException}. See {@link #open(DataSpec)} for more details. + * * @throws IOException If an error occurs closing the sink. */ void close() throws IOException; - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSource.java index 9a6618e70a94..26529253f898 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSource.java @@ -16,8 +16,12 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; import android.net.Uri; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; /** * A component from which streams of data can be read. @@ -33,9 +37,15 @@ public interface DataSource { * Creates a {@link DataSource} instance. */ DataSource createDataSource(); - } + /** + * Adds a {@link TransferListener} to listen to data transfers. This method is not thread-safe. + * + * @param transferListener A {@link TransferListener}. + */ + void addTransferListener(TransferListener transferListener); + /** * Opens the source to read the specified data. *

@@ -54,11 +64,11 @@ public interface DataSource { long open(DataSpec dataSpec) throws IOException; /** - * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at + * Reads up to {@code readLength} bytes of data and stores them into {@code buffer}, starting at * index {@code offset}. - *

- * If {@code length} is zero then 0 is returned. Otherwise, if no data is available because the - * end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is returned. + * + *

If {@code readLength} is zero then 0 is returned. Otherwise, if no data is available because + * the end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is returned. * Otherwise, the call will block until at least one byte of data has been read and the number of * bytes read is returned. * @@ -79,7 +89,15 @@ public interface DataSource { * * @return The {@link Uri} from which data is being read, or null if the source is not open. */ - Uri getUri(); + @Nullable Uri getUri(); + + /** + * When the source is open, returns the response headers associated with the last {@link #open} + * call. Otherwise, returns an empty map. + */ + default Map> getResponseHeaders() { + return Collections.emptyMap(); + } /** * Closes the source. @@ -90,5 +108,4 @@ public interface DataSource { * @throws IOException If an error occurs closing the source. */ void close() throws IOException; - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceInputStream.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceInputStream.java index 6933e61a259f..c25ba4c10acd 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceInputStream.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceInputStream.java @@ -15,7 +15,7 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.IOException; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSpec.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSpec.java index 0d8eba044dcb..6a419c663261 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSpec.java @@ -16,12 +16,17 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; import android.net.Uri; -import android.support.annotation.IntDef; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; /** * Defines a region of data. @@ -29,46 +34,78 @@ import java.util.Arrays; public final class DataSpec { /** - * The flags that apply to any request for data. + * The flags that apply to any request for data. Possible flag values are {@link + * #FLAG_ALLOW_GZIP}, {@link #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN} and {@link + * #FLAG_ALLOW_CACHE_FRAGMENTATION}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_ALLOW_GZIP, FLAG_ALLOW_CACHING_UNKNOWN_LENGTH}) + @IntDef( + flag = true, + value = {FLAG_ALLOW_GZIP, FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN, FLAG_ALLOW_CACHE_FRAGMENTATION}) public @interface Flags {} /** - * Permits an underlying network stack to request that the server use gzip compression. - *

- * Should not typically be set if the data being requested is already compressed (e.g. most audio - * and video requests). May be set when requesting other data. - *

- * When a {@link DataSource} is used to request data with this flag set, and if the - * {@link DataSource} does make a network request, then the value returned from - * {@link DataSource#open(DataSpec)} will typically be {@link C#LENGTH_UNSET}. The data read from - * {@link DataSource#read(byte[], int, int)} will be the decompressed data. + * Allows an underlying network stack to request that the server use gzip compression. + * + *

Should not typically be set if the data being requested is already compressed (e.g. most + * audio and video requests). May be set when requesting other data. + * + *

When a {@link DataSource} is used to request data with this flag set, and if the {@link + * DataSource} does make a network request, then the value returned from {@link + * DataSource#open(DataSpec)} will typically be {@link C#LENGTH_UNSET}. The data read from {@link + * DataSource#read(byte[], int, int)} will be the decompressed data. */ - public static final int FLAG_ALLOW_GZIP = 1 << 0; + public static final int FLAG_ALLOW_GZIP = 1; + /** Prevents caching if the length cannot be resolved when the {@link DataSource} is opened. */ + public static final int FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN = 1 << 1; // 2 + /** + * Allows fragmentation of this request into multiple cache files, meaning a cache eviction policy + * will be able to evict individual fragments of the data. Depending on the cache implementation, + * setting this flag may also enable more concurrent access to the data (e.g. reading one fragment + * whilst writing another). + */ + public static final int FLAG_ALLOW_CACHE_FRAGMENTATION = 1 << 2; // 4 /** - * Permits content to be cached even if its length can not be resolved. + * The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. One of {@link + * #HTTP_METHOD_GET}, {@link #HTTP_METHOD_POST} or {@link #HTTP_METHOD_HEAD}. */ - public static final int FLAG_ALLOW_CACHING_UNKNOWN_LENGTH = 1 << 1; + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({HTTP_METHOD_GET, HTTP_METHOD_POST, HTTP_METHOD_HEAD}) + public @interface HttpMethod {} + + public static final int HTTP_METHOD_GET = 1; + public static final int HTTP_METHOD_POST = 2; + public static final int HTTP_METHOD_HEAD = 3; /** * The source from which data should be read. */ public final Uri uri; + /** - * Body for a POST request, null otherwise. + * The HTTP method, which will be used by {@link HttpDataSource} when requesting this DataSpec. + * This value will be ignored by non-http {@link DataSource}s. */ - public final byte[] postBody; + public final @HttpMethod int httpMethod; + /** - * The absolute position of the data in the full stream. + * The HTTP request body, null otherwise. If the body is non-null, then httpBody.length will be + * non-zero. */ + @Nullable public final byte[] httpBody; + + /** Immutable map containing the headers to use in HTTP requests. */ + public final Map httpRequestHeaders; + + /** The absolute position of the data in the full stream. */ public final long absoluteStreamPosition; /** * The position of the data when read from {@link #uri}. *

* Always equal to {@link #absoluteStreamPosition} unless the {@link #uri} defines the location - * of a subset of the underyling data. + * of a subset of the underlying data. */ public final long position; /** @@ -77,17 +114,14 @@ public final class DataSpec { public final long length; /** * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the - * {@link DataSpec} is not intended to be used in conjunction with a cache. + * data spec is not intended to be used in conjunction with a cache. */ - public final String key; - /** - * Request flags. Currently {@link #FLAG_ALLOW_GZIP} and - * {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags. - */ - @Flags public final int flags; + @Nullable public final String key; + /** Request {@link Flags flags}. */ + public final @Flags int flags; /** - * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. + * Construct a data spec for the given uri and with {@link #key} set to null. * * @param uri {@link #uri}. */ @@ -96,7 +130,7 @@ public final class DataSpec { } /** - * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. + * Construct a data spec for the given uri and with {@link #key} set to null. * * @param uri {@link #uri}. * @param flags {@link #flags}. @@ -106,19 +140,19 @@ public final class DataSpec { } /** - * Construct a {@link DataSpec} where {@link #position} equals {@link #absoluteStreamPosition}. + * Construct a data spec where {@link #position} equals {@link #absoluteStreamPosition}. * * @param uri {@link #uri}. * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}. * @param length {@link #length}. * @param key {@link #key}. */ - public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key) { + public DataSpec(Uri uri, long absoluteStreamPosition, long length, @Nullable String key) { this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, 0); } /** - * Construct a {@link DataSpec} where {@link #position} equals {@link #absoluteStreamPosition}. + * Construct a data spec where {@link #position} equals {@link #absoluteStreamPosition}. * * @param uri {@link #uri}. * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}. @@ -126,13 +160,43 @@ public final class DataSpec { * @param key {@link #key}. * @param flags {@link #flags}. */ - public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, @Flags int flags) { + public DataSpec( + Uri uri, long absoluteStreamPosition, long length, @Nullable String key, @Flags int flags) { this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags); } /** - * Construct a {@link DataSpec} where {@link #position} may differ from - * {@link #absoluteStreamPosition}. + * Construct a data spec where {@link #position} equals {@link #absoluteStreamPosition} and has + * request headers. + * + * @param uri {@link #uri}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + * @param httpRequestHeaders {@link #httpRequestHeaders} + */ + public DataSpec( + Uri uri, + long absoluteStreamPosition, + long length, + @Nullable String key, + @Flags int flags, + Map httpRequestHeaders) { + this( + uri, + inferHttpMethod(null), + null, + absoluteStreamPosition, + absoluteStreamPosition, + length, + key, + flags, + httpRequestHeaders); + } + + /** + * Construct a data spec where {@link #position} may differ from {@link #absoluteStreamPosition}. * * @param uri {@link #uri}. * @param absoluteStreamPosition {@link #absoluteStreamPosition}. @@ -141,35 +205,117 @@ public final class DataSpec { * @param key {@link #key}. * @param flags {@link #flags}. */ - public DataSpec(Uri uri, long absoluteStreamPosition, long position, long length, String key, + public DataSpec( + Uri uri, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, @Flags int flags) { this(uri, null, absoluteStreamPosition, position, length, key, flags); } /** - * Construct a {@link DataSpec} where {@link #position} may differ from - * {@link #absoluteStreamPosition}. + * Construct a data spec by inferring the {@link #httpMethod} based on the {@code postBody} + * parameter. If postBody is non-null, then httpMethod is set to {@link #HTTP_METHOD_POST}. If + * postBody is null, then httpMethod is set to {@link #HTTP_METHOD_GET}. * * @param uri {@link #uri}. - * @param postBody {@link #postBody}. + * @param postBody {@link #httpBody} The body of the HTTP request, which is also used to infer the + * {@link #httpMethod}. * @param absoluteStreamPosition {@link #absoluteStreamPosition}. * @param position {@link #position}. * @param length {@link #length}. * @param key {@link #key}. * @param flags {@link #flags}. */ - public DataSpec(Uri uri, byte[] postBody, long absoluteStreamPosition, long position, long length, - String key, @Flags int flags) { + public DataSpec( + Uri uri, + @Nullable byte[] postBody, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, + @Flags int flags) { + this( + uri, + /* httpMethod= */ inferHttpMethod(postBody), + /* httpBody= */ postBody, + absoluteStreamPosition, + position, + length, + key, + flags); + } + + /** + * Construct a data spec where {@link #position} may differ from {@link #absoluteStreamPosition}. + * + * @param uri {@link #uri}. + * @param httpMethod {@link #httpMethod}. + * @param httpBody {@link #httpBody}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + */ + public DataSpec( + Uri uri, + @HttpMethod int httpMethod, + @Nullable byte[] httpBody, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, + @Flags int flags) { + this( + uri, + httpMethod, + httpBody, + absoluteStreamPosition, + position, + length, + key, + flags, + /* httpRequestHeaders= */ Collections.emptyMap()); + } + + /** + * Construct a data spec with request parameters to be used as HTTP headers inside HTTP requests. + * + * @param uri {@link #uri}. + * @param httpMethod {@link #httpMethod}. + * @param httpBody {@link #httpBody}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + * @param httpRequestHeaders {@link #httpRequestHeaders}. + */ + public DataSpec( + Uri uri, + @HttpMethod int httpMethod, + @Nullable byte[] httpBody, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, + @Flags int flags, + Map httpRequestHeaders) { Assertions.checkArgument(absoluteStreamPosition >= 0); Assertions.checkArgument(position >= 0); Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET); this.uri = uri; - this.postBody = postBody; + this.httpMethod = httpMethod; + this.httpBody = (httpBody != null && httpBody.length != 0) ? httpBody : null; this.absoluteStreamPosition = absoluteStreamPosition; this.position = position; this.length = length; this.key = key; this.flags = flags; + this.httpRequestHeaders = Collections.unmodifiableMap(new HashMap<>(httpRequestHeaders)); } /** @@ -183,8 +329,150 @@ public final class DataSpec { @Override public String toString() { - return "DataSpec[" + uri + ", " + Arrays.toString(postBody) + ", " + absoluteStreamPosition - + ", " + position + ", " + length + ", " + key + ", " + flags + "]"; + return "DataSpec[" + + getHttpMethodString() + + " " + + uri + + ", " + + Arrays.toString(httpBody) + + ", " + + absoluteStreamPosition + + ", " + + position + + ", " + + length + + ", " + + key + + ", " + + flags + + "]"; } + /** + * Returns an uppercase HTTP method name (e.g., "GET", "POST", "HEAD") corresponding to the {@link + * #httpMethod}. + */ + public final String getHttpMethodString() { + return getStringForHttpMethod(httpMethod); + } + + /** + * Returns an uppercase HTTP method name (e.g., "GET", "POST", "HEAD") corresponding to the {@code + * httpMethod}. + */ + public static String getStringForHttpMethod(@HttpMethod int httpMethod) { + switch (httpMethod) { + case HTTP_METHOD_GET: + return "GET"; + case HTTP_METHOD_POST: + return "POST"; + case HTTP_METHOD_HEAD: + return "HEAD"; + default: + throw new AssertionError(httpMethod); + } + } + + /** + * Returns a data spec that represents a subrange of the data defined by this DataSpec. The + * subrange includes data from the offset up to the end of this DataSpec. + * + * @param offset The offset of the subrange. + * @return A data spec that represents a subrange of the data defined by this DataSpec. + */ + public DataSpec subrange(long offset) { + return subrange(offset, length == C.LENGTH_UNSET ? C.LENGTH_UNSET : length - offset); + } + + /** + * Returns a data spec that represents a subrange of the data defined by this DataSpec. + * + * @param offset The offset of the subrange. + * @param length The length of the subrange. + * @return A data spec that represents a subrange of the data defined by this DataSpec. + */ + public DataSpec subrange(long offset, long length) { + if (offset == 0 && this.length == length) { + return this; + } else { + return new DataSpec( + uri, + httpMethod, + httpBody, + absoluteStreamPosition + offset, + position + offset, + length, + key, + flags, + httpRequestHeaders); + } + } + + /** + * Returns a copy of this data spec with the specified Uri. + * + * @param uri The new source {@link Uri}. + * @return The copied data spec with the specified Uri. + */ + public DataSpec withUri(Uri uri) { + return new DataSpec( + uri, + httpMethod, + httpBody, + absoluteStreamPosition, + position, + length, + key, + flags, + httpRequestHeaders); + } + + /** + * Returns a copy of this data spec with the specified request headers. + * + * @param requestHeaders The HTTP request headers. + * @return The copied data spec with the specified request headers. + */ + public DataSpec withRequestHeaders(Map requestHeaders) { + return new DataSpec( + uri, + httpMethod, + httpBody, + absoluteStreamPosition, + position, + length, + key, + flags, + requestHeaders); + } + + /** + * Returns a copy this data spec with additional request headers. + * + *

Note: Values in {@code requestHeaders} will overwrite values with the same header key that + * were previously set in this instance's {@code #httpRequestHeaders}. + * + * @param requestHeaders The additional HTTP request headers. + * @return The copied data with the additional HTTP request headers. + */ + public DataSpec withAdditionalHeaders(Map requestHeaders) { + Map totalHeaders = new HashMap<>(this.httpRequestHeaders); + totalHeaders.putAll(requestHeaders); + + return new DataSpec( + uri, + httpMethod, + httpBody, + absoluteStreamPosition, + position, + length, + key, + flags, + totalHeaders); + } + + @HttpMethod + private static int inferHttpMethod(@Nullable byte[] postBody) { + return postBody != null ? HTTP_METHOD_POST : HTTP_METHOD_GET; + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultAllocator.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultAllocator.java index d5dc2f93c6ba..b12efcbe4ec8 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultAllocator.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultAllocator.java @@ -117,9 +117,6 @@ public final class DefaultAllocator implements Allocator { Math.max(availableAllocations.length * 2, availableCount + allocations.length)); } for (Allocation allocation : allocations) { - // Weak sanity check that the allocation probably originated from this pool. - Assertions.checkArgument(allocation.data == initialAllocationBlock - || allocation.data.length == individualAllocationSize); availableAllocations[availableCount++] = allocation; } allocatedCount -= allocations.length; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java index 30c1cb78f31f..63ca7c7eac56 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java @@ -15,50 +15,288 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; import android.os.Handler; -import android.os.SystemClock; +import android.os.Looper; +import android.util.SparseArray; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.EventDispatcher; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.SlidingPercentile; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** - * Estimates bandwidth by listening to data transfers. The bandwidth estimate is calculated using - * a {@link SlidingPercentile} and is updated each time a transfer ends. + * Estimates bandwidth by listening to data transfers. + * + *

The bandwidth estimate is calculated using a {@link SlidingPercentile} and is updated each + * time a transfer ends. The initial estimate is based on the current operator's network country + * code or the locale of the user, as well as the network connection type. This can be configured in + * the {@link Builder}. */ -public final class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { +public final class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { /** - * The default maximum weight for the sliding window. + * Country groups used to determine the default initial bitrate estimate. The group assignment for + * each country is an array of group indices for [Wifi, 2G, 3G, 4G]. */ - public static final int DEFAULT_MAX_WEIGHT = 2000; + public static final Map DEFAULT_INITIAL_BITRATE_COUNTRY_GROUPS = + createInitialBitrateCountryGroupAssignment(); + + /** Default initial Wifi bitrate estimate in bits per second. */ + public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI = + new long[] {5_700_000, 3_500_000, 2_000_000, 1_100_000, 470_000}; + + /** Default initial 2G bitrate estimates in bits per second. */ + public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_2G = + new long[] {200_000, 148_000, 132_000, 115_000, 95_000}; + + /** Default initial 3G bitrate estimates in bits per second. */ + public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_3G = + new long[] {2_200_000, 1_300_000, 970_000, 810_000, 490_000}; + + /** Default initial 4G bitrate estimates in bits per second. */ + public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_4G = + new long[] {5_300_000, 3_200_000, 2_000_000, 1_400_000, 690_000}; + + /** + * Default initial bitrate estimate used when the device is offline or the network type cannot be + * determined, in bits per second. + */ + public static final long DEFAULT_INITIAL_BITRATE_ESTIMATE = 1_000_000; + + /** Default maximum weight for the sliding window. */ + public static final int DEFAULT_SLIDING_WINDOW_MAX_WEIGHT = 2000; + + @Nullable private static DefaultBandwidthMeter singletonInstance; + + /** Builder for a bandwidth meter. */ + public static final class Builder { + + @Nullable private final Context context; + + private SparseArray initialBitrateEstimates; + private int slidingWindowMaxWeight; + private Clock clock; + private boolean resetOnNetworkTypeChange; + + /** + * Creates a builder with default parameters and without listener. + * + * @param context A context. + */ + public Builder(Context context) { + // Handling of null is for backward compatibility only. + this.context = context == null ? null : context.getApplicationContext(); + initialBitrateEstimates = getInitialBitrateEstimatesForCountry(Util.getCountryCode(context)); + slidingWindowMaxWeight = DEFAULT_SLIDING_WINDOW_MAX_WEIGHT; + clock = Clock.DEFAULT; + resetOnNetworkTypeChange = true; + } + + /** + * Sets the maximum weight for the sliding window. + * + * @param slidingWindowMaxWeight The maximum weight for the sliding window. + * @return This builder. + */ + public Builder setSlidingWindowMaxWeight(int slidingWindowMaxWeight) { + this.slidingWindowMaxWeight = slidingWindowMaxWeight; + return this; + } + + /** + * Sets the initial bitrate estimate in bits per second that should be assumed when a bandwidth + * estimate is unavailable. + * + * @param initialBitrateEstimate The initial bitrate estimate in bits per second. + * @return This builder. + */ + public Builder setInitialBitrateEstimate(long initialBitrateEstimate) { + for (int i = 0; i < initialBitrateEstimates.size(); i++) { + initialBitrateEstimates.setValueAt(i, initialBitrateEstimate); + } + return this; + } + + /** + * Sets the initial bitrate estimate in bits per second that should be assumed when a bandwidth + * estimate is unavailable and the current network connection is of the specified type. + * + * @param networkType The {@link C.NetworkType} this initial estimate is for. + * @param initialBitrateEstimate The initial bitrate estimate in bits per second. + * @return This builder. + */ + public Builder setInitialBitrateEstimate( + @C.NetworkType int networkType, long initialBitrateEstimate) { + initialBitrateEstimates.put(networkType, initialBitrateEstimate); + return this; + } + + /** + * Sets the initial bitrate estimates to the default values of the specified country. The + * initial estimates are used when a bandwidth estimate is unavailable. + * + * @param countryCode The ISO 3166-1 alpha-2 country code of the country whose default bitrate + * estimates should be used. + * @return This builder. + */ + public Builder setInitialBitrateEstimate(String countryCode) { + initialBitrateEstimates = + getInitialBitrateEstimatesForCountry(Util.toUpperInvariant(countryCode)); + return this; + } + + /** + * Sets the clock used to estimate bandwidth from data transfers. Should only be set for testing + * purposes. + * + * @param clock The clock used to estimate bandwidth from data transfers. + * @return This builder. + */ + public Builder setClock(Clock clock) { + this.clock = clock; + return this; + } + + /** + * Sets whether to reset if the network type changes. The default value is {@code true}. + * + * @param resetOnNetworkTypeChange Whether to reset if the network type changes. + * @return This builder. + */ + public Builder setResetOnNetworkTypeChange(boolean resetOnNetworkTypeChange) { + this.resetOnNetworkTypeChange = resetOnNetworkTypeChange; + return this; + } + + /** + * Builds the bandwidth meter. + * + * @return A bandwidth meter with the configured properties. + */ + public DefaultBandwidthMeter build() { + return new DefaultBandwidthMeter( + context, + initialBitrateEstimates, + slidingWindowMaxWeight, + clock, + resetOnNetworkTypeChange); + } + + private static SparseArray getInitialBitrateEstimatesForCountry(String countryCode) { + int[] groupIndices = getCountryGroupIndices(countryCode); + SparseArray result = new SparseArray<>(/* initialCapacity= */ 6); + result.append(C.NETWORK_TYPE_UNKNOWN, DEFAULT_INITIAL_BITRATE_ESTIMATE); + result.append(C.NETWORK_TYPE_WIFI, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]); + result.append(C.NETWORK_TYPE_2G, DEFAULT_INITIAL_BITRATE_ESTIMATES_2G[groupIndices[1]]); + result.append(C.NETWORK_TYPE_3G, DEFAULT_INITIAL_BITRATE_ESTIMATES_3G[groupIndices[2]]); + result.append(C.NETWORK_TYPE_4G, DEFAULT_INITIAL_BITRATE_ESTIMATES_4G[groupIndices[3]]); + // Assume default Wifi bitrate for Ethernet and 5G to prevent using the slower fallback. + result.append( + C.NETWORK_TYPE_ETHERNET, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]); + result.append(C.NETWORK_TYPE_5G, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]); + return result; + } + + private static int[] getCountryGroupIndices(String countryCode) { + int[] groupIndices = DEFAULT_INITIAL_BITRATE_COUNTRY_GROUPS.get(countryCode); + // Assume median group if not found. + return groupIndices == null ? new int[] {2, 2, 2, 2} : groupIndices; + } + } + + /** + * Returns a singleton instance of a {@link DefaultBandwidthMeter} with default configuration. + * + * @param context A {@link Context}. + * @return The singleton instance. + */ + public static synchronized DefaultBandwidthMeter getSingletonInstance(Context context) { + if (singletonInstance == null) { + singletonInstance = new DefaultBandwidthMeter.Builder(context).build(); + } + return singletonInstance; + } private static final int ELAPSED_MILLIS_FOR_ESTIMATE = 2000; private static final int BYTES_TRANSFERRED_FOR_ESTIMATE = 512 * 1024; - private final Handler eventHandler; - private final EventListener eventListener; + @Nullable private final Context context; + private final SparseArray initialBitrateEstimates; + private final EventDispatcher eventDispatcher; private final SlidingPercentile slidingPercentile; + private final Clock clock; private int streamCount; private long sampleStartTimeMs; private long sampleBytesTransferred; + @C.NetworkType private int networkType; private long totalElapsedTimeMs; private long totalBytesTransferred; private long bitrateEstimate; + private long lastReportedBitrateEstimate; + private boolean networkTypeOverrideSet; + @C.NetworkType private int networkTypeOverride; + + /** @deprecated Use {@link Builder} instead. */ + @Deprecated public DefaultBandwidthMeter() { - this(null, null); + this( + /* context= */ null, + /* initialBitrateEstimates= */ new SparseArray<>(), + DEFAULT_SLIDING_WINDOW_MAX_WEIGHT, + Clock.DEFAULT, + /* resetOnNetworkTypeChange= */ false); } - public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener) { - this(eventHandler, eventListener, DEFAULT_MAX_WEIGHT); - } - - public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, int maxWeight) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; + private DefaultBandwidthMeter( + @Nullable Context context, + SparseArray initialBitrateEstimates, + int maxWeight, + Clock clock, + boolean resetOnNetworkTypeChange) { + this.context = context == null ? null : context.getApplicationContext(); + this.initialBitrateEstimates = initialBitrateEstimates; + this.eventDispatcher = new EventDispatcher<>(); this.slidingPercentile = new SlidingPercentile(maxWeight); - bitrateEstimate = NO_ESTIMATE; + this.clock = clock; + // Set the initial network type and bitrate estimate + networkType = context == null ? C.NETWORK_TYPE_UNKNOWN : Util.getNetworkType(context); + bitrateEstimate = getInitialBitrateEstimateForNetworkType(networkType); + // Register to receive connectivity actions if possible. + if (context != null && resetOnNetworkTypeChange) { + ConnectivityActionReceiver connectivityActionReceiver = + ConnectivityActionReceiver.getInstance(context); + connectivityActionReceiver.register(/* bandwidthMeter= */ this); + } + } + + /** + * Overrides the network type. Handled in the same way as if the meter had detected a change from + * the current network type to the specified network type internally. + * + *

Applications should not normally call this method. It is intended for testing purposes. + * + * @param networkType The overriding network type. + */ + public synchronized void setNetworkTypeOverride(@C.NetworkType int networkType) { + networkTypeOverride = networkType; + networkTypeOverrideSet = true; + onConnectivityAction(); } @Override @@ -67,51 +305,427 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList } @Override - public synchronized void onTransferStart(Object source, DataSpec dataSpec) { + @Nullable + public TransferListener getTransferListener() { + return this; + } + + @Override + public void addEventListener(Handler eventHandler, EventListener eventListener) { + eventDispatcher.addListener(eventHandler, eventListener); + } + + @Override + public void removeEventListener(EventListener eventListener) { + eventDispatcher.removeListener(eventListener); + } + + @Override + public void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork) { + // Do nothing. + } + + @Override + public synchronized void onTransferStart( + DataSource source, DataSpec dataSpec, boolean isNetwork) { + if (!isNetwork) { + return; + } if (streamCount == 0) { - sampleStartTimeMs = SystemClock.elapsedRealtime(); + sampleStartTimeMs = clock.elapsedRealtime(); } streamCount++; } @Override - public synchronized void onBytesTransferred(Object source, int bytes) { + public synchronized void onBytesTransferred( + DataSource source, DataSpec dataSpec, boolean isNetwork, int bytes) { + if (!isNetwork) { + return; + } sampleBytesTransferred += bytes; } @Override - public synchronized void onTransferEnd(Object source) { + public synchronized void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork) { + if (!isNetwork) { + return; + } Assertions.checkState(streamCount > 0); - long nowMs = SystemClock.elapsedRealtime(); + long nowMs = clock.elapsedRealtime(); int sampleElapsedTimeMs = (int) (nowMs - sampleStartTimeMs); totalElapsedTimeMs += sampleElapsedTimeMs; totalBytesTransferred += sampleBytesTransferred; if (sampleElapsedTimeMs > 0) { - float bitsPerSecond = (sampleBytesTransferred * 8000) / sampleElapsedTimeMs; + float bitsPerSecond = (sampleBytesTransferred * 8000f) / sampleElapsedTimeMs; slidingPercentile.addSample((int) Math.sqrt(sampleBytesTransferred), bitsPerSecond); if (totalElapsedTimeMs >= ELAPSED_MILLIS_FOR_ESTIMATE || totalBytesTransferred >= BYTES_TRANSFERRED_FOR_ESTIMATE) { - float bitrateEstimateFloat = slidingPercentile.getPercentile(0.5f); - bitrateEstimate = Float.isNaN(bitrateEstimateFloat) ? NO_ESTIMATE - : (long) bitrateEstimateFloat; + bitrateEstimate = (long) slidingPercentile.getPercentile(0.5f); + } + maybeNotifyBandwidthSample(sampleElapsedTimeMs, sampleBytesTransferred, bitrateEstimate); + sampleStartTimeMs = nowMs; + sampleBytesTransferred = 0; + } // Else any sample bytes transferred will be carried forward into the next sample. + streamCount--; + } + + private synchronized void onConnectivityAction() { + int networkType = + networkTypeOverrideSet + ? networkTypeOverride + : (context == null ? C.NETWORK_TYPE_UNKNOWN : Util.getNetworkType(context)); + if (this.networkType == networkType) { + return; + } + + this.networkType = networkType; + if (networkType == C.NETWORK_TYPE_OFFLINE + || networkType == C.NETWORK_TYPE_UNKNOWN + || networkType == C.NETWORK_TYPE_OTHER) { + // It's better not to reset the bandwidth meter for these network types. + return; + } + + // Reset the bitrate estimate and report it, along with any bytes transferred. + this.bitrateEstimate = getInitialBitrateEstimateForNetworkType(networkType); + long nowMs = clock.elapsedRealtime(); + int sampleElapsedTimeMs = streamCount > 0 ? (int) (nowMs - sampleStartTimeMs) : 0; + maybeNotifyBandwidthSample(sampleElapsedTimeMs, sampleBytesTransferred, bitrateEstimate); + + // Reset the remainder of the state. + sampleStartTimeMs = nowMs; + sampleBytesTransferred = 0; + totalBytesTransferred = 0; + totalElapsedTimeMs = 0; + slidingPercentile.reset(); + } + + private void maybeNotifyBandwidthSample( + int elapsedMs, long bytesTransferred, long bitrateEstimate) { + if (elapsedMs == 0 && bytesTransferred == 0 && bitrateEstimate == lastReportedBitrateEstimate) { + return; + } + lastReportedBitrateEstimate = bitrateEstimate; + eventDispatcher.dispatch( + listener -> listener.onBandwidthSample(elapsedMs, bytesTransferred, bitrateEstimate)); + } + + private long getInitialBitrateEstimateForNetworkType(@C.NetworkType int networkType) { + Long initialBitrateEstimate = initialBitrateEstimates.get(networkType); + if (initialBitrateEstimate == null) { + initialBitrateEstimate = initialBitrateEstimates.get(C.NETWORK_TYPE_UNKNOWN); + } + if (initialBitrateEstimate == null) { + initialBitrateEstimate = DEFAULT_INITIAL_BITRATE_ESTIMATE; + } + return initialBitrateEstimate; + } + + /* + * Note: This class only holds a weak reference to DefaultBandwidthMeter instances. It should not + * be made non-static, since doing so adds a strong reference (i.e. DefaultBandwidthMeter.this). + */ + private static class ConnectivityActionReceiver extends BroadcastReceiver { + + private static @MonotonicNonNull ConnectivityActionReceiver staticInstance; + + private final Handler mainHandler; + private final ArrayList> bandwidthMeters; + + public static synchronized ConnectivityActionReceiver getInstance(Context context) { + if (staticInstance == null) { + staticInstance = new ConnectivityActionReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + context.registerReceiver(staticInstance, filter); + } + return staticInstance; + } + + private ConnectivityActionReceiver() { + mainHandler = new Handler(Looper.getMainLooper()); + bandwidthMeters = new ArrayList<>(); + } + + public synchronized void register(DefaultBandwidthMeter bandwidthMeter) { + removeClearedReferences(); + bandwidthMeters.add(new WeakReference<>(bandwidthMeter)); + // Simulate an initial update on the main thread (like the sticky broadcast we'd receive if + // we were to register a separate broadcast receiver for each bandwidth meter). + mainHandler.post(() -> updateBandwidthMeter(bandwidthMeter)); + } + + @Override + public synchronized void onReceive(Context context, Intent intent) { + if (isInitialStickyBroadcast()) { + return; + } + removeClearedReferences(); + for (int i = 0; i < bandwidthMeters.size(); i++) { + WeakReference bandwidthMeterReference = bandwidthMeters.get(i); + DefaultBandwidthMeter bandwidthMeter = bandwidthMeterReference.get(); + if (bandwidthMeter != null) { + updateBandwidthMeter(bandwidthMeter); + } } } - notifyBandwidthSample(sampleElapsedTimeMs, sampleBytesTransferred, bitrateEstimate); - if (--streamCount > 0) { - sampleStartTimeMs = nowMs; - } - sampleBytesTransferred = 0; - } - private void notifyBandwidthSample(final int elapsedMs, final long bytes, final long bitrate) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onBandwidthSample(elapsedMs, bytes, bitrate); + private void updateBandwidthMeter(DefaultBandwidthMeter bandwidthMeter) { + bandwidthMeter.onConnectivityAction(); + } + + private void removeClearedReferences() { + for (int i = bandwidthMeters.size() - 1; i >= 0; i--) { + WeakReference bandwidthMeterReference = bandwidthMeters.get(i); + DefaultBandwidthMeter bandwidthMeter = bandwidthMeterReference.get(); + if (bandwidthMeter == null) { + bandwidthMeters.remove(i); } - }); + } } } + private static Map createInitialBitrateCountryGroupAssignment() { + HashMap countryGroupAssignment = new HashMap<>(); + countryGroupAssignment.put("AD", new int[] {1, 1, 0, 0}); + countryGroupAssignment.put("AE", new int[] {1, 4, 4, 4}); + countryGroupAssignment.put("AF", new int[] {4, 4, 3, 3}); + countryGroupAssignment.put("AG", new int[] {3, 1, 0, 1}); + countryGroupAssignment.put("AI", new int[] {1, 0, 0, 3}); + countryGroupAssignment.put("AL", new int[] {1, 2, 0, 1}); + countryGroupAssignment.put("AM", new int[] {2, 2, 2, 2}); + countryGroupAssignment.put("AO", new int[] {3, 4, 2, 0}); + countryGroupAssignment.put("AR", new int[] {2, 3, 2, 2}); + countryGroupAssignment.put("AS", new int[] {3, 0, 4, 2}); + countryGroupAssignment.put("AT", new int[] {0, 3, 0, 0}); + countryGroupAssignment.put("AU", new int[] {0, 3, 0, 1}); + countryGroupAssignment.put("AW", new int[] {1, 1, 0, 3}); + countryGroupAssignment.put("AX", new int[] {0, 3, 0, 2}); + countryGroupAssignment.put("AZ", new int[] {3, 3, 3, 3}); + countryGroupAssignment.put("BA", new int[] {1, 1, 0, 1}); + countryGroupAssignment.put("BB", new int[] {0, 2, 0, 0}); + countryGroupAssignment.put("BD", new int[] {2, 1, 3, 3}); + countryGroupAssignment.put("BE", new int[] {0, 0, 0, 1}); + countryGroupAssignment.put("BF", new int[] {4, 4, 4, 1}); + countryGroupAssignment.put("BG", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("BH", new int[] {2, 1, 3, 4}); + countryGroupAssignment.put("BI", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("BJ", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("BL", new int[] {1, 0, 2, 2}); + countryGroupAssignment.put("BM", new int[] {1, 2, 0, 0}); + countryGroupAssignment.put("BN", new int[] {4, 1, 3, 2}); + countryGroupAssignment.put("BO", new int[] {1, 2, 3, 2}); + countryGroupAssignment.put("BQ", new int[] {1, 1, 2, 4}); + countryGroupAssignment.put("BR", new int[] {2, 3, 3, 2}); + countryGroupAssignment.put("BS", new int[] {2, 1, 1, 4}); + countryGroupAssignment.put("BT", new int[] {3, 0, 3, 1}); + countryGroupAssignment.put("BW", new int[] {4, 4, 1, 2}); + countryGroupAssignment.put("BY", new int[] {0, 1, 1, 2}); + countryGroupAssignment.put("BZ", new int[] {2, 2, 2, 1}); + countryGroupAssignment.put("CA", new int[] {0, 3, 1, 3}); + countryGroupAssignment.put("CD", new int[] {4, 4, 2, 2}); + countryGroupAssignment.put("CF", new int[] {4, 4, 3, 0}); + countryGroupAssignment.put("CG", new int[] {3, 4, 2, 4}); + countryGroupAssignment.put("CH", new int[] {0, 0, 1, 0}); + countryGroupAssignment.put("CI", new int[] {3, 4, 3, 3}); + countryGroupAssignment.put("CK", new int[] {2, 4, 1, 0}); + countryGroupAssignment.put("CL", new int[] {1, 2, 2, 3}); + countryGroupAssignment.put("CM", new int[] {3, 4, 3, 1}); + countryGroupAssignment.put("CN", new int[] {2, 0, 2, 3}); + countryGroupAssignment.put("CO", new int[] {2, 3, 2, 2}); + countryGroupAssignment.put("CR", new int[] {2, 3, 4, 4}); + countryGroupAssignment.put("CU", new int[] {4, 4, 3, 1}); + countryGroupAssignment.put("CV", new int[] {2, 3, 1, 2}); + countryGroupAssignment.put("CW", new int[] {1, 1, 0, 0}); + countryGroupAssignment.put("CY", new int[] {1, 1, 0, 0}); + countryGroupAssignment.put("CZ", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("DE", new int[] {0, 1, 1, 3}); + countryGroupAssignment.put("DJ", new int[] {4, 3, 4, 1}); + countryGroupAssignment.put("DK", new int[] {0, 0, 1, 1}); + countryGroupAssignment.put("DM", new int[] {1, 0, 1, 3}); + countryGroupAssignment.put("DO", new int[] {3, 3, 4, 4}); + countryGroupAssignment.put("DZ", new int[] {3, 3, 4, 4}); + countryGroupAssignment.put("EC", new int[] {2, 3, 4, 3}); + countryGroupAssignment.put("EE", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("EG", new int[] {3, 4, 2, 2}); + countryGroupAssignment.put("EH", new int[] {2, 0, 3, 3}); + countryGroupAssignment.put("ER", new int[] {4, 2, 2, 0}); + countryGroupAssignment.put("ES", new int[] {0, 1, 1, 1}); + countryGroupAssignment.put("ET", new int[] {4, 4, 4, 0}); + countryGroupAssignment.put("FI", new int[] {0, 0, 1, 0}); + countryGroupAssignment.put("FJ", new int[] {3, 0, 3, 3}); + countryGroupAssignment.put("FK", new int[] {3, 4, 2, 2}); + countryGroupAssignment.put("FM", new int[] {4, 0, 4, 0}); + countryGroupAssignment.put("FO", new int[] {0, 0, 0, 0}); + countryGroupAssignment.put("FR", new int[] {1, 0, 3, 1}); + countryGroupAssignment.put("GA", new int[] {3, 3, 2, 2}); + countryGroupAssignment.put("GB", new int[] {0, 1, 3, 3}); + countryGroupAssignment.put("GD", new int[] {2, 0, 4, 4}); + countryGroupAssignment.put("GE", new int[] {1, 1, 1, 4}); + countryGroupAssignment.put("GF", new int[] {2, 3, 4, 4}); + countryGroupAssignment.put("GG", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("GH", new int[] {3, 3, 2, 2}); + countryGroupAssignment.put("GI", new int[] {0, 0, 0, 1}); + countryGroupAssignment.put("GL", new int[] {2, 2, 0, 2}); + countryGroupAssignment.put("GM", new int[] {4, 4, 3, 4}); + countryGroupAssignment.put("GN", new int[] {3, 4, 4, 2}); + countryGroupAssignment.put("GP", new int[] {2, 1, 1, 4}); + countryGroupAssignment.put("GQ", new int[] {4, 4, 3, 0}); + countryGroupAssignment.put("GR", new int[] {1, 1, 0, 2}); + countryGroupAssignment.put("GT", new int[] {3, 3, 3, 3}); + countryGroupAssignment.put("GU", new int[] {1, 2, 4, 4}); + countryGroupAssignment.put("GW", new int[] {4, 4, 4, 1}); + countryGroupAssignment.put("GY", new int[] {3, 2, 1, 1}); + countryGroupAssignment.put("HK", new int[] {0, 2, 3, 4}); + countryGroupAssignment.put("HN", new int[] {3, 2, 3, 2}); + countryGroupAssignment.put("HR", new int[] {1, 1, 0, 1}); + countryGroupAssignment.put("HT", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("HU", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("ID", new int[] {3, 2, 3, 4}); + countryGroupAssignment.put("IE", new int[] {1, 0, 1, 1}); + countryGroupAssignment.put("IL", new int[] {0, 0, 2, 3}); + countryGroupAssignment.put("IM", new int[] {0, 0, 0, 1}); + countryGroupAssignment.put("IN", new int[] {2, 2, 4, 4}); + countryGroupAssignment.put("IO", new int[] {4, 2, 2, 2}); + countryGroupAssignment.put("IQ", new int[] {3, 3, 4, 2}); + countryGroupAssignment.put("IR", new int[] {3, 0, 2, 2}); + countryGroupAssignment.put("IS", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("IT", new int[] {1, 0, 1, 2}); + countryGroupAssignment.put("JE", new int[] {1, 0, 0, 1}); + countryGroupAssignment.put("JM", new int[] {2, 3, 3, 1}); + countryGroupAssignment.put("JO", new int[] {1, 2, 1, 2}); + countryGroupAssignment.put("JP", new int[] {0, 2, 1, 1}); + countryGroupAssignment.put("KE", new int[] {3, 4, 4, 3}); + countryGroupAssignment.put("KG", new int[] {1, 1, 2, 2}); + countryGroupAssignment.put("KH", new int[] {1, 0, 4, 4}); + countryGroupAssignment.put("KI", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("KM", new int[] {4, 3, 2, 3}); + countryGroupAssignment.put("KN", new int[] {1, 0, 1, 3}); + countryGroupAssignment.put("KP", new int[] {4, 2, 4, 2}); + countryGroupAssignment.put("KR", new int[] {0, 1, 1, 1}); + countryGroupAssignment.put("KW", new int[] {2, 3, 1, 1}); + countryGroupAssignment.put("KY", new int[] {1, 1, 0, 1}); + countryGroupAssignment.put("KZ", new int[] {1, 2, 2, 3}); + countryGroupAssignment.put("LA", new int[] {2, 2, 1, 1}); + countryGroupAssignment.put("LB", new int[] {3, 2, 0, 0}); + countryGroupAssignment.put("LC", new int[] {1, 1, 0, 0}); + countryGroupAssignment.put("LI", new int[] {0, 0, 2, 4}); + countryGroupAssignment.put("LK", new int[] {2, 1, 2, 3}); + countryGroupAssignment.put("LR", new int[] {3, 4, 3, 1}); + countryGroupAssignment.put("LS", new int[] {3, 3, 2, 0}); + countryGroupAssignment.put("LT", new int[] {0, 0, 0, 0}); + countryGroupAssignment.put("LU", new int[] {0, 0, 0, 0}); + countryGroupAssignment.put("LV", new int[] {0, 0, 0, 0}); + countryGroupAssignment.put("LY", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("MA", new int[] {2, 1, 2, 1}); + countryGroupAssignment.put("MC", new int[] {0, 0, 0, 1}); + countryGroupAssignment.put("MD", new int[] {1, 1, 0, 0}); + countryGroupAssignment.put("ME", new int[] {1, 2, 1, 2}); + countryGroupAssignment.put("MF", new int[] {1, 1, 1, 1}); + countryGroupAssignment.put("MG", new int[] {3, 4, 2, 2}); + countryGroupAssignment.put("MH", new int[] {4, 0, 2, 4}); + countryGroupAssignment.put("MK", new int[] {1, 0, 0, 0}); + countryGroupAssignment.put("ML", new int[] {4, 4, 2, 0}); + countryGroupAssignment.put("MM", new int[] {3, 3, 1, 2}); + countryGroupAssignment.put("MN", new int[] {2, 3, 2, 3}); + countryGroupAssignment.put("MO", new int[] {0, 0, 4, 4}); + countryGroupAssignment.put("MP", new int[] {0, 2, 4, 4}); + countryGroupAssignment.put("MQ", new int[] {2, 1, 1, 4}); + countryGroupAssignment.put("MR", new int[] {4, 2, 4, 2}); + countryGroupAssignment.put("MS", new int[] {1, 2, 3, 3}); + countryGroupAssignment.put("MT", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("MU", new int[] {2, 2, 3, 4}); + countryGroupAssignment.put("MV", new int[] {4, 3, 0, 2}); + countryGroupAssignment.put("MW", new int[] {3, 2, 1, 0}); + countryGroupAssignment.put("MX", new int[] {2, 4, 4, 3}); + countryGroupAssignment.put("MY", new int[] {2, 2, 3, 3}); + countryGroupAssignment.put("MZ", new int[] {3, 3, 2, 1}); + countryGroupAssignment.put("NA", new int[] {3, 3, 2, 1}); + countryGroupAssignment.put("NC", new int[] {2, 0, 3, 3}); + countryGroupAssignment.put("NE", new int[] {4, 4, 4, 3}); + countryGroupAssignment.put("NF", new int[] {1, 2, 2, 2}); + countryGroupAssignment.put("NG", new int[] {3, 4, 3, 1}); + countryGroupAssignment.put("NI", new int[] {3, 3, 4, 4}); + countryGroupAssignment.put("NL", new int[] {0, 2, 3, 3}); + countryGroupAssignment.put("NO", new int[] {0, 1, 1, 0}); + countryGroupAssignment.put("NP", new int[] {2, 2, 2, 2}); + countryGroupAssignment.put("NR", new int[] {4, 0, 3, 1}); + countryGroupAssignment.put("NZ", new int[] {0, 0, 1, 2}); + countryGroupAssignment.put("OM", new int[] {3, 2, 1, 3}); + countryGroupAssignment.put("PA", new int[] {1, 3, 3, 4}); + countryGroupAssignment.put("PE", new int[] {2, 3, 4, 4}); + countryGroupAssignment.put("PF", new int[] {2, 2, 0, 1}); + countryGroupAssignment.put("PG", new int[] {4, 3, 3, 1}); + countryGroupAssignment.put("PH", new int[] {3, 0, 3, 4}); + countryGroupAssignment.put("PK", new int[] {3, 3, 3, 3}); + countryGroupAssignment.put("PL", new int[] {1, 0, 1, 3}); + countryGroupAssignment.put("PM", new int[] {0, 2, 2, 0}); + countryGroupAssignment.put("PR", new int[] {1, 2, 3, 3}); + countryGroupAssignment.put("PS", new int[] {3, 3, 2, 4}); + countryGroupAssignment.put("PT", new int[] {1, 1, 0, 0}); + countryGroupAssignment.put("PW", new int[] {2, 1, 2, 0}); + countryGroupAssignment.put("PY", new int[] {2, 0, 2, 3}); + countryGroupAssignment.put("QA", new int[] {2, 2, 1, 2}); + countryGroupAssignment.put("RE", new int[] {1, 0, 2, 2}); + countryGroupAssignment.put("RO", new int[] {0, 1, 1, 2}); + countryGroupAssignment.put("RS", new int[] {1, 2, 0, 0}); + countryGroupAssignment.put("RU", new int[] {0, 1, 1, 1}); + countryGroupAssignment.put("RW", new int[] {4, 4, 2, 4}); + countryGroupAssignment.put("SA", new int[] {2, 2, 2, 1}); + countryGroupAssignment.put("SB", new int[] {4, 4, 3, 0}); + countryGroupAssignment.put("SC", new int[] {4, 2, 0, 1}); + countryGroupAssignment.put("SD", new int[] {4, 4, 4, 3}); + countryGroupAssignment.put("SE", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("SG", new int[] {0, 2, 3, 3}); + countryGroupAssignment.put("SH", new int[] {4, 4, 2, 3}); + countryGroupAssignment.put("SI", new int[] {0, 0, 0, 0}); + countryGroupAssignment.put("SJ", new int[] {2, 0, 2, 4}); + countryGroupAssignment.put("SK", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("SL", new int[] {4, 3, 3, 3}); + countryGroupAssignment.put("SM", new int[] {0, 0, 2, 4}); + countryGroupAssignment.put("SN", new int[] {3, 4, 4, 2}); + countryGroupAssignment.put("SO", new int[] {3, 4, 4, 3}); + countryGroupAssignment.put("SR", new int[] {2, 2, 1, 0}); + countryGroupAssignment.put("SS", new int[] {4, 3, 4, 3}); + countryGroupAssignment.put("ST", new int[] {3, 4, 2, 2}); + countryGroupAssignment.put("SV", new int[] {2, 3, 3, 4}); + countryGroupAssignment.put("SX", new int[] {2, 4, 1, 0}); + countryGroupAssignment.put("SY", new int[] {4, 3, 2, 1}); + countryGroupAssignment.put("SZ", new int[] {4, 4, 3, 4}); + countryGroupAssignment.put("TC", new int[] {1, 2, 1, 1}); + countryGroupAssignment.put("TD", new int[] {4, 4, 4, 2}); + countryGroupAssignment.put("TG", new int[] {3, 3, 1, 0}); + countryGroupAssignment.put("TH", new int[] {1, 3, 4, 4}); + countryGroupAssignment.put("TJ", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("TL", new int[] {4, 2, 4, 4}); + countryGroupAssignment.put("TM", new int[] {4, 1, 2, 2}); + countryGroupAssignment.put("TN", new int[] {2, 2, 1, 2}); + countryGroupAssignment.put("TO", new int[] {3, 3, 3, 1}); + countryGroupAssignment.put("TR", new int[] {2, 2, 1, 2}); + countryGroupAssignment.put("TT", new int[] {1, 3, 1, 2}); + countryGroupAssignment.put("TV", new int[] {4, 2, 2, 4}); + countryGroupAssignment.put("TW", new int[] {0, 0, 0, 0}); + countryGroupAssignment.put("TZ", new int[] {3, 3, 4, 3}); + countryGroupAssignment.put("UA", new int[] {0, 2, 1, 2}); + countryGroupAssignment.put("UG", new int[] {4, 3, 3, 2}); + countryGroupAssignment.put("US", new int[] {1, 1, 3, 3}); + countryGroupAssignment.put("UY", new int[] {2, 2, 1, 1}); + countryGroupAssignment.put("UZ", new int[] {2, 2, 2, 2}); + countryGroupAssignment.put("VA", new int[] {1, 2, 4, 2}); + countryGroupAssignment.put("VC", new int[] {2, 0, 2, 4}); + countryGroupAssignment.put("VE", new int[] {4, 4, 4, 3}); + countryGroupAssignment.put("VG", new int[] {3, 0, 1, 3}); + countryGroupAssignment.put("VI", new int[] {1, 1, 4, 4}); + countryGroupAssignment.put("VN", new int[] {0, 2, 4, 4}); + countryGroupAssignment.put("VU", new int[] {4, 1, 3, 1}); + countryGroupAssignment.put("WS", new int[] {3, 3, 3, 2}); + countryGroupAssignment.put("XK", new int[] {1, 2, 1, 0}); + countryGroupAssignment.put("YE", new int[] {4, 4, 4, 3}); + countryGroupAssignment.put("YT", new int[] {2, 2, 2, 3}); + countryGroupAssignment.put("ZA", new int[] {2, 4, 2, 2}); + countryGroupAssignment.put("ZM", new int[] {3, 2, 2, 1}); + countryGroupAssignment.put("ZW", new int[] {3, 3, 2, 1}); + return Collections.unmodifiableMap(countryGroupAssignment); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSource.java index e23e6d3c375d..87e1c728a0fd 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -17,71 +17,106 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; import android.content.Context; import android.net.Uri; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; /** * A {@link DataSource} that supports multiple URI schemes. The supported schemes are: * *

    - *
  • file: For fetching data from a local file (e.g. file:///path/to/media/media.mp4, or just - * /path/to/media/media.mp4 because the implementation assumes that a URI without a scheme is a - * local file URI). - *
  • asset: For fetching data from an asset in the application's apk (e.g. asset:///media.mp4). - *
  • content: For fetching data from a content URI (e.g. content://authority/path/123). - *
  • http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4), if - * constructed using {@link #DefaultDataSource(Context, TransferListener, String, boolean)}, or - * any other schemes supported by a base data source if constructed using - * {@link #DefaultDataSource(Context, TransferListener, DataSource)}. + *
  • file: For fetching data from a local file (e.g. file:///path/to/media/media.mp4, or just + * /path/to/media/media.mp4 because the implementation assumes that a URI without a scheme is + * a local file URI). + *
  • asset: For fetching data from an asset in the application's apk (e.g. asset:///media.mp4). + *
  • rawresource: For fetching data from a raw resource in the application's apk (e.g. + * rawresource:///resourceId, where rawResourceId is the integer identifier of the raw + * resource). + *
  • content: For fetching data from a content URI (e.g. content://authority/path/123). + *
  • rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an + * explicit dependency on ExoPlayer's RTMP extension. + *
  • data: For parsing data inlined in the URI as defined in RFC 2397. + *
  • udp: For fetching data over UDP (e.g. udp://something.com/media). + *
  • http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4), + * if constructed using {@link #DefaultDataSource(Context, String, boolean)}, or any other + * schemes supported by a base data source if constructed using {@link + * #DefaultDataSource(Context, DataSource)}. *
*/ public final class DefaultDataSource implements DataSource { + private static final String TAG = "DefaultDataSource"; + private static final String SCHEME_ASSET = "asset"; private static final String SCHEME_CONTENT = "content"; + private static final String SCHEME_RTMP = "rtmp"; + private static final String SCHEME_UDP = "udp"; + private static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME; + private final Context context; + private final List transferListeners; private final DataSource baseDataSource; - private final DataSource fileDataSource; - private final DataSource assetDataSource; - private final DataSource contentDataSource; - private DataSource dataSource; + // Lazily initialized. + @Nullable private DataSource fileDataSource; + @Nullable private DataSource assetDataSource; + @Nullable private DataSource contentDataSource; + @Nullable private DataSource rtmpDataSource; + @Nullable private DataSource udpDataSource; + @Nullable private DataSource dataSchemeDataSource; + @Nullable private DataSource rawResourceDataSource; + + @Nullable private DataSource dataSource; /** * Constructs a new instance, optionally configured to follow cross-protocol redirects. * * @param context A context. - * @param listener An optional listener. - * @param userAgent The User-Agent string that should be used when requesting remote data. + * @param userAgent The User-Agent to use when requesting remote data. * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled when fetching remote data. */ - public DefaultDataSource(Context context, TransferListener listener, - String userAgent, boolean allowCrossProtocolRedirects) { - this(context, listener, userAgent, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, - DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects); + public DefaultDataSource(Context context, String userAgent, boolean allowCrossProtocolRedirects) { + this( + context, + userAgent, + DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, + DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, + allowCrossProtocolRedirects); } /** * Constructs a new instance, optionally configured to follow cross-protocol redirects. * * @param context A context. - * @param listener An optional listener. - * @param userAgent The User-Agent string that should be used when requesting remote data. + * @param userAgent The User-Agent to use when requesting remote data. * @param connectTimeoutMillis The connection timeout that should be used when requesting remote * data, in milliseconds. A timeout of zero is interpreted as an infinite timeout. - * @param readTimeoutMillis The read timeout that should be used when requesting remote data, - * in milliseconds. A timeout of zero is interpreted as an infinite timeout. + * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in + * milliseconds. A timeout of zero is interpreted as an infinite timeout. * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled when fetching remote data. */ - public DefaultDataSource(Context context, TransferListener listener, - String userAgent, int connectTimeoutMillis, int readTimeoutMillis, + public DefaultDataSource( + Context context, + String userAgent, + int connectTimeoutMillis, + int readTimeoutMillis, boolean allowCrossProtocolRedirects) { - this(context, listener, - new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis, - readTimeoutMillis, allowCrossProtocolRedirects, null)); + this( + context, + new DefaultHttpDataSource( + userAgent, + connectTimeoutMillis, + readTimeoutMillis, + allowCrossProtocolRedirects, + /* defaultRequestProperties= */ null)); } /** @@ -89,16 +124,26 @@ public final class DefaultDataSource implements DataSource { * than file, asset and content. * * @param context A context. - * @param listener An optional listener. * @param baseDataSource A {@link DataSource} to use for URI schemes other than file, asset and * content. This {@link DataSource} should normally support at least http(s). */ - public DefaultDataSource(Context context, TransferListener listener, - DataSource baseDataSource) { + public DefaultDataSource(Context context, DataSource baseDataSource) { + this.context = context.getApplicationContext(); this.baseDataSource = Assertions.checkNotNull(baseDataSource); - this.fileDataSource = new FileDataSource(listener); - this.assetDataSource = new AssetDataSource(context, listener); - this.contentDataSource = new ContentDataSource(context, listener); + transferListeners = new ArrayList<>(); + } + + @Override + public void addTransferListener(TransferListener transferListener) { + baseDataSource.addTransferListener(transferListener); + transferListeners.add(transferListener); + maybeAddListenerToDataSource(fileDataSource, transferListener); + maybeAddListenerToDataSource(assetDataSource, transferListener); + maybeAddListenerToDataSource(contentDataSource, transferListener); + maybeAddListenerToDataSource(rtmpDataSource, transferListener); + maybeAddListenerToDataSource(udpDataSource, transferListener); + maybeAddListenerToDataSource(dataSchemeDataSource, transferListener); + maybeAddListenerToDataSource(rawResourceDataSource, transferListener); } @Override @@ -107,15 +152,24 @@ public final class DefaultDataSource implements DataSource { // Choose the correct source for the scheme. String scheme = dataSpec.uri.getScheme(); if (Util.isLocalFileUri(dataSpec.uri)) { - if (dataSpec.uri.getPath().startsWith("/android_asset/")) { - dataSource = assetDataSource; + String uriPath = dataSpec.uri.getPath(); + if (uriPath != null && uriPath.startsWith("/android_asset/")) { + dataSource = getAssetDataSource(); } else { - dataSource = fileDataSource; + dataSource = getFileDataSource(); } } else if (SCHEME_ASSET.equals(scheme)) { - dataSource = assetDataSource; + dataSource = getAssetDataSource(); } else if (SCHEME_CONTENT.equals(scheme)) { - dataSource = contentDataSource; + dataSource = getContentDataSource(); + } else if (SCHEME_RTMP.equals(scheme)) { + dataSource = getRtmpDataSource(); + } else if (SCHEME_UDP.equals(scheme)) { + dataSource = getUdpDataSource(); + } else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) { + dataSource = getDataSchemeDataSource(); + } else if (SCHEME_RAW.equals(scheme)) { + dataSource = getRawResourceDataSource(); } else { dataSource = baseDataSource; } @@ -125,14 +179,20 @@ public final class DefaultDataSource implements DataSource { @Override public int read(byte[] buffer, int offset, int readLength) throws IOException { - return dataSource.read(buffer, offset, readLength); + return Assertions.checkNotNull(dataSource).read(buffer, offset, readLength); } @Override + @Nullable public Uri getUri() { return dataSource == null ? null : dataSource.getUri(); } + @Override + public Map> getResponseHeaders() { + return dataSource == null ? Collections.emptyMap() : dataSource.getResponseHeaders(); + } + @Override public void close() throws IOException { if (dataSource != null) { @@ -144,4 +204,86 @@ public final class DefaultDataSource implements DataSource { } } + private DataSource getUdpDataSource() { + if (udpDataSource == null) { + udpDataSource = new UdpDataSource(); + addListenersToDataSource(udpDataSource); + } + return udpDataSource; + } + + private DataSource getFileDataSource() { + if (fileDataSource == null) { + fileDataSource = new FileDataSource(); + addListenersToDataSource(fileDataSource); + } + return fileDataSource; + } + + private DataSource getAssetDataSource() { + if (assetDataSource == null) { + assetDataSource = new AssetDataSource(context); + addListenersToDataSource(assetDataSource); + } + return assetDataSource; + } + + private DataSource getContentDataSource() { + if (contentDataSource == null) { + contentDataSource = new ContentDataSource(context); + addListenersToDataSource(contentDataSource); + } + return contentDataSource; + } + + private DataSource getRtmpDataSource() { + if (rtmpDataSource == null) { + try { + // LINT.IfChange + Class clazz = Class.forName("com.google.android.exoplayer2.ext.rtmp.RtmpDataSource"); + rtmpDataSource = (DataSource) clazz.getConstructor().newInstance(); + // LINT.ThenChange(../../../../../../../../proguard-rules.txt) + addListenersToDataSource(rtmpDataSource); + } catch (ClassNotFoundException e) { + // Expected if the app was built without the RTMP extension. + Log.w(TAG, "Attempting to play RTMP stream without depending on the RTMP extension"); + } catch (Exception e) { + // The RTMP extension is present, but instantiation failed. + throw new RuntimeException("Error instantiating RTMP extension", e); + } + if (rtmpDataSource == null) { + rtmpDataSource = baseDataSource; + } + } + return rtmpDataSource; + } + + private DataSource getDataSchemeDataSource() { + if (dataSchemeDataSource == null) { + dataSchemeDataSource = new DataSchemeDataSource(); + addListenersToDataSource(dataSchemeDataSource); + } + return dataSchemeDataSource; + } + + private DataSource getRawResourceDataSource() { + if (rawResourceDataSource == null) { + rawResourceDataSource = new RawResourceDataSource(context); + addListenersToDataSource(rawResourceDataSource); + } + return rawResourceDataSource; + } + + private void addListenersToDataSource(DataSource dataSource) { + for (int i = 0; i < transferListeners.size(); i++) { + dataSource.addTransferListener(transferListeners.get(i)); + } + } + + private void maybeAddListenerToDataSource( + @Nullable DataSource dataSource, TransferListener listener) { + if (dataSource != null) { + dataSource.addTransferListener(listener); + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java index 7a310d6e22d0..81add13c10a3 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java @@ -16,6 +16,7 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; import android.content.Context; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource.Factory; /** @@ -25,7 +26,7 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource. public final class DefaultDataSourceFactory implements Factory { private final Context context; - private final TransferListener listener; + @Nullable private final TransferListener listener; private final DataSource.Factory baseDataSourceFactory; /** @@ -33,7 +34,7 @@ public final class DefaultDataSourceFactory implements Factory { * @param userAgent The User-Agent string that should be used. */ public DefaultDataSourceFactory(Context context, String userAgent) { - this(context, userAgent, null); + this(context, userAgent, /* listener= */ null); } /** @@ -41,19 +42,31 @@ public final class DefaultDataSourceFactory implements Factory { * @param userAgent The User-Agent string that should be used. * @param listener An optional listener. */ - public DefaultDataSourceFactory(Context context, String userAgent, - TransferListener listener) { + public DefaultDataSourceFactory( + Context context, String userAgent, @Nullable TransferListener listener) { this(context, listener, new DefaultHttpDataSourceFactory(userAgent, listener)); } + /** + * @param context A context. + * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource} + * for {@link DefaultDataSource}. + * @see DefaultDataSource#DefaultDataSource(Context, DataSource) + */ + public DefaultDataSourceFactory(Context context, DataSource.Factory baseDataSourceFactory) { + this(context, /* listener= */ null, baseDataSourceFactory); + } + /** * @param context A context. * @param listener An optional listener. * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource} * for {@link DefaultDataSource}. - * @see DefaultDataSource#DefaultDataSource(Context, TransferListener, DataSource) + * @see DefaultDataSource#DefaultDataSource(Context, DataSource) */ - public DefaultDataSourceFactory(Context context, TransferListener listener, + public DefaultDataSourceFactory( + Context context, + @Nullable TransferListener listener, DataSource.Factory baseDataSourceFactory) { this.context = context.getApplicationContext(); this.listener = listener; @@ -62,7 +75,11 @@ public final class DefaultDataSourceFactory implements Factory { @Override public DefaultDataSource createDataSource() { - return new DefaultDataSource(context, listener, baseDataSourceFactory.createDataSource()); + DefaultDataSource dataSource = + new DefaultDataSource(context, baseDataSourceFactory.createDataSource()); + if (listener != null) { + dataSource.addTransferListener(listener); + } + return dataSource; } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index fdb44cc2ea1f..6e5095b0a4c9 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -17,9 +17,12 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; import android.net.Uri; import android.text.TextUtils; -import android.util.Log; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec.HttpMethod; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Predicate; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.EOFException; @@ -33,27 +36,31 @@ import java.net.NoRouteToHostException; import java.net.ProtocolException; import java.net.URISyntaxException; import java.net.URL; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.zip.GZIPInputStream; import org.mozilla.gecko.util.ProxySelector; - /** * An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}. - *

- * By default this implementation will not follow cross-protocol redirects (i.e. redirects from - * HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the - * {@link #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean, - * RequestProperties)} constructor and passing {@code true} as the second last argument. + * + *

By default this implementation will not follow cross-protocol redirects (i.e. redirects from + * HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the {@link + * #DefaultHttpDataSource(String, int, int, boolean, RequestProperties)} constructor and passing + * {@code true} for the {@code allowCrossProtocolRedirects} argument. + * + *

Note: HTTP request headers will be set using all parameters passed via (in order of decreasing + * priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to + * construct the instance. */ -public class DefaultHttpDataSource implements HttpDataSource { +public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSource { - /** - * The default connection timeout, in milliseconds. - */ + /** The default connection timeout, in milliseconds. */ public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000; /** * The default read timeout, in milliseconds. @@ -62,6 +69,8 @@ public class DefaultHttpDataSource implements HttpDataSource { private static final String TAG = "DefaultHttpDataSource"; private static final int MAX_REDIRECTS = 20; // Same limit as okhttp. + private static final int HTTP_STATUS_TEMPORARY_REDIRECT = 307; + private static final int HTTP_STATUS_PERMANENT_REDIRECT = 308; private static final long MAX_BYTES_TO_DRAIN = 2048; private static final Pattern CONTENT_RANGE_HEADER = Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$"); @@ -71,15 +80,15 @@ public class DefaultHttpDataSource implements HttpDataSource { private final int connectTimeoutMillis; private final int readTimeoutMillis; private final String userAgent; - private final Predicate contentTypePredicate; - private final RequestProperties defaultRequestProperties; + @Nullable private final RequestProperties defaultRequestProperties; private final RequestProperties requestProperties; - private final TransferListener listener; - private DataSpec dataSpec; - private HttpURLConnection connection; - private InputStream inputStream; + @Nullable private Predicate contentTypePredicate; + @Nullable private DataSpec dataSpec; + @Nullable private HttpURLConnection connection; + @Nullable private InputStream inputStream; private boolean opened; + private int responseCode; private long bytesToSkip; private long bytesToRead; @@ -87,70 +96,47 @@ public class DefaultHttpDataSource implements HttpDataSource { private long bytesSkipped; private long bytesRead; - /** - * @param userAgent The User-Agent string that should be used. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. - */ - public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate) { - this(userAgent, contentTypePredicate, null); + /** @param userAgent The User-Agent string that should be used. */ + public DefaultHttpDataSource(String userAgent) { + this(userAgent, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS); } /** * @param userAgent The User-Agent string that should be used. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. - * @param listener An optional listener. - */ - public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, - TransferListener listener) { - this(userAgent, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS, - DEFAULT_READ_TIMEOUT_MILLIS); - } - - /** - * @param userAgent The User-Agent string that should be used. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. - * @param listener An optional listener. * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is * interpreted as an infinite timeout. - * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted - * as an infinite timeout. + * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as + * an infinite timeout. */ - public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, - TransferListener listener, int connectTimeoutMillis, - int readTimeoutMillis) { - this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false, - null); + public DefaultHttpDataSource(String userAgent, int connectTimeoutMillis, int readTimeoutMillis) { + this( + userAgent, + connectTimeoutMillis, + readTimeoutMillis, + /* allowCrossProtocolRedirects= */ false, + /* defaultRequestProperties= */ null); } /** * @param userAgent The User-Agent string that should be used. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. - * @param listener An optional listener. * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is - * interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use - * the default value. - * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted - * as an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value. + * interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use the + * default value. + * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as + * an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value. * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled. - * @param defaultRequestProperties The default request properties to be sent to the server as - * HTTP headers or {@code null} if not required. + * @param defaultRequestProperties The default request properties to be sent to the server as HTTP + * headers or {@code null} if not required. */ - public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, - TransferListener listener, int connectTimeoutMillis, - int readTimeoutMillis, boolean allowCrossProtocolRedirects, - RequestProperties defaultRequestProperties) { + public DefaultHttpDataSource( + String userAgent, + int connectTimeoutMillis, + int readTimeoutMillis, + boolean allowCrossProtocolRedirects, + @Nullable RequestProperties defaultRequestProperties) { + super(/* isNetwork= */ true); this.userAgent = Assertions.checkNotEmpty(userAgent); - this.contentTypePredicate = contentTypePredicate; - this.listener = listener; this.requestProperties = new RequestProperties(); this.connectTimeoutMillis = connectTimeoutMillis; this.readTimeoutMillis = readTimeoutMillis; @@ -158,14 +144,111 @@ public class DefaultHttpDataSource implements HttpDataSource { this.defaultRequestProperties = defaultRequestProperties; } + /** + * @param userAgent The User-Agent string that should be used. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the + * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. + * @deprecated Use {@link #DefaultHttpDataSource(String)} and {@link + * #setContentTypePredicate(Predicate)}. + */ + @Deprecated + public DefaultHttpDataSource(String userAgent, @Nullable Predicate contentTypePredicate) { + this( + userAgent, + contentTypePredicate, + DEFAULT_CONNECT_TIMEOUT_MILLIS, + DEFAULT_READ_TIMEOUT_MILLIS); + } + + /** + * @param userAgent The User-Agent string that should be used. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the + * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. + * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is + * interpreted as an infinite timeout. + * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as + * an infinite timeout. + * @deprecated Use {@link #DefaultHttpDataSource(String, int, int)} and {@link + * #setContentTypePredicate(Predicate)}. + */ + @SuppressWarnings("deprecation") + @Deprecated + public DefaultHttpDataSource( + String userAgent, + @Nullable Predicate contentTypePredicate, + int connectTimeoutMillis, + int readTimeoutMillis) { + this( + userAgent, + contentTypePredicate, + connectTimeoutMillis, + readTimeoutMillis, + /* allowCrossProtocolRedirects= */ false, + /* defaultRequestProperties= */ null); + } + + /** + * @param userAgent The User-Agent string that should be used. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the + * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link + * #open(DataSpec)}. + * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is + * interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use the + * default value. + * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as + * an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value. + * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP + * to HTTPS and vice versa) are enabled. + * @param defaultRequestProperties The default request properties to be sent to the server as HTTP + * headers or {@code null} if not required. + * @deprecated Use {@link #DefaultHttpDataSource(String, int, int, boolean, RequestProperties)} + * and {@link #setContentTypePredicate(Predicate)}. + */ + @Deprecated + public DefaultHttpDataSource( + String userAgent, + @Nullable Predicate contentTypePredicate, + int connectTimeoutMillis, + int readTimeoutMillis, + boolean allowCrossProtocolRedirects, + @Nullable RequestProperties defaultRequestProperties) { + super(/* isNetwork= */ true); + this.userAgent = Assertions.checkNotEmpty(userAgent); + this.contentTypePredicate = contentTypePredicate; + this.requestProperties = new RequestProperties(); + this.connectTimeoutMillis = connectTimeoutMillis; + this.readTimeoutMillis = readTimeoutMillis; + this.allowCrossProtocolRedirects = allowCrossProtocolRedirects; + this.defaultRequestProperties = defaultRequestProperties; + } + + /** + * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a + * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. + * + * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a + * predicate that was previously set. + */ + public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) { + this.contentTypePredicate = contentTypePredicate; + } + @Override + @Nullable public Uri getUri() { return connection == null ? null : Uri.parse(connection.getURL().toString()); } + @Override + public int getResponseCode() { + return connection == null || responseCode <= 0 ? -1 : responseCode; + } + @Override public Map> getResponseHeaders() { - return connection == null ? null : connection.getHeaderFields(); + return connection == null ? Collections.emptyMap() : connection.getHeaderFields(); } @Override @@ -186,27 +269,32 @@ public class DefaultHttpDataSource implements HttpDataSource { requestProperties.clear(); } + /** + * Opens the source to read the specified data. + */ @Override public long open(DataSpec dataSpec) throws HttpDataSourceException { this.dataSpec = dataSpec; this.bytesRead = 0; this.bytesSkipped = 0; + transferInitializing(dataSpec); try { connection = makeConnection(dataSpec); } catch (IOException e) { - throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, - dataSpec, HttpDataSourceException.TYPE_OPEN); + throw new HttpDataSourceException( + "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN); } catch (URISyntaxException e) { throw new HttpDataSourceException("URI invalid: " + dataSpec.uri.toString(), dataSpec, HttpDataSourceException.TYPE_OPEN); } - int responseCode; + String responseMessage; try { responseCode = connection.getResponseCode(); + responseMessage = connection.getResponseMessage(); } catch (IOException e) { closeConnectionQuietly(); - throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, - dataSpec, HttpDataSourceException.TYPE_OPEN); + throw new HttpDataSourceException( + "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN); } // Check for a valid response code. @@ -214,7 +302,7 @@ public class DefaultHttpDataSource implements HttpDataSource { Map> headers = connection.getHeaderFields(); closeConnectionQuietly(); InvalidResponseCodeException exception = - new InvalidResponseCodeException(responseCode, headers, dataSpec); + new InvalidResponseCodeException(responseCode, responseMessage, headers, dataSpec); if (responseCode == 416) { exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE)); } @@ -234,7 +322,8 @@ public class DefaultHttpDataSource implements HttpDataSource { bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; // Determine the length of the data to be read, after skipping. - if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) { + boolean isCompressed = isCompressed(connection); + if (!isCompressed) { if (dataSpec.length != C.LENGTH_UNSET) { bytesToRead = dataSpec.length; } else { @@ -244,23 +333,23 @@ public class DefaultHttpDataSource implements HttpDataSource { } } else { // Gzip is enabled. If the server opts to use gzip then the content length in the response - // will be that of the compressed data, which isn't what we want. Furthermore, there isn't a - // reliable way to determine whether the gzip was used or not. Always use the dataSpec length - // in this case. + // will be that of the compressed data, which isn't what we want. Always use the dataSpec + // length in this case. bytesToRead = dataSpec.length; } try { inputStream = connection.getInputStream(); + if (isCompressed) { + inputStream = new GZIPInputStream(inputStream); + } } catch (IOException e) { closeConnectionQuietly(); throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN); } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesToRead; } @@ -291,9 +380,7 @@ public class DefaultHttpDataSource implements HttpDataSource { closeConnectionQuietly(); if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } } @@ -303,7 +390,7 @@ public class DefaultHttpDataSource implements HttpDataSource { * * @return The current open connection, or null. */ - protected final HttpURLConnection getConnection() { + protected final @Nullable HttpURLConnection getConnection() { return connection; } @@ -344,7 +431,8 @@ public class DefaultHttpDataSource implements HttpDataSource { */ private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException, URISyntaxException { URL url = new URL(dataSpec.uri.toString()); - byte[] postBody = dataSpec.postBody; + @HttpMethod int httpMethod = dataSpec.httpMethod; + byte[] httpBody = dataSpec.httpBody; long position = dataSpec.position; long length = dataSpec.length; boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP); @@ -352,28 +440,51 @@ public class DefaultHttpDataSource implements HttpDataSource { if (!allowCrossProtocolRedirects) { // HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection // automatically. This is the behavior we want, so use it. - return makeConnection(url, postBody, position, length, allowGzip, true /* followRedirects */); + return makeConnection( + url, + httpMethod, + httpBody, + position, + length, + allowGzip, + /* followRedirects= */ true, + dataSpec.httpRequestHeaders); } // We need to handle redirects ourselves to allow cross-protocol redirects. int redirectCount = 0; while (redirectCount++ <= MAX_REDIRECTS) { - HttpURLConnection connection = makeConnection( - url, postBody, position, length, allowGzip, false /* followRedirects */); + HttpURLConnection connection = + makeConnection( + url, + httpMethod, + httpBody, + position, + length, + allowGzip, + /* followRedirects= */ false, + dataSpec.httpRequestHeaders); int responseCode = connection.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_MULT_CHOICE - || responseCode == HttpURLConnection.HTTP_MOVED_PERM - || responseCode == HttpURLConnection.HTTP_MOVED_TEMP - || responseCode == HttpURLConnection.HTTP_SEE_OTHER - || (postBody == null - && (responseCode == 307 /* HTTP_TEMP_REDIRECT */ - || responseCode == 308 /* HTTP_PERM_REDIRECT */))) { - // For 300, 301, 302, and 303 POST requests follow the redirect and are transformed into - // GET requests. For 307 and 308 POST requests are not redirected. - postBody = null; - String location = connection.getHeaderField("Location"); + String location = connection.getHeaderField("Location"); + if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD) + && (responseCode == HttpURLConnection.HTTP_MULT_CHOICE + || responseCode == HttpURLConnection.HTTP_MOVED_PERM + || responseCode == HttpURLConnection.HTTP_MOVED_TEMP + || responseCode == HttpURLConnection.HTTP_SEE_OTHER + || responseCode == HTTP_STATUS_TEMPORARY_REDIRECT + || responseCode == HTTP_STATUS_PERMANENT_REDIRECT)) { connection.disconnect(); url = handleRedirect(url, location); + } else if (httpMethod == DataSpec.HTTP_METHOD_POST + && (responseCode == HttpURLConnection.HTTP_MULT_CHOICE + || responseCode == HttpURLConnection.HTTP_MOVED_PERM + || responseCode == HttpURLConnection.HTTP_MOVED_TEMP + || responseCode == HttpURLConnection.HTTP_SEE_OTHER)) { + // POST request follows the redirect and is transformed into a GET request. + connection.disconnect(); + httpMethod = DataSpec.HTTP_METHOD_GET; + httpBody = null; + url = handleRedirect(url, location); } else { return connection; } @@ -387,14 +498,24 @@ public class DefaultHttpDataSource implements HttpDataSource { * Configures a connection and opens it. * * @param url The url to connect to. - * @param postBody The body data for a POST request. + * @param httpMethod The http method. + * @param httpBody The body data. * @param position The byte offset of the requested data. * @param length The length of the requested data, or {@link C#LENGTH_UNSET}. * @param allowGzip Whether to allow the use of gzip. * @param followRedirects Whether to follow redirects. + * @param requestParameters parameters (HTTP headers) to include in request. */ - private HttpURLConnection makeConnection(URL url, byte[] postBody, long position, - long length, boolean allowGzip, boolean followRedirects) throws IOException, URISyntaxException { + private HttpURLConnection makeConnection( + URL url, + @HttpMethod int httpMethod, + byte[] httpBody, + long position, + long length, + boolean allowGzip, + boolean followRedirects, + Map requestParameters) + throws IOException, URISyntaxException { /** * Tor Project modified the way the connection object was created. For the sake of * simplicity, instead of duplicating the whole file we changed the connection object @@ -404,14 +525,18 @@ public class DefaultHttpDataSource implements HttpDataSource { connection.setConnectTimeout(connectTimeoutMillis); connection.setReadTimeout(readTimeoutMillis); + + Map requestHeaders = new HashMap<>(); if (defaultRequestProperties != null) { - for (Map.Entry property : defaultRequestProperties.getSnapshot().entrySet()) { - connection.setRequestProperty(property.getKey(), property.getValue()); - } + requestHeaders.putAll(defaultRequestProperties.getSnapshot()); } - for (Map.Entry property : requestProperties.getSnapshot().entrySet()) { + requestHeaders.putAll(requestProperties.getSnapshot()); + requestHeaders.putAll(requestParameters); + + for (Map.Entry property : requestHeaders.entrySet()) { connection.setRequestProperty(property.getKey(), property.getValue()); } + if (!(position == 0 && length == C.LENGTH_UNSET)) { String rangeRequest = "bytes=" + position + "-"; if (length != C.LENGTH_UNSET) { @@ -420,28 +545,29 @@ public class DefaultHttpDataSource implements HttpDataSource { connection.setRequestProperty("Range", rangeRequest); } connection.setRequestProperty("User-Agent", userAgent); - if (!allowGzip) { - connection.setRequestProperty("Accept-Encoding", "identity"); - } + connection.setRequestProperty("Accept-Encoding", allowGzip ? "gzip" : "identity"); connection.setInstanceFollowRedirects(followRedirects); - connection.setDoOutput(postBody != null); - if (postBody != null) { - connection.setRequestMethod("POST"); - if (postBody.length == 0) { - connection.connect(); - } else { - connection.setFixedLengthStreamingMode(postBody.length); - connection.connect(); - OutputStream os = connection.getOutputStream(); - os.write(postBody); - os.close(); - } + connection.setDoOutput(httpBody != null); + connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod)); + + if (httpBody != null) { + connection.setFixedLengthStreamingMode(httpBody.length); + connection.connect(); + OutputStream os = connection.getOutputStream(); + os.write(httpBody); + os.close(); } else { connection.connect(); } return connection; } + /** Creates an {@link HttpURLConnection} that is connected with the {@code url}. */ + @VisibleForTesting + /* package */ HttpURLConnection openConnection(URL url) throws IOException { + return (HttpURLConnection) url.openConnection(); + } + /** * Handles a redirect. * @@ -537,16 +663,14 @@ public class DefaultHttpDataSource implements HttpDataSource { while (bytesSkipped != bytesToSkip) { int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length); int read = inputStream.read(skipBuffer, 0, readLength); - if (Thread.interrupted()) { + if (Thread.currentThread().isInterrupted()) { throw new InterruptedIOException(); } if (read == -1) { throw new EOFException(); } bytesSkipped += read; - if (listener != null) { - listener.onBytesTransferred(this, read); - } + bytesTransferred(read); } // Release the shared skip buffer. @@ -589,9 +713,7 @@ public class DefaultHttpDataSource implements HttpDataSource { } bytesRead += read; - if (listener != null) { - listener.onBytesTransferred(this, read); - } + bytesTransferred(read); return read; } @@ -624,9 +746,9 @@ public class DefaultHttpDataSource implements HttpDataSource { return; } String className = inputStream.getClass().getName(); - if (className.equals("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream") - || className.equals( - "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream")) { + if ("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream".equals(className) + || "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream" + .equals(className)) { Class superclass = inputStream.getClass().getSuperclass(); Method unexpectedEndOfInput = superclass.getDeclaredMethod("unexpectedEndOfInput"); unexpectedEndOfInput.setAccessible(true); @@ -654,4 +776,8 @@ public class DefaultHttpDataSource implements HttpDataSource { } } + private static boolean isCompressed(HttpURLConnection connection) { + String contentEncoding = connection.getHeaderField("Content-Encoding"); + return "gzip".equalsIgnoreCase(contentEncoding); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java index 0d268580c5a6..cf7448fbd093 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java @@ -15,14 +15,16 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource.Factory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; /** A {@link Factory} that produces {@link DefaultHttpDataSource} instances. */ public final class DefaultHttpDataSourceFactory extends BaseFactory { private final String userAgent; - private final TransferListener listener; + @Nullable private final TransferListener listener; private final int connectTimeoutMillis; private final int readTimeoutMillis; private final boolean allowCrossProtocolRedirects; @@ -49,12 +51,33 @@ public final class DefaultHttpDataSourceFactory extends BaseFactory { * @param listener An optional listener. * @see #DefaultHttpDataSourceFactory(String, TransferListener, int, int, boolean) */ - public DefaultHttpDataSourceFactory( - String userAgent, TransferListener listener) { + public DefaultHttpDataSourceFactory(String userAgent, @Nullable TransferListener listener) { this(userAgent, listener, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, false); } + /** + * @param userAgent The User-Agent string that should be used. + * @param connectTimeoutMillis The connection timeout that should be used when requesting remote + * data, in milliseconds. A timeout of zero is interpreted as an infinite timeout. + * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in + * milliseconds. A timeout of zero is interpreted as an infinite timeout. + * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP + * to HTTPS and vice versa) are enabled. + */ + public DefaultHttpDataSourceFactory( + String userAgent, + int connectTimeoutMillis, + int readTimeoutMillis, + boolean allowCrossProtocolRedirects) { + this( + userAgent, + /* listener= */ null, + connectTimeoutMillis, + readTimeoutMillis, + allowCrossProtocolRedirects); + } + /** * @param userAgent The User-Agent string that should be used. * @param listener An optional listener. @@ -65,10 +88,13 @@ public final class DefaultHttpDataSourceFactory extends BaseFactory { * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled. */ - public DefaultHttpDataSourceFactory(String userAgent, - TransferListener listener, int connectTimeoutMillis, - int readTimeoutMillis, boolean allowCrossProtocolRedirects) { - this.userAgent = userAgent; + public DefaultHttpDataSourceFactory( + String userAgent, + @Nullable TransferListener listener, + int connectTimeoutMillis, + int readTimeoutMillis, + boolean allowCrossProtocolRedirects) { + this.userAgent = Assertions.checkNotEmpty(userAgent); this.listener = listener; this.connectTimeoutMillis = connectTimeoutMillis; this.readTimeoutMillis = readTimeoutMillis; @@ -78,8 +104,16 @@ public final class DefaultHttpDataSourceFactory extends BaseFactory { @Override protected DefaultHttpDataSource createDataSourceInternal( HttpDataSource.RequestProperties defaultRequestProperties) { - return new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis, - readTimeoutMillis, allowCrossProtocolRedirects, defaultRequestProperties); + DefaultHttpDataSource dataSource = + new DefaultHttpDataSource( + userAgent, + connectTimeoutMillis, + readTimeoutMillis, + allowCrossProtocolRedirects, + defaultRequestProperties); + if (listener != null) { + dataSource.addTransferListener(listener); + } + return dataSource; } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java new file mode 100644 index 000000000000..082014b7efb2 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.UnexpectedLoaderException; +import java.io.FileNotFoundException; +import java.io.IOException; + +/** Default implementation of {@link LoadErrorHandlingPolicy}. */ +public class DefaultLoadErrorHandlingPolicy implements LoadErrorHandlingPolicy { + + /** The default minimum number of times to retry loading data prior to propagating the error. */ + public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; + /** + * The default minimum number of times to retry loading prior to failing for progressive live + * streams. + */ + public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_PROGRESSIVE_LIVE = 6; + /** The default duration for which a track is blacklisted in milliseconds. */ + public static final long DEFAULT_TRACK_BLACKLIST_MS = 60000; + + private static final int DEFAULT_BEHAVIOR_MIN_LOADABLE_RETRY_COUNT = -1; + + private final int minimumLoadableRetryCount; + + /** + * Creates an instance with default behavior. + * + *

{@link #getMinimumLoadableRetryCount} will return {@link + * #DEFAULT_MIN_LOADABLE_RETRY_COUNT_PROGRESSIVE_LIVE} for {@code dataType} {@link + * C#DATA_TYPE_MEDIA_PROGRESSIVE_LIVE}. For other {@code dataType} values, it will return {@link + * #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + */ + public DefaultLoadErrorHandlingPolicy() { + this(DEFAULT_BEHAVIOR_MIN_LOADABLE_RETRY_COUNT); + } + + /** + * Creates an instance with the given value for {@link #getMinimumLoadableRetryCount(int)}. + * + * @param minimumLoadableRetryCount See {@link #getMinimumLoadableRetryCount}. + */ + public DefaultLoadErrorHandlingPolicy(int minimumLoadableRetryCount) { + this.minimumLoadableRetryCount = minimumLoadableRetryCount; + } + + /** + * Blacklists resources whose load error was an {@link InvalidResponseCodeException} with response + * code HTTP 404 or 410. The duration of the blacklisting is {@link #DEFAULT_TRACK_BLACKLIST_MS}. + */ + @Override + public long getBlacklistDurationMsFor( + int dataType, long loadDurationMs, IOException exception, int errorCount) { + if (exception instanceof InvalidResponseCodeException) { + int responseCode = ((InvalidResponseCodeException) exception).responseCode; + return responseCode == 404 // HTTP 404 Not Found. + || responseCode == 410 // HTTP 410 Gone. + || responseCode == 416 // HTTP 416 Range Not Satisfiable. + ? DEFAULT_TRACK_BLACKLIST_MS + : C.TIME_UNSET; + } + return C.TIME_UNSET; + } + + /** + * Retries for any exception that is not a subclass of {@link ParserException}, {@link + * FileNotFoundException} or {@link UnexpectedLoaderException}. The retry delay is calculated as + * {@code Math.min((errorCount - 1) * 1000, 5000)}. + */ + @Override + public long getRetryDelayMsFor( + int dataType, long loadDurationMs, IOException exception, int errorCount) { + return exception instanceof ParserException + || exception instanceof FileNotFoundException + || exception instanceof UnexpectedLoaderException + ? C.TIME_UNSET + : Math.min((errorCount - 1) * 1000, 5000); + } + + /** + * See {@link #DefaultLoadErrorHandlingPolicy()} and {@link #DefaultLoadErrorHandlingPolicy(int)} + * for documentation about the behavior of this method. + */ + @Override + public int getMinimumLoadableRetryCount(int dataType) { + if (minimumLoadableRetryCount == DEFAULT_BEHAVIOR_MIN_LOADABLE_RETRY_COUNT) { + return dataType == C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE + ? DEFAULT_MIN_LOADABLE_RETRY_COUNT_PROGRESSIVE_LIVE + : DEFAULT_MIN_LOADABLE_RETRY_COUNT; + } else { + return minimumLoadableRetryCount; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DummyDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DummyDataSource.java index f1801e372337..585c37cc7800 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DummyDataSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DummyDataSource.java @@ -16,6 +16,7 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; import android.net.Uri; +import androidx.annotation.Nullable; import java.io.IOException; /** @@ -25,34 +26,34 @@ public final class DummyDataSource implements DataSource { public static final DummyDataSource INSTANCE = new DummyDataSource(); - /** A factory that that produces {@link DummyDataSource}. */ - public static final Factory FACTORY = new Factory() { - @Override - public DataSource createDataSource() { - return new DummyDataSource(); - } - }; + /** A factory that produces {@link DummyDataSource}. */ + public static final Factory FACTORY = DummyDataSource::new; private DummyDataSource() {} + @Override + public void addTransferListener(TransferListener transferListener) { + // Do nothing. + } + @Override public long open(DataSpec dataSpec) throws IOException { throw new IOException("Dummy source"); } @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { + public int read(byte[] buffer, int offset, int readLength) { throw new UnsupportedOperationException(); } @Override + @Nullable public Uri getUri() { return null; } @Override - public void close() throws IOException { + public void close() { // do nothing. } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSource.java index 795b0f30656b..eee30e668f46 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSource.java @@ -15,51 +15,78 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.net.Uri; +import android.text.TextUtils; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; -/** - * A {@link DataSource} for reading local files. - */ -public final class FileDataSource implements DataSource { +/** A {@link DataSource} for reading local files. */ +public final class FileDataSource extends BaseDataSource { - /** - * Thrown when IOException is encountered during local file read operation. - */ + /** Thrown when a {@link FileDataSource} encounters an error reading a file. */ public static class FileDataSourceException extends IOException { public FileDataSourceException(IOException cause) { super(cause); } + public FileDataSourceException(String message, IOException cause) { + super(message, cause); + } } - private final TransferListener listener; + /** {@link DataSource.Factory} for {@link FileDataSource} instances. */ + public static final class Factory implements DataSource.Factory { - private RandomAccessFile file; - private Uri uri; + @Nullable private TransferListener listener; + + /** + * Sets a {@link TransferListener} for {@link FileDataSource} instances created by this factory. + * + * @param listener The {@link TransferListener}. + * @return This factory. + */ + public Factory setListener(@Nullable TransferListener listener) { + this.listener = listener; + return this; + } + + @Override + public FileDataSource createDataSource() { + FileDataSource dataSource = new FileDataSource(); + if (listener != null) { + dataSource.addTransferListener(listener); + } + return dataSource; + } + } + + @Nullable private RandomAccessFile file; + @Nullable private Uri uri; private long bytesRemaining; private boolean opened; public FileDataSource() { - this(null); - } - - /** - * @param listener An optional listener. - */ - public FileDataSource(TransferListener listener) { - this.listener = listener; + super(/* isNetwork= */ false); } @Override public long open(DataSpec dataSpec) throws FileDataSourceException { try { - uri = dataSpec.uri; - file = new RandomAccessFile(dataSpec.uri.getPath(), "r"); + Uri uri = dataSpec.uri; + this.uri = uri; + + transferInitializing(dataSpec); + + this.file = openLocalFile(uri); + file.seek(dataSpec.position); bytesRemaining = dataSpec.length == C.LENGTH_UNSET ? file.length() - dataSpec.position : dataSpec.length; @@ -71,13 +98,28 @@ public final class FileDataSource implements DataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesRemaining; } + private static RandomAccessFile openLocalFile(Uri uri) throws FileDataSourceException { + try { + return new RandomAccessFile(Assertions.checkNotNull(uri.getPath()), "r"); + } catch (FileNotFoundException e) { + if (!TextUtils.isEmpty(uri.getQuery()) || !TextUtils.isEmpty(uri.getFragment())) { + throw new FileDataSourceException( + String.format( + "uri has query and/or fragment, which are not supported. Did you call Uri.parse()" + + " on a string containing '?' or '#'? Use Uri.fromFile(new File(path)) to" + + " avoid this. path=%s,query=%s,fragment=%s", + uri.getPath(), uri.getQuery(), uri.getFragment()), + e); + } + throw new FileDataSourceException(e); + } + } + @Override public int read(byte[] buffer, int offset, int readLength) throws FileDataSourceException { if (readLength == 0) { @@ -87,16 +129,15 @@ public final class FileDataSource implements DataSource { } else { int bytesRead; try { - bytesRead = file.read(buffer, offset, (int) Math.min(bytesRemaining, readLength)); + bytesRead = + castNonNull(file).read(buffer, offset, (int) Math.min(bytesRemaining, readLength)); } catch (IOException e) { throw new FileDataSourceException(e); } if (bytesRead > 0) { bytesRemaining -= bytesRead; - if (listener != null) { - listener.onBytesTransferred(this, bytesRead); - } + bytesTransferred(bytesRead); } return bytesRead; @@ -104,6 +145,7 @@ public final class FileDataSource implements DataSource { } @Override + @Nullable public Uri getUri() { return uri; } @@ -121,9 +163,7 @@ public final class FileDataSource implements DataSource { file = null; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java index 94a8bf2d4bc0..660a38161cf3 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java @@ -15,24 +15,24 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; -/** - * A {@link DataSource.Factory} that produces {@link FileDataSource}. - */ +import androidx.annotation.Nullable; + +/** @deprecated Use {@link FileDataSource.Factory}. */ +@Deprecated public final class FileDataSourceFactory implements DataSource.Factory { - private final TransferListener listener; + private final FileDataSource.Factory wrappedFactory; public FileDataSourceFactory() { - this(null); + this(/* listener= */ null); } - public FileDataSourceFactory(TransferListener listener) { - this.listener = listener; + public FileDataSourceFactory(@Nullable TransferListener listener) { + wrappedFactory = new FileDataSource.Factory().setListener(listener); } @Override - public DataSource createDataSource() { - return new FileDataSource(listener); + public FileDataSource createDataSource() { + return wrappedFactory.createDataSource(); } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/HttpDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/HttpDataSource.java index 7381f192cba1..ffac1ca89392 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/HttpDataSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/HttpDataSource.java @@ -15,11 +15,13 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; -import android.support.annotation.IntDef; import android.text.TextUtils; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Predicate; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; @@ -181,18 +183,21 @@ public interface HttpDataSource extends DataSource { return defaultRequestProperties; } + /** @deprecated Use {@link #getDefaultRequestProperties} instead. */ @Deprecated @Override public final void setDefaultRequestProperty(String name, String value) { defaultRequestProperties.set(name, value); } + /** @deprecated Use {@link #getDefaultRequestProperties} instead. */ @Deprecated @Override public final void clearDefaultRequestProperty(String name) { defaultRequestProperties.remove(name); } + /** @deprecated Use {@link #getDefaultRequestProperties} instead. */ @Deprecated @Override public final void clearAllDefaultRequestProperties() { @@ -211,29 +216,26 @@ public interface HttpDataSource extends DataSource { } - /** - * A {@link Predicate} that rejects content types often used for pay-walls. - */ - Predicate REJECT_PAYWALL_TYPES = new Predicate() { - - @Override - public boolean evaluate(String contentType) { - contentType = Util.toLowerInvariant(contentType); - return !TextUtils.isEmpty(contentType) - && (!contentType.contains("text") || contentType.contains("text/vtt")) - && !contentType.contains("html") && !contentType.contains("xml"); - } - - }; + /** A {@link Predicate} that rejects content types often used for pay-walls. */ + Predicate REJECT_PAYWALL_TYPES = + contentType -> { + contentType = Util.toLowerInvariant(contentType); + return !TextUtils.isEmpty(contentType) + && (!contentType.contains("text") || contentType.contains("text/vtt")) + && !contentType.contains("html") + && !contentType.contains("xml"); + }; /** * Thrown when an error is encountered when trying to read from a {@link HttpDataSource}. */ class HttpDataSourceException extends IOException { + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({TYPE_OPEN, TYPE_READ, TYPE_CLOSE}) public @interface Type {} + public static final int TYPE_OPEN = 1; public static final int TYPE_READ = 2; public static final int TYPE_CLOSE = 3; @@ -296,20 +298,41 @@ public interface HttpDataSource extends DataSource { */ public final int responseCode; + /** The http status message. */ + @Nullable public final String responseMessage; + /** * An unmodifiable map of the response header fields and values. */ public final Map> headerFields; - public InvalidResponseCodeException(int responseCode, Map> headerFields, + /** @deprecated Use {@link #InvalidResponseCodeException(int, String, Map, DataSpec)}. */ + @Deprecated + public InvalidResponseCodeException( + int responseCode, Map> headerFields, DataSpec dataSpec) { + this(responseCode, /* responseMessage= */ null, headerFields, dataSpec); + } + + public InvalidResponseCodeException( + int responseCode, + @Nullable String responseMessage, + Map> headerFields, DataSpec dataSpec) { super("Response code: " + responseCode, dataSpec, TYPE_OPEN); this.responseCode = responseCode; + this.responseMessage = responseMessage; this.headerFields = headerFields; } } + /** + * Opens the source to read the specified data. + * + *

Note: {@link HttpDataSource} implementations are advised to set request headers passed via + * (in order of decreasing priority) the {@code dataSpec}, {@link #setRequestProperty} and the + * default parameters set in the {@link Factory}. + */ @Override long open(DataSpec dataSpec) throws HttpDataSourceException; @@ -323,6 +346,10 @@ public interface HttpDataSource extends DataSource { * Sets the value of a request header. The value will be used for subsequent connections * established by the source. * + *

Note: If the same header is set as a default parameter in the {@link Factory}, then the + * header value set with this method should be preferred when connecting with the data source. See + * {@link #open}. + * * @param name The name of the header field. * @param value The value of the field. */ @@ -342,9 +369,11 @@ public interface HttpDataSource extends DataSource { void clearAllRequestProperties(); /** - * Returns the headers provided in the response, or {@code null} if response headers are - * unavailable. + * When the source is open, returns the HTTP response status code associated with the last {@link + * #open} call. Otherwise, returns a negative value. */ - Map> getResponseHeaders(); + int getResponseCode(); + @Override + Map> getResponseHeaders(); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java new file mode 100644 index 000000000000..03c861c5f1d1 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.Callback; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.Loadable; +import java.io.IOException; + +/** + * Defines how errors encountered by {@link Loader Loaders} are handled. + * + *

Loader clients may blacklist a resource when a load error occurs. Blacklisting works around + * load errors by loading an alternative resource. Clients do not try blacklisting when a resource + * does not have an alternative. When a resource does have valid alternatives, {@link + * #getBlacklistDurationMsFor(int, long, IOException, int)} defines whether the resource should be + * blacklisted. Blacklisting will succeed if any of the alternatives is not in the black list. + * + *

When blacklisting does not take place, {@link #getRetryDelayMsFor(int, long, IOException, + * int)} defines whether the load is retried. Errors whose load is not retried are propagated. Load + * errors whose load is retried are propagated according to {@link + * #getMinimumLoadableRetryCount(int)}. + * + *

Methods are invoked on the playback thread. + */ +public interface LoadErrorHandlingPolicy { + + /** + * Returns the number of milliseconds for which a resource associated to a provided load error + * should be blacklisted, or {@link C#TIME_UNSET} if the resource should not be blacklisted. + * + * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to + * load. + * @param loadDurationMs The duration in milliseconds of the load from the start of the first load + * attempt up to the point at which the error occurred. + * @param exception The load error. + * @param errorCount The number of errors this load has encountered, including this one. + * @return The blacklist duration in milliseconds, or {@link C#TIME_UNSET} if the resource should + * not be blacklisted. + */ + long getBlacklistDurationMsFor( + int dataType, long loadDurationMs, IOException exception, int errorCount); + + /** + * Returns the number of milliseconds to wait before attempting the load again, or {@link + * C#TIME_UNSET} if the error is fatal and should not be retried. + * + *

{@link Loader} clients may ignore the retry delay returned by this method in order to wait + * for a specific event before retrying. However, the load is retried if and only if this method + * does not return {@link C#TIME_UNSET}. + * + * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to + * load. + * @param loadDurationMs The duration in milliseconds of the load from the start of the first load + * attempt up to the point at which the error occurred. + * @param exception The load error. + * @param errorCount The number of errors this load has encountered, including this one. + * @return The number of milliseconds to wait before attempting the load again, or {@link + * C#TIME_UNSET} if the error is fatal and should not be retried. + */ + long getRetryDelayMsFor(int dataType, long loadDurationMs, IOException exception, int errorCount); + + /** + * Returns the minimum number of times to retry a load in the case of a load error, before + * propagating the error. + * + * @param dataType One of the {@link C C.DATA_TYPE_*} constants indicating the type of data to + * load. + * @return The minimum number of times to retry a load in the case of a load error, before + * propagating the error. + * @see Loader#startLoading(Loadable, Callback, int) + */ + int getMinimumLoadableRetryCount(int dataType); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Loader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Loader.java index dd9010c64717..0e79759b3671 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Loader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Loader.java @@ -20,11 +20,17 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.SystemClock; -import android.util.Log; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TraceUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.concurrent.ExecutorService; /** @@ -33,11 +39,11 @@ import java.util.concurrent.ExecutorService; public final class Loader implements LoaderErrorThrower { /** - * Thrown when an unexpected exception is encountered during loading. + * Thrown when an unexpected exception or error is encountered during loading. */ public static final class UnexpectedLoaderException extends IOException { - public UnexpectedLoaderException(Exception cause) { + public UnexpectedLoaderException(Throwable cause) { super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause); } @@ -53,16 +59,11 @@ public final class Loader implements LoaderErrorThrower { */ void cancelLoad(); - /** - * Returns whether the load has been canceled. - */ - boolean isLoadCanceled(); - /** * Performs the load, returning on completion or cancellation. * - * @throws IOException - * @throws InterruptedException + * @throws IOException If the input could not be loaded. + * @throws InterruptedException If the thread was interrupted. */ void load() throws IOException, InterruptedException; @@ -75,27 +76,29 @@ public final class Loader implements LoaderErrorThrower { /** * Called when a load has completed. - *

- * Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting and - * this callback being called. + * + *

Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting + * and this callback being called. * * @param loadable The loadable whose load has completed. * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the load ended. - * @param loadDurationMs The duration of the load. + * @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading} + * was called. */ void onLoadCompleted(T loadable, long elapsedRealtimeMs, long loadDurationMs); /** * Called when a load has been canceled. - *

- * Note: If the {@link Loader} has not been released then there is guaranteed to be a memory - * barrier between {@link Loadable#load()} exiting and this callback being called. If the - * {@link Loader} has been released then this callback may be called before - * {@link Loadable#load()} exits. + * + *

Note: If the {@link Loader} has not been released then there is guaranteed to be a memory + * barrier between {@link Loadable#load()} exiting and this callback being called. If the {@link + * Loader} has been released then this callback may be called before {@link Loadable#load()} + * exits. * * @param loadable The loadable whose load has been canceled. * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the load was canceled. - * @param loadDurationMs The duration of the load up to the point at which it was canceled. + * @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading} + * was called up to the point at which it was canceled. * @param released True if the load was canceled because the {@link Loader} was released. False * otherwise. */ @@ -103,37 +106,92 @@ public final class Loader implements LoaderErrorThrower { /** * Called when a load encounters an error. - *

- * Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting and - * this callback being called. + * + *

Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting + * and this callback being called. * * @param loadable The loadable whose load has encountered an error. * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the error occurred. - * @param loadDurationMs The duration of the load up to the point at which the error occurred. + * @param loadDurationMs The duration in milliseconds of the load since {@link #startLoading} + * was called up to the point at which the error occurred. * @param error The load error. - * @return The desired retry action. One of {@link Loader#RETRY}, - * {@link Loader#RETRY_RESET_ERROR_COUNT}, {@link Loader#DONT_RETRY} and - * {@link Loader#DONT_RETRY_FATAL}. + * @param errorCount The number of errors this load has encountered, including this one. + * @return The desired error handling action. One of {@link Loader#RETRY}, {@link + * Loader#RETRY_RESET_ERROR_COUNT}, {@link Loader#DONT_RETRY}, {@link + * Loader#DONT_RETRY_FATAL} or a retry action created by {@link #createRetryAction}. */ - int onLoadError(T loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error); + LoadErrorAction onLoadError( + T loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error, int errorCount); + } + + /** + * A callback to be notified when a {@link Loader} has finished being released. + */ + public interface ReleaseCallback { + + /** + * Called when the {@link Loader} has finished being released. + */ + void onLoaderReleased(); } - public static final int RETRY = 0; - public static final int RETRY_RESET_ERROR_COUNT = 1; - public static final int DONT_RETRY = 2; - public static final int DONT_RETRY_FATAL = 3; + /** Types of action that can be taken in response to a load error. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + ACTION_TYPE_RETRY, + ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT, + ACTION_TYPE_DONT_RETRY, + ACTION_TYPE_DONT_RETRY_FATAL + }) + private @interface RetryActionType {} - private static final int MSG_START = 0; - private static final int MSG_CANCEL = 1; - private static final int MSG_END_OF_SOURCE = 2; - private static final int MSG_IO_EXCEPTION = 3; - private static final int MSG_FATAL_ERROR = 4; + private static final int ACTION_TYPE_RETRY = 0; + private static final int ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT = 1; + private static final int ACTION_TYPE_DONT_RETRY = 2; + private static final int ACTION_TYPE_DONT_RETRY_FATAL = 3; + + /** Retries the load using the default delay. */ + public static final LoadErrorAction RETRY = + createRetryAction(/* resetErrorCount= */ false, C.TIME_UNSET); + /** Retries the load using the default delay and resets the error count. */ + public static final LoadErrorAction RETRY_RESET_ERROR_COUNT = + createRetryAction(/* resetErrorCount= */ true, C.TIME_UNSET); + /** Discards the failed {@link Loadable} and ignores any errors that have occurred. */ + public static final LoadErrorAction DONT_RETRY = + new LoadErrorAction(ACTION_TYPE_DONT_RETRY, C.TIME_UNSET); + /** + * Discards the failed {@link Loadable}. The next call to {@link #maybeThrowError()} will throw + * the last load error. + */ + public static final LoadErrorAction DONT_RETRY_FATAL = + new LoadErrorAction(ACTION_TYPE_DONT_RETRY_FATAL, C.TIME_UNSET); + + /** + * Action that can be taken in response to {@link Callback#onLoadError(Loadable, long, long, + * IOException, int)}. + */ + public static final class LoadErrorAction { + + private final @RetryActionType int type; + private final long retryDelayMillis; + + private LoadErrorAction(@RetryActionType int type, long retryDelayMillis) { + this.type = type; + this.retryDelayMillis = retryDelayMillis; + } + + /** Returns whether this is a retry action. */ + public boolean isRetry() { + return type == ACTION_TYPE_RETRY || type == ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT; + } + } private final ExecutorService downloadExecutorService; - private LoadTask currentTask; - private IOException fatalError; + @Nullable private LoadTask currentTask; + @Nullable private IOException fatalError; /** * @param threadName A name for the loader's thread. @@ -142,64 +200,86 @@ public final class Loader implements LoaderErrorThrower { this.downloadExecutorService = Util.newSingleThreadExecutor(threadName); } + /** + * Creates a {@link LoadErrorAction} for retrying with the given parameters. + * + * @param resetErrorCount Whether the previous error count should be set to zero. + * @param retryDelayMillis The number of milliseconds to wait before retrying. + * @return A {@link LoadErrorAction} for retrying with the given parameters. + */ + public static LoadErrorAction createRetryAction(boolean resetErrorCount, long retryDelayMillis) { + return new LoadErrorAction( + resetErrorCount ? ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT : ACTION_TYPE_RETRY, + retryDelayMillis); + } + + /** + * Whether the last call to {@link #startLoading} resulted in a fatal error. Calling {@link + * #maybeThrowError()} will throw the fatal error. + */ + public boolean hasFatalError() { + return fatalError != null; + } + + /** Clears any stored fatal error. */ + public void clearFatalError() { + fatalError = null; + } + /** * Starts loading a {@link Loadable}. - *

- * The calling thread must be a {@link Looper} thread, which is the thread on which the - * {@link Callback} will be called. + * + *

The calling thread must be a {@link Looper} thread, which is the thread on which the {@link + * Callback} will be called. * * @param The type of the loadable. * @param loadable The {@link Loadable} to load. - * @param callback A callback to called when the load ends. - * @param defaultMinRetryCount The minimum number of times the load must be retried before - * {@link #maybeThrowError()} will propagate an error. + * @param callback A callback to be called when the load ends. + * @param defaultMinRetryCount The minimum number of times the load must be retried before {@link + * #maybeThrowError()} will propagate an error. * @throws IllegalStateException If the calling thread does not have an associated {@link Looper}. * @return {@link SystemClock#elapsedRealtime} when the load started. */ - public long startLoading(T loadable, Callback callback, - int defaultMinRetryCount) { - Looper looper = Looper.myLooper(); - Assertions.checkState(looper != null); + public long startLoading( + T loadable, Callback callback, int defaultMinRetryCount) { + Looper looper = Assertions.checkStateNotNull(Looper.myLooper()); + fatalError = null; long startTimeMs = SystemClock.elapsedRealtime(); new LoadTask<>(looper, loadable, callback, defaultMinRetryCount, startTimeMs).start(0); return startTimeMs; } - /** - * Returns whether the {@link Loader} is currently loading a {@link Loadable}. - */ + /** Returns whether the loader is currently loading. */ public boolean isLoading() { return currentTask != null; } /** - * Cancels the current load. This method should only be called when a load is in progress. + * Cancels the current load. + * + * @throws IllegalStateException If the loader is not currently loading. */ public void cancelLoading() { - currentTask.cancel(false); + Assertions.checkStateNotNull(currentTask).cancel(false); } - /** - * Releases the {@link Loader}. This method should be called when the {@link Loader} is no longer - * required. - */ + /** Releases the loader. This method should be called when the loader is no longer required. */ public void release() { release(null); } /** - * Releases the {@link Loader}, running {@code postLoadAction} on its thread. This method should - * be called when the {@link Loader} is no longer required. + * Releases the loader. This method should be called when the loader is no longer required. * - * @param postLoadAction A {@link Runnable} to run on the loader's thread when - * {@link Loadable#load()} is no longer running. + * @param callback An optional callback to be called on the loading thread once the loader has + * been released. */ - public void release(Runnable postLoadAction) { + public void release(@Nullable ReleaseCallback callback) { if (currentTask != null) { currentTask.cancel(true); } - if (postLoadAction != null) { - downloadExecutorService.execute(postLoadAction); + if (callback != null) { + downloadExecutorService.execute(new ReleaseTask(callback)); } downloadExecutorService.shutdown(); } @@ -228,15 +308,23 @@ public final class Loader implements LoaderErrorThrower { private static final String TAG = "LoadTask"; - private final T loadable; - private final Loader.Callback callback; + private static final int MSG_START = 0; + private static final int MSG_CANCEL = 1; + private static final int MSG_END_OF_SOURCE = 2; + private static final int MSG_IO_EXCEPTION = 3; + private static final int MSG_FATAL_ERROR = 4; + public final int defaultMinRetryCount; + + private final T loadable; private final long startTimeMs; - private IOException currentError; + @Nullable private Loader.Callback callback; + @Nullable private IOException currentError; private int errorCount; - private volatile Thread executorThread; + @Nullable private volatile Thread executorThread; + private volatile boolean canceled; private volatile boolean released; public LoadTask(Looper looper, T loadable, Loader.Callback callback, @@ -273,7 +361,9 @@ public final class Loader implements LoaderErrorThrower { sendEmptyMessage(MSG_CANCEL); } } else { + canceled = true; loadable.cancelLoad(); + Thread executorThread = this.executorThread; if (executorThread != null) { executorThread.interrupt(); } @@ -281,7 +371,13 @@ public final class Loader implements LoaderErrorThrower { if (released) { finish(); long nowMs = SystemClock.elapsedRealtime(); - callback.onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true); + Assertions.checkNotNull(callback) + .onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true); + // If loading, this task will be referenced from a GC root (the loading thread) until + // cancellation completes. The time taken for cancellation to complete depends on the + // implementation of the Loadable that the task is loading. We null the callback reference + // here so that it doesn't prevent garbage collection whilst cancellation is ongoing. + callback = null; } } @@ -289,7 +385,7 @@ public final class Loader implements LoaderErrorThrower { public void run() { try { executorThread = Thread.currentThread(); - if (!loadable.isLoadCanceled()) { + if (!canceled) { TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName()); try { loadable.load(); @@ -306,7 +402,7 @@ public final class Loader implements LoaderErrorThrower { } } catch (InterruptedException e) { // The load was canceled. - Assertions.checkState(loadable.isLoadCanceled()); + Assertions.checkState(canceled); if (!released) { sendEmptyMessage(MSG_END_OF_SOURCE); } @@ -316,6 +412,14 @@ public final class Loader implements LoaderErrorThrower { if (!released) { obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget(); } + } catch (OutOfMemoryError e) { + // This can occur if a stream is malformed in a way that causes an extractor to think it + // needs to allocate a large amount of memory. We don't want the process to die in this + // case, but we do want the playback to fail. + Log.e(TAG, "OutOfMemory error loading stream", e); + if (!released) { + obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget(); + } } catch (Error e) { // We'd hope that the platform would kill the process if an Error is thrown here, but the // executor may catch the error (b/20616433). Throw it here, but also pass and throw it from @@ -343,7 +447,8 @@ public final class Loader implements LoaderErrorThrower { finish(); long nowMs = SystemClock.elapsedRealtime(); long durationMs = nowMs - startTimeMs; - if (loadable.isLoadCanceled()) { + Loader.Callback callback = Assertions.checkNotNull(this.callback); + if (canceled) { callback.onLoadCanceled(loadable, nowMs, durationMs, false); return; } @@ -352,24 +457,40 @@ public final class Loader implements LoaderErrorThrower { callback.onLoadCanceled(loadable, nowMs, durationMs, false); break; case MSG_END_OF_SOURCE: - callback.onLoadCompleted(loadable, nowMs, durationMs); + try { + callback.onLoadCompleted(loadable, nowMs, durationMs); + } catch (RuntimeException e) { + // This should never happen, but handle it anyway. + Log.e(TAG, "Unexpected exception handling load completed", e); + fatalError = new UnexpectedLoaderException(e); + } break; case MSG_IO_EXCEPTION: currentError = (IOException) msg.obj; - int retryAction = callback.onLoadError(loadable, nowMs, durationMs, currentError); - if (retryAction == DONT_RETRY_FATAL) { + errorCount++; + LoadErrorAction action = + callback.onLoadError(loadable, nowMs, durationMs, currentError, errorCount); + if (action.type == ACTION_TYPE_DONT_RETRY_FATAL) { fatalError = currentError; - } else if (retryAction != DONT_RETRY) { - errorCount = retryAction == RETRY_RESET_ERROR_COUNT ? 1 : errorCount + 1; - start(getRetryDelayMillis()); + } else if (action.type != ACTION_TYPE_DONT_RETRY) { + if (action.type == ACTION_TYPE_RETRY_AND_RESET_ERROR_COUNT) { + errorCount = 1; + } + start( + action.retryDelayMillis != C.TIME_UNSET + ? action.retryDelayMillis + : getRetryDelayMillis()); } break; + default: + // Never happens. + break; } } private void execute() { currentError = null; - downloadExecutorService.execute(currentTask); + downloadExecutorService.execute(Assertions.checkNotNull(currentTask)); } private void finish() { @@ -382,4 +503,19 @@ public final class Loader implements LoaderErrorThrower { } + private static final class ReleaseTask implements Runnable { + + private final ReleaseCallback callback; + + public ReleaseTask(ReleaseCallback callback) { + this.callback = callback; + } + + @Override + public void run() { + callback.onLoaderReleased(); + } + + } + } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ParsingLoadable.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ParsingLoadable.java index 5da9328f46d5..3e4192b65133 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ParsingLoadable.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ParsingLoadable.java @@ -16,12 +16,16 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; import android.net.Uri; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.Loadable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.io.InputStream; +import java.util.List; +import java.util.Map; /** * A {@link Loadable} for objects that can be parsed from binary data using a {@link Parser}. @@ -48,6 +52,41 @@ public final class ParsingLoadable implements Loadable { } + /** + * Loads a single parsable object. + * + * @param dataSource The {@link DataSource} through which the object should be read. + * @param parser The {@link Parser} to parse the object from the response. + * @param uri The {@link Uri} of the object to read. + * @param type The type of the data. One of the {@link C}{@code DATA_TYPE_*} constants. + * @return The parsed object + * @throws IOException Thrown if there is an error while loading or parsing. + */ + public static T load(DataSource dataSource, Parser parser, Uri uri, int type) + throws IOException { + ParsingLoadable loadable = new ParsingLoadable<>(dataSource, uri, type, parser); + loadable.load(); + return Assertions.checkNotNull(loadable.getResult()); + } + + /** + * Loads a single parsable object. + * + * @param dataSource The {@link DataSource} through which the object should be read. + * @param parser The {@link Parser} to parse the object from the response. + * @param dataSpec The {@link DataSpec} of the object to read. + * @param type The type of the data. One of the {@link C}{@code DATA_TYPE_*} constants. + * @return The parsed object + * @throws IOException Thrown if there is an error while loading or parsing. + */ + public static T load( + DataSource dataSource, Parser parser, DataSpec dataSpec, int type) + throws IOException { + ParsingLoadable loadable = new ParsingLoadable<>(dataSource, dataSpec, type, parser); + loadable.load(); + return Assertions.checkNotNull(loadable.getResult()); + } + /** * The {@link DataSpec} that defines the data to be loaded. */ @@ -58,12 +97,10 @@ public final class ParsingLoadable implements Loadable { */ public final int type; - private final DataSource dataSource; + private final StatsDataSource dataSource; private final Parser parser; - private volatile T result; - private volatile boolean isCanceled; - private volatile long bytesLoaded; + private volatile @Nullable T result; /** * @param dataSource A {@link DataSource} to use when loading the data. @@ -72,51 +109,69 @@ public final class ParsingLoadable implements Loadable { * @param parser Parses the object from the response. */ public ParsingLoadable(DataSource dataSource, Uri uri, int type, Parser parser) { - this.dataSource = dataSource; - this.dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP); + this(dataSource, new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP), type, parser); + } + + /** + * @param dataSource A {@link DataSource} to use when loading the data. + * @param dataSpec The {@link DataSpec} from which the object should be loaded. + * @param type See {@link #type}. + * @param parser Parses the object from the response. + */ + public ParsingLoadable(DataSource dataSource, DataSpec dataSpec, int type, + Parser parser) { + this.dataSource = new StatsDataSource(dataSource); + this.dataSpec = dataSpec; this.type = type; this.parser = parser; } - /** - * Returns the loaded object, or null if an object has not been loaded. - */ - public final T getResult() { + /** Returns the loaded object, or null if an object has not been loaded. */ + public final @Nullable T getResult() { return result; } /** * Returns the number of bytes loaded. In the case that the network response was compressed, the - * value returned is the size of the data after decompression. - * - * @return The number of bytes loaded. + * value returned is the size of the data after decompression. Must only be called after + * the load completed, failed, or was canceled. */ public long bytesLoaded() { - return bytesLoaded; + return dataSource.getBytesRead(); + } + + /** + * Returns the {@link Uri} from which data was read. If redirection occurred, this is the + * redirected uri. Must only be called after the load completed, failed, or was canceled. + */ + public Uri getUri() { + return dataSource.getLastOpenedUri(); + } + + /** + * Returns the response headers associated with the load. Must only be called after the load + * completed, failed, or was canceled. + */ + public Map> getResponseHeaders() { + return dataSource.getLastResponseHeaders(); } @Override public final void cancelLoad() { - // We don't actually cancel anything, but we need to record the cancellation so that - // isLoadCanceled can return the correct value. - isCanceled = true; + // Do nothing. } @Override - public final boolean isLoadCanceled() { - return isCanceled; - } - - @Override - public final void load() throws IOException, InterruptedException { + public final void load() throws IOException { + // We always load from the beginning, so reset bytesRead to 0. + dataSource.resetBytesRead(); DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); try { inputStream.open(); - result = parser.parse(dataSource.getUri(), inputStream); + Uri dataSourceUri = Assertions.checkNotNull(dataSource.getUri()); + result = parser.parse(dataSourceUri, inputStream); } finally { - bytesLoaded = inputStream.bytesRead(); Util.closeQuietly(inputStream); } } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSource.java index 7436eb3f0186..18a7fb623816 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSource.java @@ -16,9 +16,12 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; import android.net.Uri; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.PriorityTaskManager; import java.io.IOException; +import java.util.List; +import java.util.Map; /** * A {@link DataSource} that can be used as part of a task registered with a @@ -50,6 +53,11 @@ public final class PriorityDataSource implements DataSource { this.priority = priority; } + @Override + public void addTransferListener(TransferListener transferListener) { + upstream.addTransferListener(transferListener); + } + @Override public long open(DataSpec dataSpec) throws IOException { priorityTaskManager.proceedOrThrow(priority); @@ -63,10 +71,16 @@ public final class PriorityDataSource implements DataSource { } @Override + @Nullable public Uri getUri() { return upstream.getUri(); } + @Override + public Map> getResponseHeaders() { + return upstream.getResponseHeaders(); + } + @Override public void close() throws IOException { upstream.close(); diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/RawResourceDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/RawResourceDataSource.java index e4045508f666..ec5263d8ac6c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/RawResourceDataSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/RawResourceDataSource.java @@ -15,12 +15,16 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.net.Uri; import android.text.TextUtils; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; import java.io.FileInputStream; import java.io.IOException; @@ -28,12 +32,12 @@ import java.io.InputStream; /** * A {@link DataSource} for reading a raw resource inside the APK. - *

- * URIs supported by this source are of the form {@code rawresource:///rawResourceId}, where + * + *

URIs supported by this source are of the form {@code rawresource:///rawResourceId}, where * rawResourceId is the integer identifier of a raw resource. {@link #buildRawResourceUri(int)} can * be used to build {@link Uri}s in this format. */ -public final class RawResourceDataSource implements DataSource { +public final class RawResourceDataSource extends BaseDataSource { /** * Thrown when an {@link IOException} is encountered reading from a raw resource. @@ -58,14 +62,14 @@ public final class RawResourceDataSource implements DataSource { return Uri.parse(RAW_RESOURCE_SCHEME + ":///" + rawResourceId); } - private static final String RAW_RESOURCE_SCHEME = "rawresource"; + /** The scheme part of a raw resource URI. */ + public static final String RAW_RESOURCE_SCHEME = "rawresource"; private final Resources resources; - private final TransferListener listener; - private Uri uri; - private AssetFileDescriptor assetFileDescriptor; - private InputStream inputStream; + @Nullable private Uri uri; + @Nullable private AssetFileDescriptor assetFileDescriptor; + @Nullable private InputStream inputStream; private long bytesRemaining; private boolean opened; @@ -73,36 +77,35 @@ public final class RawResourceDataSource implements DataSource { * @param context A context. */ public RawResourceDataSource(Context context) { - this(context, null); - } - - /** - * @param context A context. - * @param listener An optional listener. - */ - public RawResourceDataSource(Context context, - TransferListener listener) { + super(/* isNetwork= */ false); this.resources = context.getResources(); - this.listener = listener; } @Override public long open(DataSpec dataSpec) throws RawResourceDataSourceException { try { - uri = dataSpec.uri; + Uri uri = dataSpec.uri; + this.uri = uri; if (!TextUtils.equals(RAW_RESOURCE_SCHEME, uri.getScheme())) { throw new RawResourceDataSourceException("URI must use scheme " + RAW_RESOURCE_SCHEME); } int resourceId; try { - resourceId = Integer.parseInt(uri.getLastPathSegment()); + resourceId = Integer.parseInt(Assertions.checkNotNull(uri.getLastPathSegment())); } catch (NumberFormatException e) { throw new RawResourceDataSourceException("Resource identifier must be an integer."); } - assetFileDescriptor = resources.openRawResourceFd(resourceId); - inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + transferInitializing(dataSpec); + AssetFileDescriptor assetFileDescriptor = resources.openRawResourceFd(resourceId); + this.assetFileDescriptor = assetFileDescriptor; + if (assetFileDescriptor == null) { + throw new RawResourceDataSourceException("Resource is compressed: " + uri); + } + FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + this.inputStream = inputStream; + inputStream.skip(assetFileDescriptor.getStartOffset()); long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) { @@ -123,9 +126,7 @@ public final class RawResourceDataSource implements DataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return bytesRemaining; } @@ -142,7 +143,7 @@ public final class RawResourceDataSource implements DataSource { try { int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength : (int) Math.min(bytesRemaining, readLength); - bytesRead = inputStream.read(buffer, offset, bytesToRead); + bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead); } catch (IOException e) { throw new RawResourceDataSourceException(e); } @@ -157,17 +158,17 @@ public final class RawResourceDataSource implements DataSource { if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= bytesRead; } - if (listener != null) { - listener.onBytesTransferred(this, bytesRead); - } + bytesTransferred(bytesRead); return bytesRead; } @Override + @Nullable public Uri getUri() { return uri; } + @SuppressWarnings("Finally") @Override public void close() throws RawResourceDataSourceException { uri = null; @@ -189,9 +190,7 @@ public final class RawResourceDataSource implements DataSource { assetFileDescriptor = null; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ResolvingDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ResolvingDataSource.java new file mode 100644 index 000000000000..80046e175713 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ResolvingDataSource.java @@ -0,0 +1,132 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; + +import android.net.Uri; +import androidx.annotation.Nullable; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** {@link DataSource} wrapper allowing just-in-time resolution of {@link DataSpec DataSpecs}. */ +public final class ResolvingDataSource implements DataSource { + + /** Resolves {@link DataSpec DataSpecs}. */ + public interface Resolver { + + /** + * Resolves a {@link DataSpec} before forwarding it to the wrapped {@link DataSource}. This + * method is allowed to block until the {@link DataSpec} has been resolved. + * + *

Note that this method is called for every new connection, so caching of results is + * recommended, especially if network operations are involved. + * + * @param dataSpec The original {@link DataSpec}. + * @return The resolved {@link DataSpec}. + * @throws IOException If an {@link IOException} occurred while resolving the {@link DataSpec}. + */ + DataSpec resolveDataSpec(DataSpec dataSpec) throws IOException; + + /** + * Resolves a URI reported by {@link DataSource#getUri()} for event reporting and caching + * purposes. + * + *

Implementations do not need to overwrite this method unless they want to change the + * reported URI. + * + *

This method is not allowed to block. + * + * @param uri The URI as reported by {@link DataSource#getUri()}. + * @return The resolved URI used for event reporting and caching. + */ + default Uri resolveReportedUri(Uri uri) { + return uri; + } + } + + /** {@link DataSource.Factory} for {@link ResolvingDataSource} instances. */ + public static final class Factory implements DataSource.Factory { + + private final DataSource.Factory upstreamFactory; + private final Resolver resolver; + + /** + * @param upstreamFactory The wrapped {@link DataSource.Factory} for handling resolved {@link + * DataSpec DataSpecs}. + * @param resolver The {@link Resolver} to resolve the {@link DataSpec DataSpecs}. + */ + public Factory(DataSource.Factory upstreamFactory, Resolver resolver) { + this.upstreamFactory = upstreamFactory; + this.resolver = resolver; + } + + @Override + public ResolvingDataSource createDataSource() { + return new ResolvingDataSource(upstreamFactory.createDataSource(), resolver); + } + } + + private final DataSource upstreamDataSource; + private final Resolver resolver; + + private boolean upstreamOpened; + + /** + * @param upstreamDataSource The wrapped {@link DataSource}. + * @param resolver The {@link Resolver} to resolve the {@link DataSpec DataSpecs}. + */ + public ResolvingDataSource(DataSource upstreamDataSource, Resolver resolver) { + this.upstreamDataSource = upstreamDataSource; + this.resolver = resolver; + } + + @Override + public void addTransferListener(TransferListener transferListener) { + upstreamDataSource.addTransferListener(transferListener); + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + DataSpec resolvedDataSpec = resolver.resolveDataSpec(dataSpec); + upstreamOpened = true; + return upstreamDataSource.open(resolvedDataSpec); + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + return upstreamDataSource.read(buffer, offset, readLength); + } + + @Nullable + @Override + public Uri getUri() { + Uri reportedUri = upstreamDataSource.getUri(); + return reportedUri == null ? null : resolver.resolveReportedUri(reportedUri); + } + + @Override + public Map> getResponseHeaders() { + return upstreamDataSource.getResponseHeaders(); + } + + @Override + public void close() throws IOException { + if (upstreamOpened) { + upstreamOpened = false; + upstreamDataSource.close(); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/StatsDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/StatsDataSource.java new file mode 100644 index 000000000000..e2a179cc9d97 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/StatsDataSource.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; + +import android.net.Uri; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * {@link DataSource} wrapper which keeps track of bytes transferred, redirected uris, and response + * headers. + */ +public final class StatsDataSource implements DataSource { + + private final DataSource dataSource; + + private long bytesRead; + private Uri lastOpenedUri; + private Map> lastResponseHeaders; + + /** + * Creates the stats data source. + * + * @param dataSource The wrapped {@link DataSource}. + */ + public StatsDataSource(DataSource dataSource) { + this.dataSource = Assertions.checkNotNull(dataSource); + lastOpenedUri = Uri.EMPTY; + lastResponseHeaders = Collections.emptyMap(); + } + + /** Resets the number of bytes read as returned from {@link #getBytesRead()} to zero. */ + public void resetBytesRead() { + bytesRead = 0; + } + + /** Returns the total number of bytes that have been read from the data source. */ + public long getBytesRead() { + return bytesRead; + } + + /** + * Returns the {@link Uri} associated with the last {@link #open(DataSpec)} call. If redirection + * occurred, this is the redirected uri. + */ + public Uri getLastOpenedUri() { + return lastOpenedUri; + } + + /** Returns the response headers associated with the last {@link #open(DataSpec)} call. */ + public Map> getLastResponseHeaders() { + return lastResponseHeaders; + } + + @Override + public void addTransferListener(TransferListener transferListener) { + dataSource.addTransferListener(transferListener); + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + // Reassign defaults in case dataSource.open throws an exception. + lastOpenedUri = dataSpec.uri; + lastResponseHeaders = Collections.emptyMap(); + long availableBytes = dataSource.open(dataSpec); + lastOpenedUri = Assertions.checkNotNull(getUri()); + lastResponseHeaders = getResponseHeaders(); + return availableBytes; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + int bytesRead = dataSource.read(buffer, offset, readLength); + if (bytesRead != C.RESULT_END_OF_INPUT) { + this.bytesRead += bytesRead; + } + return bytesRead; + } + + @Override + @Nullable + public Uri getUri() { + return dataSource.getUri(); + } + + @Override + public Map> getResponseHeaders() { + return dataSource.getResponseHeaders(); + } + + @Override + public void close() throws IOException { + dataSource.close(); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TeeDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TeeDataSource.java index 35780451c206..c6063b916f5c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TeeDataSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TeeDataSource.java @@ -16,9 +16,12 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; import android.net.Uri; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.util.List; +import java.util.Map; /** * Tees data into a {@link DataSink} as the data is read. @@ -28,6 +31,9 @@ public final class TeeDataSource implements DataSource { private final DataSource upstream; private final DataSink dataSink; + private boolean dataSinkNeedsClosing; + private long bytesRemaining; + /** * @param upstream The upstream {@link DataSource}. * @param dataSink The {@link DataSink} into which data is written. @@ -37,39 +43,62 @@ public final class TeeDataSource implements DataSource { this.dataSink = Assertions.checkNotNull(dataSink); } + @Override + public void addTransferListener(TransferListener transferListener) { + upstream.addTransferListener(transferListener); + } + @Override public long open(DataSpec dataSpec) throws IOException { - long dataLength = upstream.open(dataSpec); - if (dataSpec.length == C.LENGTH_UNSET && dataLength != C.LENGTH_UNSET) { - // Reconstruct dataSpec in order to provide the resolved length to the sink. - dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataSpec.position, - dataLength, dataSpec.key, dataSpec.flags); + bytesRemaining = upstream.open(dataSpec); + if (bytesRemaining == 0) { + return 0; } + if (dataSpec.length == C.LENGTH_UNSET && bytesRemaining != C.LENGTH_UNSET) { + // Reconstruct dataSpec in order to provide the resolved length to the sink. + dataSpec = dataSpec.subrange(0, bytesRemaining); + } + dataSinkNeedsClosing = true; dataSink.open(dataSpec); - return dataLength; + return bytesRemaining; } @Override public int read(byte[] buffer, int offset, int max) throws IOException { - int num = upstream.read(buffer, offset, max); - if (num > 0) { - // TODO: Consider continuing even if disk writes fail. - dataSink.write(buffer, offset, num); + if (bytesRemaining == 0) { + return C.RESULT_END_OF_INPUT; } - return num; + int bytesRead = upstream.read(buffer, offset, max); + if (bytesRead > 0) { + // TODO: Consider continuing even if writes to the sink fail. + dataSink.write(buffer, offset, bytesRead); + if (bytesRemaining != C.LENGTH_UNSET) { + bytesRemaining -= bytesRead; + } + } + return bytesRead; } @Override + @Nullable public Uri getUri() { return upstream.getUri(); } + @Override + public Map> getResponseHeaders() { + return upstream.getResponseHeaders(); + } + @Override public void close() throws IOException { try { upstream.close(); } finally { - dataSink.close(); + if (dataSinkNeedsClosing) { + dataSinkNeedsClosing = false; + dataSink.close(); + } } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TransferListener.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TransferListener.java index 8719cabe9191..f6574120ff3c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TransferListener.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TransferListener.java @@ -17,31 +17,61 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; /** * A listener of data transfer events. + * + *

A transfer usually progresses through multiple steps: + * + *

    + *
  1. Initializing the underlying resource (e.g. opening a HTTP connection). {@link + * #onTransferInitializing(DataSource, DataSpec, boolean)} is called before the initialization + * starts. + *
  2. Starting the transfer after successfully initializing the resource. {@link + * #onTransferStart(DataSource, DataSpec, boolean)} is called. Note that this only happens if + * the initialization was successful. + *
  3. Transferring data. {@link #onBytesTransferred(DataSource, DataSpec, boolean, int)} is + * called frequently during the transfer to indicate progress. + *
  4. Closing the transfer and the underlying resource. {@link #onTransferEnd(DataSource, + * DataSpec, boolean)} is called. Note that each {@link #onTransferStart(DataSource, DataSpec, + * boolean)} will have exactly one corresponding call to {@link #onTransferEnd(DataSource, + * DataSpec, boolean)}. + *
*/ -public interface TransferListener { +public interface TransferListener { + + /** + * Called when a transfer is being initialized. + * + * @param source The source performing the transfer. + * @param dataSpec Describes the data for which the transfer is initialized. + * @param isNetwork Whether the data is transferred through a network. + */ + void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork); /** * Called when a transfer starts. * * @param source The source performing the transfer. * @param dataSpec Describes the data being transferred. + * @param isNetwork Whether the data is transferred through a network. */ - void onTransferStart(S source, DataSpec dataSpec); + void onTransferStart(DataSource source, DataSpec dataSpec, boolean isNetwork); /** * Called incrementally during a transfer. * * @param source The source performing the transfer. - * @param bytesTransferred The number of bytes transferred since the previous call to this - * method (or if the first call, since the transfer was started). + * @param dataSpec Describes the data being transferred. + * @param isNetwork Whether the data is transferred through a network. + * @param bytesTransferred The number of bytes transferred since the previous call to this method */ - void onBytesTransferred(S source, int bytesTransferred); + void onBytesTransferred( + DataSource source, DataSpec dataSpec, boolean isNetwork, int bytesTransferred); /** * Called when a transfer ends. * * @param source The source performing the transfer. + * @param dataSpec Describes the data being transferred. + * @param isNetwork Whether the data is transferred through a network. */ - void onTransferEnd(S source); - + void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/UdpDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/UdpDataSource.java index 2e326c75eef6..8e9b44563c73 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/UdpDataSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/UdpDataSource.java @@ -16,6 +16,7 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream; import android.net.Uri; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import java.io.IOException; import java.net.DatagramPacket; @@ -25,10 +26,8 @@ import java.net.InetSocketAddress; import java.net.MulticastSocket; import java.net.SocketException; -/** - * A UDP {@link DataSource}. - */ -public final class UdpDataSource implements DataSource { +/** A UDP {@link DataSource}. */ +public final class UdpDataSource extends BaseDataSource { /** * Thrown when an error is encountered when trying to read from a {@link UdpDataSource}. @@ -46,49 +45,44 @@ public final class UdpDataSource implements DataSource { */ public static final int DEFAULT_MAX_PACKET_SIZE = 2000; - /** - * The default socket timeout, in milliseconds. - */ - public static final int DEAFULT_SOCKET_TIMEOUT_MILLIS = 8 * 1000; + /** The default socket timeout, in milliseconds. */ + public static final int DEFAULT_SOCKET_TIMEOUT_MILLIS = 8 * 1000; - private final TransferListener listener; private final int socketTimeoutMillis; private final byte[] packetBuffer; private final DatagramPacket packet; - private Uri uri; - private DatagramSocket socket; - private MulticastSocket multicastSocket; - private InetAddress address; - private InetSocketAddress socketAddress; + @Nullable private Uri uri; + @Nullable private DatagramSocket socket; + @Nullable private MulticastSocket multicastSocket; + @Nullable private InetAddress address; + @Nullable private InetSocketAddress socketAddress; private boolean opened; private int packetRemaining; - /** - * @param listener An optional listener. - */ - public UdpDataSource(TransferListener listener) { - this(listener, DEFAULT_MAX_PACKET_SIZE); + public UdpDataSource() { + this(DEFAULT_MAX_PACKET_SIZE); } /** - * @param listener An optional listener. + * Constructs a new instance. + * * @param maxPacketSize The maximum datagram packet size, in bytes. */ - public UdpDataSource(TransferListener listener, int maxPacketSize) { - this(listener, maxPacketSize, DEAFULT_SOCKET_TIMEOUT_MILLIS); + public UdpDataSource(int maxPacketSize) { + this(maxPacketSize, DEFAULT_SOCKET_TIMEOUT_MILLIS); } /** - * @param listener An optional listener. + * Constructs a new instance. + * * @param maxPacketSize The maximum datagram packet size, in bytes. * @param socketTimeoutMillis The socket timeout in milliseconds. A timeout of zero is interpreted * as an infinite timeout. */ - public UdpDataSource(TransferListener listener, int maxPacketSize, - int socketTimeoutMillis) { - this.listener = listener; + public UdpDataSource(int maxPacketSize, int socketTimeoutMillis) { + super(/* isNetwork= */ true); this.socketTimeoutMillis = socketTimeoutMillis; packetBuffer = new byte[maxPacketSize]; packet = new DatagramPacket(packetBuffer, 0, maxPacketSize); @@ -99,7 +93,7 @@ public final class UdpDataSource implements DataSource { uri = dataSpec.uri; String host = uri.getHost(); int port = uri.getPort(); - + transferInitializing(dataSpec); try { address = InetAddress.getByName(host); socketAddress = new InetSocketAddress(address, port); @@ -121,9 +115,7 @@ public final class UdpDataSource implements DataSource { } opened = true; - if (listener != null) { - listener.onTransferStart(this, dataSpec); - } + transferStarted(dataSpec); return C.LENGTH_UNSET; } @@ -141,9 +133,7 @@ public final class UdpDataSource implements DataSource { throw new UdpDataSourceException(e); } packetRemaining = packet.getLength(); - if (listener != null) { - listener.onBytesTransferred(this, packetRemaining); - } + bytesTransferred(packetRemaining); } int packetOffset = packet.getLength() - packetRemaining; @@ -154,6 +144,7 @@ public final class UdpDataSource implements DataSource { } @Override + @Nullable public Uri getUri() { return uri; } @@ -178,9 +169,7 @@ public final class UdpDataSource implements DataSource { packetRemaining = 0; if (opened) { opened = false; - if (listener != null) { - listener.onTransferEnd(this); - } + transferEnded(); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/Cache.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/Cache.java index ae6bbd67d335..cb90d95bb4b1 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/Cache.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/Cache.java @@ -15,6 +15,9 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import java.io.File; import java.io.IOException; import java.util.NavigableSet; @@ -47,21 +50,20 @@ public interface Cache { void onSpanRemoved(Cache cache, CacheSpan span); /** - * Called when an existing {@link CacheSpan} is accessed, causing it to be replaced. The new + * Called when an existing {@link CacheSpan} is touched, causing it to be replaced. The new * {@link CacheSpan} is guaranteed to represent the same data as the one it replaces, however - * {@link CacheSpan#file} and {@link CacheSpan#lastAccessTimestamp} may have changed. - *

- * Note that for span replacement, {@link #onSpanAdded(Cache, CacheSpan)} and - * {@link #onSpanRemoved(Cache, CacheSpan)} are not called in addition to this method. + * {@link CacheSpan#file} and {@link CacheSpan#lastTouchTimestamp} may have changed. + * + *

Note that for span replacement, {@link #onSpanAdded(Cache, CacheSpan)} and {@link + * #onSpanRemoved(Cache, CacheSpan)} are not called in addition to this method. * * @param cache The source of the event. * @param oldSpan The old {@link CacheSpan}, which has been removed from the cache. * @param newSpan The new {@link CacheSpan}, which has been added to the cache. */ void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan); - } - + /** * Thrown when an error is encountered when writing data. */ @@ -71,18 +73,46 @@ public interface Cache { super(message); } - public CacheException(IOException cause) { + public CacheException(Throwable cause) { super(cause); } + public CacheException(String message, Throwable cause) { + super(message, cause); + } } + /** + * Returned by {@link #getUid()} if initialization failed before the unique identifier was read or + * generated. + */ + long UID_UNSET = -1; + + /** + * Returns a non-negative unique identifier for the cache, or {@link #UID_UNSET} if initialization + * failed before the unique identifier was determined. + * + *

Implementations are expected to generate and store the unique identifier alongside the + * cached content. If the location of the cache is deleted or swapped, it is expected that a new + * unique identifier will be generated when the cache is recreated. + */ + long getUid(); + + /** + * Releases the cache. This method must be called when the cache is no longer required. The cache + * must not be used after calling this method. + * + *

This method may be slow and shouldn't normally be called on the main thread. + */ + @WorkerThread + void release(); + /** * Registers a listener to listen for changes to a given key. - *

- * No guarantees are made about the thread or threads on which the listener is called, but it is - * guaranteed that listener methods will be called in a serial fashion (i.e. one at a time) and in - * the same order as events occurred. + * + *

No guarantees are made about the thread or threads on which the listener is called, but it + * is guaranteed that listener methods will be called in a serial fashion (i.e. one at a time) and + * in the same order as events occurred. * * @param key The key to listen to. * @param listener The listener to add. @@ -102,7 +132,7 @@ public interface Cache { * Returns the cached spans for a given cache key. * * @param key The key for which spans should be returned. - * @return The spans for the key. May be null if there are no such spans. + * @return The spans for the key. */ NavigableSet getCachedSpans(String key); @@ -123,55 +153,73 @@ public interface Cache { /** * A caller should invoke this method when they require data from a given position for a given * key. - *

- * If there is a cache entry that overlaps the position, then the returned {@link CacheSpan} + * + *

If there is a cache entry that overlaps the position, then the returned {@link CacheSpan} * defines the file in which the data is stored. {@link CacheSpan#isCached} is true. The caller * may read from the cache file, but does not acquire any locks. - *

- * If there is no cache entry overlapping {@code offset}, then the returned {@link CacheSpan} + * + *

If there is no cache entry overlapping {@code offset}, then the returned {@link CacheSpan} * defines a hole in the cache starting at {@code position} into which the caller may write as it * obtains the data from some other source. The returned {@link CacheSpan} serves as a lock. * Whilst the caller holds the lock it may write data into the hole. It may split data into - * multiple files. When the caller has finished writing a file it should commit it to the cache - * by calling {@link #commitFile(File)}. When the caller has finished writing, it must release + * multiple files. When the caller has finished writing a file it should commit it to the cache by + * calling {@link #commitFile(File, long)}. When the caller has finished writing, it must release * the lock by calling {@link #releaseHoleSpan}. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param key The key of the data being requested. * @param position The position of the data being requested. * @return The {@link CacheSpan}. - * @throws InterruptedException + * @throws InterruptedException If the thread was interrupted. + * @throws CacheException If an error is encountered. */ + @WorkerThread CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException; /** * Same as {@link #startReadWrite(String, long)}. However, if the cache entry is locked, then * instead of blocking, this method will return null as the {@link CacheSpan}. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param key The key of the data being requested. * @param position The position of the data being requested. * @return The {@link CacheSpan}. Or null if the cache entry is locked. + * @throws CacheException If an error is encountered. */ + @WorkerThread + @Nullable CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException; /** * Obtains a cache file into which data can be written. Must only be called when holding a * corresponding hole {@link CacheSpan} obtained from {@link #startReadWrite(String, long)}. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param key The cache key for the data. * @param position The starting position of the data. - * @param maxLength The maximum length of the data to be written. Used only to ensure that there - * is enough space in the cache. + * @param length The length of the data being written, or {@link C#LENGTH_UNSET} if unknown. Used + * only to ensure that there is enough space in the cache. * @return The file into which data should be written. + * @throws CacheException If an error is encountered. */ - File startFile(String key, long position, long maxLength) throws CacheException; + @WorkerThread + File startFile(String key, long position, long length) throws CacheException; /** - * Commits a file into the cache. Must only be called when holding a corresponding hole - * {@link CacheSpan} obtained from {@link #startReadWrite(String, long)} + * Commits a file into the cache. Must only be called when holding a corresponding hole {@link + * CacheSpan} obtained from {@link #startReadWrite(String, long)}. + * + *

This method may be slow and shouldn't normally be called on the main thread. * * @param file A newly written cache file. + * @param length The length of the newly written cache file in bytes. + * @throws CacheException If an error is encountered. */ - void commitFile(File file) throws CacheException; + @WorkerThread + void commitFile(File file, long length) throws CacheException; /** * Releases a {@link CacheSpan} obtained from {@link #startReadWrite(String, long)} which @@ -184,18 +232,22 @@ public interface Cache { /** * Removes a cached {@link CacheSpan} from the cache, deleting the underlying file. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param span The {@link CacheSpan} to remove. + * @throws CacheException If an error is encountered. */ + @WorkerThread void removeSpan(CacheSpan span) throws CacheException; - /** - * Queries if a range is entirely available in the cache. - * - * @param key The cache key for the data. - * @param position The starting position of the data. - * @param length The length of the data. - * @return true if the data is available in the Cache otherwise false; - */ + /** + * Queries if a range is entirely available in the cache. + * + * @param key The cache key for the data. + * @param position The starting position of the data. + * @param length The length of the data. + * @return true if the data is available in the Cache otherwise false; + */ boolean isCached(String key, long position, long length); /** @@ -206,24 +258,29 @@ public interface Cache { * @param key The cache key for the data. * @param position The starting position of the data. * @param length The maximum length of the data to be returned. - * @return the length of the cached or not cached data block length. + * @return The length of the cached or not cached data block length. */ - long getCachedBytes(String key, long position, long length); + long getCachedLength(String key, long position, long length); /** - * Sets the content length for the given key. + * Applies {@code mutations} to the {@link ContentMetadata} for the given key. A new {@link + * CachedContent} is added if there isn't one already with the given key. + * + *

This method may be slow and shouldn't normally be called on the main thread. * * @param key The cache key for the data. - * @param length The length of the data. + * @param mutations Contains mutations to be applied to the metadata. + * @throws CacheException If an error is encountered. */ - void setContentLength(String key, long length) throws CacheException; + @WorkerThread + void applyContentMetadataMutations(String key, ContentMetadataMutations mutations) + throws CacheException; /** - * Returns the content length for the given key if one set, or {@link - * org.mozilla.thirdparty.com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise. + * Returns a {@link ContentMetadata} for the given key. * * @param key The cache key for the data. + * @return A {@link ContentMetadata} for the given key. */ - long getContentLength(String key); - + ContentMetadata getContentMetadata(String key); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 1f6e28f1c73f..e372a028511c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -20,6 +20,7 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSink; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ReusableBufferedOutputStream; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.File; @@ -29,20 +30,29 @@ import java.io.OutputStream; /** * Writes data into a cache. + * + *

If the {@link DataSpec} passed to {@link #open(DataSpec)} has the {@code length} field set to + * {@link C#LENGTH_UNSET} and {@link DataSpec#FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN} set, then {@link + * #write(byte[], int, int)} calls are ignored. */ public final class CacheDataSink implements DataSink { - /** Default buffer size. */ - public static final int DEFAULT_BUFFER_SIZE = 20480; + /** Default {@code fragmentSize} recommended for caching use cases. */ + public static final long DEFAULT_FRAGMENT_SIZE = 5 * 1024 * 1024; + /** Default buffer size in bytes. */ + public static final int DEFAULT_BUFFER_SIZE = 20 * 1024; + + private static final long MIN_RECOMMENDED_FRAGMENT_SIZE = 2 * 1024 * 1024; + private static final String TAG = "CacheDataSink"; private final Cache cache; - private final long maxCacheFileSize; + private final long fragmentSize; private final int bufferSize; private DataSpec dataSpec; + private long dataSpecFragmentSize; private File file; private OutputStream outputStream; - private FileOutputStream underlyingFileOutputStream; private long outputStreamBytesWritten; private long dataSpecBytesWritten; private ReusableBufferedOutputStream bufferedOutputStream; @@ -59,39 +69,55 @@ public final class CacheDataSink implements DataSink { } /** - * Constructs a CacheDataSink using the {@link #DEFAULT_BUFFER_SIZE}. + * Constructs an instance using {@link #DEFAULT_BUFFER_SIZE}. * * @param cache The cache into which data should be written. - * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for - * a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into - * multiple cache files. + * @param fragmentSize For requests that should be fragmented into multiple cache files, this is + * the maximum size of a cache file in bytes. If set to {@link C#LENGTH_UNSET} then no + * fragmentation will occur. Using a small value allows for finer-grained cache eviction + * policies, at the cost of increased overhead both on the cache implementation and the file + * system. Values under {@code (2 * 1024 * 1024)} are not recommended. */ - public CacheDataSink(Cache cache, long maxCacheFileSize) { - this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE); + public CacheDataSink(Cache cache, long fragmentSize) { + this(cache, fragmentSize, DEFAULT_BUFFER_SIZE); } /** * @param cache The cache into which data should be written. - * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for - * a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into - * multiple cache files. + * @param fragmentSize For requests that should be fragmented into multiple cache files, this is + * the maximum size of a cache file in bytes. If set to {@link C#LENGTH_UNSET} then no + * fragmentation will occur. Using a small value allows for finer-grained cache eviction + * policies, at the cost of increased overhead both on the cache implementation and the file + * system. Values under {@code (2 * 1024 * 1024)} are not recommended. * @param bufferSize The buffer size in bytes for writing to a cache file. A zero or negative - * value disables buffering. + * value disables buffering. */ - public CacheDataSink(Cache cache, long maxCacheFileSize, int bufferSize) { + public CacheDataSink(Cache cache, long fragmentSize, int bufferSize) { + Assertions.checkState( + fragmentSize > 0 || fragmentSize == C.LENGTH_UNSET, + "fragmentSize must be positive or C.LENGTH_UNSET."); + if (fragmentSize != C.LENGTH_UNSET && fragmentSize < MIN_RECOMMENDED_FRAGMENT_SIZE) { + Log.w( + TAG, + "fragmentSize is below the minimum recommended value of " + + MIN_RECOMMENDED_FRAGMENT_SIZE + + ". This may cause poor cache performance."); + } this.cache = Assertions.checkNotNull(cache); - this.maxCacheFileSize = maxCacheFileSize; + this.fragmentSize = fragmentSize == C.LENGTH_UNSET ? Long.MAX_VALUE : fragmentSize; this.bufferSize = bufferSize; } @Override public void open(DataSpec dataSpec) throws CacheDataSinkException { if (dataSpec.length == C.LENGTH_UNSET - && !dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)) { + && dataSpec.isFlagSet(DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN)) { this.dataSpec = null; return; } this.dataSpec = dataSpec; + this.dataSpecFragmentSize = + dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION) ? fragmentSize : Long.MAX_VALUE; dataSpecBytesWritten = 0; try { openNextOutputStream(); @@ -108,12 +134,12 @@ public final class CacheDataSink implements DataSink { try { int bytesWritten = 0; while (bytesWritten < length) { - if (outputStreamBytesWritten == maxCacheFileSize) { + if (outputStreamBytesWritten == dataSpecFragmentSize) { closeCurrentOutputStream(); openNextOutputStream(); } - int bytesToWrite = (int) Math.min(length - bytesWritten, - maxCacheFileSize - outputStreamBytesWritten); + int bytesToWrite = + (int) Math.min(length - bytesWritten, dataSpecFragmentSize - outputStreamBytesWritten); outputStream.write(buffer, offset + bytesWritten, bytesToWrite); bytesWritten += bytesToWrite; outputStreamBytesWritten += bytesToWrite; @@ -137,11 +163,14 @@ public final class CacheDataSink implements DataSink { } private void openNextOutputStream() throws IOException { - long maxLength = dataSpec.length == C.LENGTH_UNSET ? maxCacheFileSize - : Math.min(dataSpec.length - dataSpecBytesWritten, maxCacheFileSize); - file = cache.startFile(dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten, - maxLength); - underlyingFileOutputStream = new FileOutputStream(file); + long length = + dataSpec.length == C.LENGTH_UNSET + ? C.LENGTH_UNSET + : Math.min(dataSpec.length - dataSpecBytesWritten, dataSpecFragmentSize); + file = + cache.startFile( + dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten, length); + FileOutputStream underlyingFileOutputStream = new FileOutputStream(file); if (bufferSize > 0) { if (bufferedOutputStream == null) { bufferedOutputStream = new ReusableBufferedOutputStream(underlyingFileOutputStream, @@ -156,7 +185,6 @@ public final class CacheDataSink implements DataSink { outputStreamBytesWritten = 0; } - @SuppressWarnings("ThrowFromFinallyBlock") private void closeCurrentOutputStream() throws IOException { if (outputStream == null) { return; @@ -165,7 +193,6 @@ public final class CacheDataSink implements DataSink { boolean success = false; try { outputStream.flush(); - underlyingFileOutputStream.getFD().sync(); success = true; } finally { Util.closeQuietly(outputStream); @@ -173,7 +200,7 @@ public final class CacheDataSink implements DataSink { File fileToCommit = file; file = null; if (success) { - cache.commitFile(fileToCommit); + cache.commitFile(fileToCommit, outputStreamBytesWritten); } else { fileToCommit.delete(); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java index 17688f161abd..51ba6f429447 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java @@ -23,28 +23,23 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSink; public final class CacheDataSinkFactory implements DataSink.Factory { private final Cache cache; - private final long maxCacheFileSize; + private final long fragmentSize; private final int bufferSize; - /** - * @see CacheDataSink#CacheDataSink(Cache, long) - */ - public CacheDataSinkFactory(Cache cache, long maxCacheFileSize) { - this(cache, maxCacheFileSize, CacheDataSink.DEFAULT_BUFFER_SIZE); + /** @see CacheDataSink#CacheDataSink(Cache, long) */ + public CacheDataSinkFactory(Cache cache, long fragmentSize) { + this(cache, fragmentSize, CacheDataSink.DEFAULT_BUFFER_SIZE); } - /** - * @see CacheDataSink#CacheDataSink(Cache, long, int) - */ - public CacheDataSinkFactory(Cache cache, long maxCacheFileSize, int bufferSize) { + /** @see CacheDataSink#CacheDataSink(Cache, long, int) */ + public CacheDataSinkFactory(Cache cache, long fragmentSize, int bufferSize) { this.cache = cache; - this.maxCacheFileSize = maxCacheFileSize; + this.fragmentSize = fragmentSize; this.bufferSize = bufferSize; } @Override public DataSink createDataSink() { - return new CacheDataSink(cache, maxCacheFileSize, bufferSize); + return new CacheDataSink(cache, fragmentSize, bufferSize); } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 3a9f2443d2c9..19fb8191e475 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -16,20 +16,27 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; import android.net.Uri; -import android.support.annotation.IntDef; -import android.support.annotation.Nullable; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSink; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSourceException; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec.HttpMethod; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.FileDataSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TeeDataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.Cache.CacheException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.io.InterruptedIOException; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.List; +import java.util.Map; /** * A {@link DataSource} that reads and writes a {@link Cache}. Requests are fulfilled from the cache @@ -39,37 +46,56 @@ import java.lang.annotation.RetentionPolicy; public final class CacheDataSource implements DataSource { /** - * Default maximum single cache file size. - * - * @see #CacheDataSource(Cache, DataSource, int) - * @see #CacheDataSource(Cache, DataSource, int, long) - */ - public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024; - - /** - * Flags controlling the cache's behavior. + * Flags controlling the CacheDataSource's behavior. Possible flag values are {@link + * #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} and {@link + * #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}. */ + @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {FLAG_BLOCK_ON_CACHE, FLAG_IGNORE_CACHE_ON_ERROR, - FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}) + @IntDef( + flag = true, + value = { + FLAG_BLOCK_ON_CACHE, + FLAG_IGNORE_CACHE_ON_ERROR, + FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS + }) public @interface Flags {} /** - * A flag indicating whether we will block reads if the cache key is locked. If this flag is - * set, then we will read from upstream if the cache key is locked. + * A flag indicating whether we will block reads if the cache key is locked. If unset then data is + * read from upstream if the cache key is locked, regardless of whether the data is cached. */ - public static final int FLAG_BLOCK_ON_CACHE = 1 << 0; + public static final int FLAG_BLOCK_ON_CACHE = 1; /** * A flag indicating whether the cache is bypassed following any cache related error. If set * then cache related exceptions may be thrown for one cycle of open, read and close calls. * Subsequent cycles of these calls will then bypass the cache. */ - public static final int FLAG_IGNORE_CACHE_ON_ERROR = 1 << 1; + public static final int FLAG_IGNORE_CACHE_ON_ERROR = 1 << 1; // 2 /** - * A flag indicating that the cache should be bypassed for requests whose lengths are unset. + * A flag indicating that the cache should be bypassed for requests whose lengths are unset. This + * flag is provided for legacy reasons only. */ - public static final int FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS = 1 << 2; + public static final int FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS = 1 << 2; // 4 + + /** + * Reasons the cache may be ignored. One of {@link #CACHE_IGNORED_REASON_ERROR} or {@link + * #CACHE_IGNORED_REASON_UNSET_LENGTH}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({CACHE_IGNORED_REASON_ERROR, CACHE_IGNORED_REASON_UNSET_LENGTH}) + public @interface CacheIgnoredReason {} + + /** Cache not ignored. */ + private static final int CACHE_NOT_IGNORED = -1; + + /** Cache ignored due to a cache related error. */ + public static final int CACHE_IGNORED_REASON_ERROR = 0; + + /** Cache ignored due to a request with an unset length. */ + public static final int CACHE_IGNORED_REASON_UNSET_LENGTH = 1; /** * Listener of {@link CacheDataSource} events. @@ -84,55 +110,73 @@ public final class CacheDataSource implements DataSource { */ void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead); + /** + * Called when the current request ignores cache. + * + * @param reason Reason cache is bypassed. + */ + void onCacheIgnored(@CacheIgnoredReason int reason); } + /** Minimum number of bytes to read before checking cache for availability. */ + private static final long MIN_READ_BEFORE_CHECKING_CACHE = 100 * 1024; + private final Cache cache; private final DataSource cacheReadDataSource; - private final DataSource cacheWriteDataSource; + @Nullable private final DataSource cacheWriteDataSource; private final DataSource upstreamDataSource; + private final CacheKeyFactory cacheKeyFactory; @Nullable private final EventListener eventListener; private final boolean blockOnCache; private final boolean ignoreCacheOnError; private final boolean ignoreCacheForUnsetLengthRequests; - private DataSource currentDataSource; - private boolean currentRequestUnbounded; - private Uri uri; - private int flags; - private String key; + @Nullable private DataSource currentDataSource; + private boolean currentDataSpecLengthUnset; + @Nullable private Uri uri; + @Nullable private Uri actualUri; + @HttpMethod private int httpMethod; + @Nullable private byte[] httpBody; + private Map httpRequestHeaders = Collections.emptyMap(); + @DataSpec.Flags private int flags; + @Nullable private String key; private long readPosition; private long bytesRemaining; - private CacheSpan lockedSpan; + @Nullable private CacheSpan currentHoleSpan; private boolean seenCacheError; private boolean currentRequestIgnoresCache; private long totalCachedBytesRead; + private long checkCachePosition; /** * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for - * reading and writing the cache and with {@link #DEFAULT_MAX_CACHE_FILE_SIZE}. + * reading and writing the cache. + * + * @param cache The cache. + * @param upstream A {@link DataSource} for reading data not in the cache. */ - public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags) { - this(cache, upstream, flags, DEFAULT_MAX_CACHE_FILE_SIZE); + public CacheDataSource(Cache cache, DataSource upstream) { + this(cache, upstream, /* flags= */ 0); } /** * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for - * reading and writing the cache. The sink is configured to fragment data such that no single - * cache file is greater than maxCacheFileSize bytes. + * reading and writing the cache. * * @param cache The cache. * @param upstream A {@link DataSource} for reading data not in the cache. - * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link - * #FLAG_IGNORE_CACHE_ON_ERROR} or 0. - * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the cached data size - * exceeds this value, then the data will be fragmented into multiple cache files. The - * finer-grained this is the finer-grained the eviction policy can be. + * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} + * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0. */ - public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags, - long maxCacheFileSize) { - this(cache, upstream, new FileDataSource(), new CacheDataSink(cache, maxCacheFileSize), - flags, null); + public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags) { + this( + cache, + upstream, + new FileDataSource(), + new CacheDataSink(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE), + flags, + /* eventListener= */ null); } /** @@ -145,14 +189,54 @@ public final class CacheDataSource implements DataSource { * @param cacheReadDataSource A {@link DataSource} for reading data from the cache. * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is * accessed read-only. - * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link - * #FLAG_IGNORE_CACHE_ON_ERROR} or 0. + * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} + * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0. * @param eventListener An optional {@link EventListener} to receive events. */ - public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource, - DataSink cacheWriteDataSink, @Flags int flags, @Nullable EventListener eventListener) { + public CacheDataSource( + Cache cache, + DataSource upstream, + DataSource cacheReadDataSource, + @Nullable DataSink cacheWriteDataSink, + @Flags int flags, + @Nullable EventListener eventListener) { + this( + cache, + upstream, + cacheReadDataSource, + cacheWriteDataSink, + flags, + eventListener, + /* cacheKeyFactory= */ null); + } + + /** + * Constructs an instance with arbitrary {@link DataSource} and {@link DataSink} instances for + * reading and writing the cache. One use of this constructor is to allow data to be transformed + * before it is written to disk. + * + * @param cache The cache. + * @param upstream A {@link DataSource} for reading data not in the cache. + * @param cacheReadDataSource A {@link DataSource} for reading data from the cache. + * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is + * accessed read-only. + * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} + * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0. + * @param eventListener An optional {@link EventListener} to receive events. + * @param cacheKeyFactory An optional factory for cache keys. + */ + public CacheDataSource( + Cache cache, + DataSource upstream, + DataSource cacheReadDataSource, + @Nullable DataSink cacheWriteDataSink, + @Flags int flags, + @Nullable EventListener eventListener, + @Nullable CacheKeyFactory cacheKeyFactory) { this.cache = cache; this.cacheReadDataSource = cacheReadDataSource; + this.cacheKeyFactory = + cacheKeyFactory != null ? cacheKeyFactory : CacheUtil.DEFAULT_CACHE_KEY_FACTORY; this.blockOnCache = (flags & FLAG_BLOCK_ON_CACHE) != 0; this.ignoreCacheOnError = (flags & FLAG_IGNORE_CACHE_ON_ERROR) != 0; this.ignoreCacheForUnsetLengthRequests = @@ -166,19 +250,34 @@ public final class CacheDataSource implements DataSource { this.eventListener = eventListener; } + @Override + public void addTransferListener(TransferListener transferListener) { + cacheReadDataSource.addTransferListener(transferListener); + upstreamDataSource.addTransferListener(transferListener); + } + @Override public long open(DataSpec dataSpec) throws IOException { try { + key = cacheKeyFactory.buildCacheKey(dataSpec); uri = dataSpec.uri; + actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri); + httpMethod = dataSpec.httpMethod; + httpBody = dataSpec.httpBody; + httpRequestHeaders = dataSpec.httpRequestHeaders; flags = dataSpec.flags; - key = CacheUtil.getKey(dataSpec); readPosition = dataSpec.position; - currentRequestIgnoresCache = (ignoreCacheOnError && seenCacheError) - || (dataSpec.length == C.LENGTH_UNSET && ignoreCacheForUnsetLengthRequests); + + int reason = shouldIgnoreCacheForRequest(dataSpec); + currentRequestIgnoresCache = reason != CACHE_NOT_IGNORED; + if (currentRequestIgnoresCache) { + notifyCacheIgnored(reason); + } + if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) { bytesRemaining = dataSpec.length; } else { - bytesRemaining = cache.getContentLength(key); + bytesRemaining = ContentMetadata.getContentLength(cache.getContentMetadata(key)); if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= dataSpec.position; if (bytesRemaining <= 0) { @@ -186,9 +285,9 @@ public final class CacheDataSource implements DataSource { } } } - openNextSource(true); + openNextSource(false); return bytesRemaining; - } catch (IOException e) { + } catch (Throwable e) { handleBeforeThrow(e); throw e; } @@ -203,48 +302,67 @@ public final class CacheDataSource implements DataSource { return C.RESULT_END_OF_INPUT; } try { + if (readPosition >= checkCachePosition) { + openNextSource(true); + } int bytesRead = currentDataSource.read(buffer, offset, readLength); - if (bytesRead >= 0) { - if (currentDataSource == cacheReadDataSource) { + if (bytesRead != C.RESULT_END_OF_INPUT) { + if (isReadingFromCache()) { totalCachedBytesRead += bytesRead; } readPosition += bytesRead; if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= bytesRead; } - } else { - if (currentRequestUnbounded) { - // We only do unbounded requests to upstream and only when we don't know the actual stream - // length. So we reached the end of stream. - setContentLength(readPosition); - bytesRemaining = 0; - } + } else if (currentDataSpecLengthUnset) { + setNoBytesRemainingAndMaybeStoreLength(); + } else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) { closeCurrentSource(); - if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) { - if (openNextSource(false)) { - return read(buffer, offset, readLength); - } - } + openNextSource(false); + return read(buffer, offset, readLength); } return bytesRead; } catch (IOException e) { + if (currentDataSpecLengthUnset && CacheUtil.isCausedByPositionOutOfRange(e)) { + setNoBytesRemainingAndMaybeStoreLength(); + return C.RESULT_END_OF_INPUT; + } + handleBeforeThrow(e); + throw e; + } catch (Throwable e) { handleBeforeThrow(e); throw e; } } @Override + @Nullable public Uri getUri() { - return currentDataSource == upstreamDataSource ? currentDataSource.getUri() : uri; + return actualUri; + } + + @Override + public Map> getResponseHeaders() { + // TODO: Implement. + return isReadingFromUpstream() + ? upstreamDataSource.getResponseHeaders() + : Collections.emptyMap(); } @Override public void close() throws IOException { uri = null; + actualUri = null; + httpMethod = DataSpec.HTTP_METHOD_GET; + httpBody = null; + httpRequestHeaders = Collections.emptyMap(); + flags = 0; + readPosition = 0; + key = null; notifyBytesRead(); try { closeCurrentSource(); - } catch (IOException e) { + } catch (Throwable e) { handleBeforeThrow(e); throw e; } @@ -254,125 +372,204 @@ public final class CacheDataSource implements DataSource { * Opens the next source. If the cache contains data spanning the current read position then * {@link #cacheReadDataSource} is opened to read from it. Else {@link #upstreamDataSource} is * opened to read from the upstream source and write into the cache. - * @param initial Whether it is the initial open call. + * + *

There must not be a currently open source when this method is called, except in the case + * that {@code checkCache} is true. If {@code checkCache} is true then there must be a currently + * open source, and it must be {@link #upstreamDataSource}. It will be closed and a new source + * opened if it's possible to switch to reading from or writing to the cache. If a switch isn't + * possible then the current source is left unchanged. + * + * @param checkCache If true tries to switch to reading from or writing to cache instead of + * reading from {@link #upstreamDataSource}, which is the currently open source. */ - private boolean openNextSource(boolean initial) throws IOException { - DataSpec dataSpec; - CacheSpan span; + private void openNextSource(boolean checkCache) throws IOException { + CacheSpan nextSpan; if (currentRequestIgnoresCache) { - span = null; + nextSpan = null; } else if (blockOnCache) { try { - span = cache.startReadWrite(key, readPosition); + nextSpan = cache.startReadWrite(key, readPosition); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); throw new InterruptedIOException(); } } else { - span = cache.startReadWriteNonBlocking(key, readPosition); + nextSpan = cache.startReadWriteNonBlocking(key, readPosition); } - if (span == null) { + DataSpec nextDataSpec; + DataSource nextDataSource; + if (nextSpan == null) { // The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read // from upstream. - currentDataSource = upstreamDataSource; - dataSpec = new DataSpec(uri, readPosition, bytesRemaining, key, flags); - } else if (span.isCached) { + nextDataSource = upstreamDataSource; + nextDataSpec = + new DataSpec( + uri, + httpMethod, + httpBody, + readPosition, + readPosition, + bytesRemaining, + key, + flags, + httpRequestHeaders); + } else if (nextSpan.isCached) { // Data is cached, read from cache. - Uri fileUri = Uri.fromFile(span.file); - long filePosition = readPosition - span.position; - long length = span.length - filePosition; + Uri fileUri = Uri.fromFile(nextSpan.file); + long filePosition = readPosition - nextSpan.position; + long length = nextSpan.length - filePosition; if (bytesRemaining != C.LENGTH_UNSET) { length = Math.min(length, bytesRemaining); } - dataSpec = new DataSpec(fileUri, readPosition, filePosition, length, key, flags); - currentDataSource = cacheReadDataSource; + // Deliberately skip the HTTP-related parameters since we're reading from the cache, not + // making an HTTP request. + nextDataSpec = new DataSpec(fileUri, readPosition, filePosition, length, key, flags); + nextDataSource = cacheReadDataSource; } else { // Data is not cached, and data is not locked, read from upstream with cache backing. long length; - if (span.isOpenEnded()) { + if (nextSpan.isOpenEnded()) { length = bytesRemaining; } else { - length = span.length; + length = nextSpan.length; if (bytesRemaining != C.LENGTH_UNSET) { length = Math.min(length, bytesRemaining); } } - dataSpec = new DataSpec(uri, readPosition, length, key, flags); + nextDataSpec = + new DataSpec( + uri, + httpMethod, + httpBody, + readPosition, + readPosition, + length, + key, + flags, + httpRequestHeaders); if (cacheWriteDataSource != null) { - currentDataSource = cacheWriteDataSource; - lockedSpan = span; + nextDataSource = cacheWriteDataSource; } else { - currentDataSource = upstreamDataSource; - cache.releaseHoleSpan(span); + nextDataSource = upstreamDataSource; + cache.releaseHoleSpan(nextSpan); + nextSpan = null; } } - currentRequestUnbounded = dataSpec.length == C.LENGTH_UNSET; - boolean successful = false; - long currentBytesRemaining = 0; - try { - currentBytesRemaining = currentDataSource.open(dataSpec); - successful = true; - } catch (IOException e) { - // if this isn't the initial open call (we had read some bytes) and an unbounded range request - // failed because of POSITION_OUT_OF_RANGE then mute the exception. We are trying to find the - // end of the stream. - if (!initial && currentRequestUnbounded) { - Throwable cause = e; - while (cause != null) { - if (cause instanceof DataSourceException) { - int reason = ((DataSourceException) cause).reason; - if (reason == DataSourceException.POSITION_OUT_OF_RANGE) { - e = null; - break; - } - } - cause = cause.getCause(); - } + checkCachePosition = + !currentRequestIgnoresCache && nextDataSource == upstreamDataSource + ? readPosition + MIN_READ_BEFORE_CHECKING_CACHE + : Long.MAX_VALUE; + if (checkCache) { + Assertions.checkState(isBypassingCache()); + if (nextDataSource == upstreamDataSource) { + // Continue reading from upstream. + return; } - if (e != null) { + // We're switching to reading from or writing to the cache. + try { + closeCurrentSource(); + } catch (Throwable e) { + if (nextSpan.isHoleSpan()) { + // Release the hole span before throwing, else we'll hold it forever. + cache.releaseHoleSpan(nextSpan); + } throw e; } } - // If we did an unbounded request (which means it's to upstream and - // bytesRemaining == C.LENGTH_UNSET) and got a resolved length from open() request - if (currentRequestUnbounded && currentBytesRemaining != C.LENGTH_UNSET) { - bytesRemaining = currentBytesRemaining; - setContentLength(dataSpec.position + bytesRemaining); + if (nextSpan != null && nextSpan.isHoleSpan()) { + currentHoleSpan = nextSpan; + } + currentDataSource = nextDataSource; + currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET; + long resolvedLength = nextDataSource.open(nextDataSpec); + + // Update bytesRemaining, actualUri and (if writing to cache) the cache metadata. + ContentMetadataMutations mutations = new ContentMetadataMutations(); + if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) { + bytesRemaining = resolvedLength; + ContentMetadataMutations.setContentLength(mutations, readPosition + bytesRemaining); + } + if (isReadingFromUpstream()) { + actualUri = currentDataSource.getUri(); + boolean isRedirected = !uri.equals(actualUri); + ContentMetadataMutations.setRedirectedUri(mutations, isRedirected ? actualUri : null); + } + if (isWritingToCache()) { + cache.applyContentMetadataMutations(key, mutations); } - return successful; } - private void setContentLength(long length) throws IOException { - // If writing into cache - if (currentDataSource == cacheWriteDataSource) { - cache.setContentLength(key, length); + private void setNoBytesRemainingAndMaybeStoreLength() throws IOException { + bytesRemaining = 0; + if (isWritingToCache()) { + ContentMetadataMutations mutations = new ContentMetadataMutations(); + ContentMetadataMutations.setContentLength(mutations, readPosition); + cache.applyContentMetadataMutations(key, mutations); } } + private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) { + Uri redirectedUri = ContentMetadata.getRedirectedUri(cache.getContentMetadata(key)); + return redirectedUri != null ? redirectedUri : defaultUri; + } + + private boolean isReadingFromUpstream() { + return !isReadingFromCache(); + } + + private boolean isBypassingCache() { + return currentDataSource == upstreamDataSource; + } + + private boolean isReadingFromCache() { + return currentDataSource == cacheReadDataSource; + } + + private boolean isWritingToCache() { + return currentDataSource == cacheWriteDataSource; + } + private void closeCurrentSource() throws IOException { if (currentDataSource == null) { return; } try { currentDataSource.close(); - currentDataSource = null; - currentRequestUnbounded = false; } finally { - if (lockedSpan != null) { - cache.releaseHoleSpan(lockedSpan); - lockedSpan = null; + currentDataSource = null; + currentDataSpecLengthUnset = false; + if (currentHoleSpan != null) { + cache.releaseHoleSpan(currentHoleSpan); + currentHoleSpan = null; } } } - private void handleBeforeThrow(IOException exception) { - if (currentDataSource == cacheReadDataSource || exception instanceof CacheException) { + private void handleBeforeThrow(Throwable exception) { + if (isReadingFromCache() || exception instanceof CacheException) { seenCacheError = true; } } + private int shouldIgnoreCacheForRequest(DataSpec dataSpec) { + if (ignoreCacheOnError && seenCacheError) { + return CACHE_IGNORED_REASON_ERROR; + } else if (ignoreCacheForUnsetLengthRequests && dataSpec.length == C.LENGTH_UNSET) { + return CACHE_IGNORED_REASON_UNSET_LENGTH; + } else { + return CACHE_NOT_IGNORED; + } + } + + private void notifyCacheIgnored(@CacheIgnoredReason int reason) { + if (eventListener != null) { + eventListener.onCacheIgnored(reason); + } + } + private void notifyBytesRead() { if (eventListener != null && totalCachedBytesRead > 0) { eventListener.onCachedBytesRead(cache.getCacheSpace(), totalCachedBytesRead); diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java index 7e24402d8ce0..21aef3f93abb 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java @@ -15,61 +15,98 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSink; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource.Factory; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.FileDataSourceFactory; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.CacheDataSource.EventListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.FileDataSource; -/** - * A {@link DataSource.Factory} that produces {@link CacheDataSource}. - */ +/** A {@link DataSource.Factory} that produces {@link CacheDataSource}. */ public final class CacheDataSourceFactory implements DataSource.Factory { private final Cache cache; private final DataSource.Factory upstreamFactory; private final DataSource.Factory cacheReadDataSourceFactory; - private final DataSink.Factory cacheWriteDataSinkFactory; - private final int flags; - private final EventListener eventListener; + @CacheDataSource.Flags private final int flags; + @Nullable private final DataSink.Factory cacheWriteDataSinkFactory; + @Nullable private final CacheDataSource.EventListener eventListener; + @Nullable private final CacheKeyFactory cacheKeyFactory; /** - * @see CacheDataSource#CacheDataSource(Cache, DataSource, int) + * Constructs a factory which creates {@link CacheDataSource} instances with default {@link + * DataSource} and {@link DataSink} instances for reading and writing the cache. + * + * @param cache The cache. + * @param upstreamFactory A {@link DataSource.Factory} for creating upstream {@link DataSource}s + * for reading data not in the cache. */ - public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, int flags) { - this(cache, upstreamFactory, flags, CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE); + public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory) { + this(cache, upstreamFactory, /* flags= */ 0); } - /** - * @see CacheDataSource#CacheDataSource(Cache, DataSource, int, long) - */ - public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, int flags, - long maxCacheFileSize) { - this(cache, upstreamFactory, new FileDataSourceFactory(), - new CacheDataSinkFactory(cache, maxCacheFileSize), flags, null); + /** @see CacheDataSource#CacheDataSource(Cache, DataSource, int) */ + public CacheDataSourceFactory( + Cache cache, DataSource.Factory upstreamFactory, @CacheDataSource.Flags int flags) { + this( + cache, + upstreamFactory, + new FileDataSource.Factory(), + new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE), + flags, + /* eventListener= */ null); } /** * @see CacheDataSource#CacheDataSource(Cache, DataSource, DataSource, DataSink, int, - * EventListener) + * CacheDataSource.EventListener) */ - public CacheDataSourceFactory(Cache cache, Factory upstreamFactory, - Factory cacheReadDataSourceFactory, - DataSink.Factory cacheWriteDataSinkFactory, int flags, EventListener eventListener) { + public CacheDataSourceFactory( + Cache cache, + DataSource.Factory upstreamFactory, + DataSource.Factory cacheReadDataSourceFactory, + @Nullable DataSink.Factory cacheWriteDataSinkFactory, + @CacheDataSource.Flags int flags, + @Nullable CacheDataSource.EventListener eventListener) { + this( + cache, + upstreamFactory, + cacheReadDataSourceFactory, + cacheWriteDataSinkFactory, + flags, + eventListener, + /* cacheKeyFactory= */ null); + } + + /** + * @see CacheDataSource#CacheDataSource(Cache, DataSource, DataSource, DataSink, int, + * CacheDataSource.EventListener, CacheKeyFactory) + */ + public CacheDataSourceFactory( + Cache cache, + DataSource.Factory upstreamFactory, + DataSource.Factory cacheReadDataSourceFactory, + @Nullable DataSink.Factory cacheWriteDataSinkFactory, + @CacheDataSource.Flags int flags, + @Nullable CacheDataSource.EventListener eventListener, + @Nullable CacheKeyFactory cacheKeyFactory) { this.cache = cache; this.upstreamFactory = upstreamFactory; this.cacheReadDataSourceFactory = cacheReadDataSourceFactory; this.cacheWriteDataSinkFactory = cacheWriteDataSinkFactory; this.flags = flags; this.eventListener = eventListener; + this.cacheKeyFactory = cacheKeyFactory; } @Override public CacheDataSource createDataSource() { - return new CacheDataSource(cache, upstreamFactory.createDataSource(), + return new CacheDataSource( + cache, + upstreamFactory.createDataSource(), cacheReadDataSourceFactory.createDataSource(), - cacheWriteDataSinkFactory != null ? cacheWriteDataSinkFactory.createDataSink() : null, - flags, eventListener); + cacheWriteDataSinkFactory == null ? null : cacheWriteDataSinkFactory.createDataSink(), + flags, + eventListener, + cacheKeyFactory); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java index a551d8b8db72..017e84c8c8bd 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java @@ -15,12 +15,21 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; + /** * Evicts data from a {@link Cache}. Implementations should call {@link Cache#removeSpan(CacheSpan)} * to evict cache entries based on their eviction policies. */ public interface CacheEvictor extends Cache.Listener { + /** + * Returns whether the evictor requires the {@link Cache} to touch {@link CacheSpan CacheSpans} + * when it accesses them. Implementations that do not use {@link CacheSpan#lastTouchTimestamp} + * should return {@code false}. + */ + boolean requiresCacheSpanTouches(); + /** * Called when cache has been initialized. */ @@ -32,8 +41,7 @@ public interface CacheEvictor extends Cache.Listener { * @param cache The source of the event. * @param key The key being written. * @param position The starting position of the data being written. - * @param maxLength The maximum length of the data being written. + * @param length The length of the data being written, or {@link C#LENGTH_UNSET} if unknown. */ - void onStartFile(Cache cache, String key, long position, long maxLength); - + void onStartFile(Cache cache, String key, long position, long length); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java new file mode 100644 index 000000000000..2618a3ef6ab5 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; + +/** Metadata associated with a cache file. */ +/* package */ final class CacheFileMetadata { + + public final long length; + public final long lastTouchTimestamp; + + public CacheFileMetadata(long length, long lastTouchTimestamp) { + this.length = length; + this.lastTouchTimestamp = lastTouchTimestamp; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java new file mode 100644 index 000000000000..cd69336ff4bc --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import androidx.annotation.WorkerThread; +import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseIOException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseProvider; +import org.mozilla.thirdparty.com.google.android.exoplayer2.database.VersionTable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** Maintains an index of cache file metadata. */ +/* package */ final class CacheFileMetadataIndex { + + private static final String TABLE_PREFIX = DatabaseProvider.TABLE_PREFIX + "CacheFileMetadata"; + private static final int TABLE_VERSION = 1; + + private static final String COLUMN_NAME = "name"; + private static final String COLUMN_LENGTH = "length"; + private static final String COLUMN_LAST_TOUCH_TIMESTAMP = "last_touch_timestamp"; + + private static final int COLUMN_INDEX_NAME = 0; + private static final int COLUMN_INDEX_LENGTH = 1; + private static final int COLUMN_INDEX_LAST_TOUCH_TIMESTAMP = 2; + + private static final String WHERE_NAME_EQUALS = COLUMN_NAME + " = ?"; + + private static final String[] COLUMNS = + new String[] { + COLUMN_NAME, COLUMN_LENGTH, COLUMN_LAST_TOUCH_TIMESTAMP, + }; + private static final String TABLE_SCHEMA = + "(" + + COLUMN_NAME + + " TEXT PRIMARY KEY NOT NULL," + + COLUMN_LENGTH + + " INTEGER NOT NULL," + + COLUMN_LAST_TOUCH_TIMESTAMP + + " INTEGER NOT NULL)"; + + private final DatabaseProvider databaseProvider; + + private @MonotonicNonNull String tableName; + + /** + * Deletes index data for the specified cache. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @param databaseProvider Provides the database in which the index is stored. + * @param uid The cache UID. + * @throws DatabaseIOException If an error occurs deleting the index data. + */ + @WorkerThread + public static void delete(DatabaseProvider databaseProvider, long uid) + throws DatabaseIOException { + String hexUid = Long.toHexString(uid); + try { + String tableName = getTableName(hexUid); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransactionNonExclusive(); + try { + VersionTable.removeVersion( + writableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid); + dropTable(writableDatabase, tableName); + writableDatabase.setTransactionSuccessful(); + } finally { + writableDatabase.endTransaction(); + } + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + /** @param databaseProvider Provides the database in which the index is stored. */ + public CacheFileMetadataIndex(DatabaseProvider databaseProvider) { + this.databaseProvider = databaseProvider; + } + + /** + * Initializes the index for the given cache UID. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @param uid The cache UID. + * @throws DatabaseIOException If an error occurs initializing the index. + */ + @WorkerThread + public void initialize(long uid) throws DatabaseIOException { + try { + String hexUid = Long.toHexString(uid); + tableName = getTableName(hexUid); + SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase(); + int version = + VersionTable.getVersion( + readableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid); + if (version != TABLE_VERSION) { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransactionNonExclusive(); + try { + VersionTable.setVersion( + writableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid, TABLE_VERSION); + dropTable(writableDatabase, tableName); + writableDatabase.execSQL("CREATE TABLE " + tableName + " " + TABLE_SCHEMA); + writableDatabase.setTransactionSuccessful(); + } finally { + writableDatabase.endTransaction(); + } + } + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + /** + * Returns all file metadata keyed by file name. The returned map is mutable and may be modified + * by the caller. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @return The file metadata keyed by file name. + * @throws DatabaseIOException If an error occurs loading the metadata. + */ + @WorkerThread + public Map getAll() throws DatabaseIOException { + try (Cursor cursor = getCursor()) { + Map fileMetadata = new HashMap<>(cursor.getCount()); + while (cursor.moveToNext()) { + String name = cursor.getString(COLUMN_INDEX_NAME); + long length = cursor.getLong(COLUMN_INDEX_LENGTH); + long lastTouchTimestamp = cursor.getLong(COLUMN_INDEX_LAST_TOUCH_TIMESTAMP); + fileMetadata.put(name, new CacheFileMetadata(length, lastTouchTimestamp)); + } + return fileMetadata; + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + /** + * Sets metadata for a given file. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @param name The name of the file. + * @param length The file length. + * @param lastTouchTimestamp The file last touch timestamp. + * @throws DatabaseIOException If an error occurs setting the metadata. + */ + @WorkerThread + public void set(String name, long length, long lastTouchTimestamp) throws DatabaseIOException { + Assertions.checkNotNull(tableName); + try { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(COLUMN_NAME, name); + values.put(COLUMN_LENGTH, length); + values.put(COLUMN_LAST_TOUCH_TIMESTAMP, lastTouchTimestamp); + writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values); + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + /** + * Removes metadata. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @param name The name of the file whose metadata is to be removed. + * @throws DatabaseIOException If an error occurs removing the metadata. + */ + @WorkerThread + public void remove(String name) throws DatabaseIOException { + Assertions.checkNotNull(tableName); + try { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.delete(tableName, WHERE_NAME_EQUALS, new String[] {name}); + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + /** + * Removes metadata. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @param names The names of the files whose metadata is to be removed. + * @throws DatabaseIOException If an error occurs removing the metadata. + */ + @WorkerThread + public void removeAll(Set names) throws DatabaseIOException { + Assertions.checkNotNull(tableName); + try { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransactionNonExclusive(); + try { + for (String name : names) { + writableDatabase.delete(tableName, WHERE_NAME_EQUALS, new String[] {name}); + } + writableDatabase.setTransactionSuccessful(); + } finally { + writableDatabase.endTransaction(); + } + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + private Cursor getCursor() { + Assertions.checkNotNull(tableName); + return databaseProvider + .getReadableDatabase() + .query( + tableName, + COLUMNS, + /* selection */ null, + /* selectionArgs= */ null, + /* groupBy= */ null, + /* having= */ null, + /* orderBy= */ null); + } + + private static void dropTable(SQLiteDatabase writableDatabase, String tableName) { + writableDatabase.execSQL("DROP TABLE IF EXISTS " + tableName); + } + + private static String getTableName(String hexUid) { + return TABLE_PREFIX + hexUid; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java new file mode 100644 index 000000000000..1c30a4b03e7f --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; + +/** Factory for cache keys. */ +public interface CacheKeyFactory { + + /** + * Returns a cache key for the given {@link DataSpec}. + * + * @param dataSpec The data being cached. + */ + String buildCacheKey(DataSpec dataSpec); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheSpan.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheSpan.java index e7cf3a6a330e..f57544f12bcf 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheSpan.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheSpan.java @@ -15,7 +15,8 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import java.io.File; @@ -40,17 +41,14 @@ public class CacheSpan implements Comparable { * Whether the {@link CacheSpan} is cached. */ public final boolean isCached; - /** - * The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false. - */ - public final File file; - /** - * The last access timestamp, or {@link C#TIME_UNSET} if {@link #isCached} is false. - */ - public final long lastAccessTimestamp; + /** The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false. */ + @Nullable public final File file; + /** The last touch timestamp, or {@link C#TIME_UNSET} if {@link #isCached} is false. */ + public final long lastTouchTimestamp; /** - * Creates a hole CacheSpan which isn't cached, has no last access time and no file associated. + * Creates a hole CacheSpan which isn't cached, has no last touch timestamp and no file + * associated. * * @param key The cache key that uniquely identifies the original stream. * @param position The position of the {@link CacheSpan} in the original stream. @@ -68,17 +66,18 @@ public class CacheSpan implements Comparable { * @param position The position of the {@link CacheSpan} in the original stream. * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an * open-ended hole. - * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if - * {@link #isCached} is false. + * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} if {@link + * #isCached} is false. * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole. */ - public CacheSpan(String key, long position, long length, long lastAccessTimestamp, File file) { + public CacheSpan( + String key, long position, long length, long lastTouchTimestamp, @Nullable File file) { this.key = key; this.position = position; this.length = length; this.isCached = file != null; this.file = file; - this.lastAccessTimestamp = lastAccessTimestamp; + this.lastTouchTimestamp = lastTouchTimestamp; } /** diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheUtil.java index de50bb08215a..01fef2b6052b 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheUtil.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheUtil.java @@ -1,49 +1,63 @@ /* - * Copyright (C) 2017 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, + * Copyright (C) 2017 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 + * See the License for the specific language governing permissions and * limitations under the License. */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; import android.net.Uri; +import android.util.Pair; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSourceException; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.PriorityTaskManager; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.io.EOFException; import java.io.IOException; import java.util.NavigableSet; +import java.util.concurrent.atomic.AtomicBoolean; /** * Caching related utility methods. */ public final class CacheUtil { - /** Holds the counters used during caching. */ - public static class CachingCounters { - /** Total number of already cached bytes. */ - public long alreadyCachedBytes; + /** Receives progress updates during cache operations. */ + public interface ProgressListener { + /** - * Total number of downloaded bytes. + * Called when progress is made during a cache operation. * - *

{@link #getCached(DataSpec, Cache, CachingCounters)} sets it to the count of the missing - * bytes or to {@link C#LENGTH_UNSET} if {@code dataSpec} is unbounded and content length isn't - * available in the {@code cache}. + * @param requestLength The length of the content being cached in bytes, or {@link + * C#LENGTH_UNSET} if unknown. + * @param bytesCached The number of bytes that are cached. + * @param newBytesCached The number of bytes that have been newly cached since the last progress + * update. */ - public long downloadedBytes; + void onProgress(long requestLength, long bytesCached, long newBytesCached); } + /** Default buffer size to be used while caching. */ + public static final int DEFAULT_BUFFER_SIZE_BYTES = 128 * 1024; + + /** Default {@link CacheKeyFactory}. */ + public static final CacheKeyFactory DEFAULT_CACHE_KEY_FACTORY = + (dataSpec) -> dataSpec.key != null ? dataSpec.key : generateKey(dataSpec.uri); + /** * Generates a cache key out of the given {@link Uri}. * @@ -54,161 +68,267 @@ public final class CacheUtil { } /** - * Returns the {@code dataSpec.key} if not null, otherwise generates a cache key out of {@code - * dataSpec.uri} - * - * @param dataSpec Defines a content which the requested key is for. - */ - public static String getKey(DataSpec dataSpec) { - return dataSpec.key != null ? dataSpec.key : generateKey(dataSpec.uri); - } - - /** - * Returns already cached and missing bytes in the {@cache} for the data defined by {@code - * dataSpec}. + * Queries the cache to obtain the request length and the number of bytes already cached for a + * given {@link DataSpec}. * * @param dataSpec Defines the data to be checked. * @param cache A {@link Cache} which has the data. - * @param counters The counters to be set. If null a new {@link CachingCounters} is created and - * used. - * @return The used {@link CachingCounters} instance. + * @param cacheKeyFactory An optional factory for cache keys. + * @return A pair containing the request length and the number of bytes that are already cached. */ - public static CachingCounters getCached(DataSpec dataSpec, Cache cache, - CachingCounters counters) { - try { - return internalCache(dataSpec, cache, null, null, null, 0, counters); - } catch (IOException | InterruptedException e) { - throw new IllegalStateException(e); + public static Pair getCached( + DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) { + String key = buildCacheKey(dataSpec, cacheKeyFactory); + long position = dataSpec.absoluteStreamPosition; + long requestLength = getRequestLength(dataSpec, cache, key); + long bytesAlreadyCached = 0; + long bytesLeft = requestLength; + while (bytesLeft != 0) { + long blockLength = + cache.getCachedLength( + key, position, bytesLeft != C.LENGTH_UNSET ? bytesLeft : Long.MAX_VALUE); + if (blockLength > 0) { + bytesAlreadyCached += blockLength; + } else { + blockLength = -blockLength; + if (blockLength == Long.MAX_VALUE) { + break; + } + } + position += blockLength; + bytesLeft -= bytesLeft == C.LENGTH_UNSET ? 0 : blockLength; } + return Pair.create(requestLength, bytesAlreadyCached); } /** - * Caches the data defined by {@code dataSpec} while skipping already cached data. + * Caches the data defined by {@code dataSpec}, skipping already cached data. Caching stops early + * if the end of the input is reached. + * + *

This method may be slow and shouldn't normally be called on the main thread. * * @param dataSpec Defines the data to be cached. * @param cache A {@link Cache} to store the data. + * @param cacheKeyFactory An optional factory for cache keys. + * @param upstream A {@link DataSource} for reading data not in the cache. + * @param progressListener A listener to receive progress updates, or {@code null}. + * @param isCanceled An optional flag that will interrupt caching if set to true. + * @throws IOException If an error occurs reading from the source. + * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}. + */ + @WorkerThread + public static void cache( + DataSpec dataSpec, + Cache cache, + @Nullable CacheKeyFactory cacheKeyFactory, + DataSource upstream, + @Nullable ProgressListener progressListener, + @Nullable AtomicBoolean isCanceled) + throws IOException, InterruptedException { + cache( + dataSpec, + cache, + cacheKeyFactory, + new CacheDataSource(cache, upstream), + new byte[DEFAULT_BUFFER_SIZE_BYTES], + /* priorityTaskManager= */ null, + /* priority= */ 0, + progressListener, + isCanceled, + /* enableEOFException= */ false); + } + + /** + * Caches the data defined by {@code dataSpec} while skipping already cached data. Caching stops + * early if end of input is reached and {@code enableEOFException} is false. + * + *

If a {@link PriorityTaskManager} is given, it's used to pause and resume caching depending + * on {@code priority} and the priority of other tasks registered to the PriorityTaskManager. + * Please note that it's the responsibility of the calling code to call {@link + * PriorityTaskManager#add} to register with the manager before calling this method, and to call + * {@link PriorityTaskManager#remove} afterwards to unregister. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @param dataSpec Defines the data to be cached. + * @param cache A {@link Cache} to store the data. + * @param cacheKeyFactory An optional factory for cache keys. * @param dataSource A {@link CacheDataSource} that works on the {@code cache}. * @param buffer The buffer to be used while caching. * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with * caching. * @param priority The priority of this task. Used with {@code priorityTaskManager}. - * @param counters The counters to be set during caching. If not null its values reset to - * zero before using. If null a new {@link CachingCounters} is created and used. - * @return The used {@link CachingCounters} instance. + * @param progressListener A listener to receive progress updates, or {@code null}. + * @param isCanceled An optional flag that will interrupt caching if set to true. + * @param enableEOFException Whether to throw an {@link EOFException} if end of input has been + * reached unexpectedly. * @throws IOException If an error occurs reading from the source. - * @throws InterruptedException If the thread was interrupted. + * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}. */ - public static CachingCounters cache(DataSpec dataSpec, Cache cache, CacheDataSource dataSource, - byte[] buffer, PriorityTaskManager priorityTaskManager, int priority, - CachingCounters counters) throws IOException, InterruptedException { + @WorkerThread + public static void cache( + DataSpec dataSpec, + Cache cache, + @Nullable CacheKeyFactory cacheKeyFactory, + CacheDataSource dataSource, + byte[] buffer, + @Nullable PriorityTaskManager priorityTaskManager, + int priority, + @Nullable ProgressListener progressListener, + @Nullable AtomicBoolean isCanceled, + boolean enableEOFException) + throws IOException, InterruptedException { Assertions.checkNotNull(dataSource); Assertions.checkNotNull(buffer); - return internalCache(dataSpec, cache, dataSource, buffer, priorityTaskManager, priority, - counters); - } - /** - * Caches the data defined by {@code dataSpec} while skipping already cached data. If {@code - * dataSource} or {@code buffer} is null performs a dry run. - * - * @param dataSpec Defines the data to be cached. - * @param cache A {@link Cache} to store the data. - * @param dataSource A {@link CacheDataSource} that works on the {@code cache}. If null a dry run - * is performed. - * @param buffer The buffer to be used while caching. If null a dry run is performed. - * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with - * caching. - * @param priority The priority of this task. Used with {@code priorityTaskManager}. - * @param counters The counters to be set during caching. If not null its values reset to - * zero before using. If null a new {@link CachingCounters} is created and used. - * @return The used {@link CachingCounters} instance. - * @throws IOException If not dry run and an error occurs reading from the source. - * @throws InterruptedException If not dry run and the thread was interrupted. - */ - private static CachingCounters internalCache(DataSpec dataSpec, Cache cache, - CacheDataSource dataSource, byte[] buffer, PriorityTaskManager priorityTaskManager, - int priority, CachingCounters counters) throws IOException, InterruptedException { - long start = dataSpec.position; - long left = dataSpec.length; - String key = getKey(dataSpec); - if (left == C.LENGTH_UNSET) { - left = cache.getContentLength(key); - if (left == C.LENGTH_UNSET) { - left = Long.MAX_VALUE; - } - } - if (counters == null) { - counters = new CachingCounters(); + String key = buildCacheKey(dataSpec, cacheKeyFactory); + long bytesLeft; + ProgressNotifier progressNotifier = null; + if (progressListener != null) { + progressNotifier = new ProgressNotifier(progressListener); + Pair lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory); + progressNotifier.init(lengthAndBytesAlreadyCached.first, lengthAndBytesAlreadyCached.second); + bytesLeft = lengthAndBytesAlreadyCached.first; } else { - counters.alreadyCachedBytes = 0; - counters.downloadedBytes = 0; + bytesLeft = getRequestLength(dataSpec, cache, key); } - while (left > 0) { - long blockLength = cache.getCachedBytes(key, start, left); - // Skip already cached data + + long position = dataSpec.absoluteStreamPosition; + boolean lengthUnset = bytesLeft == C.LENGTH_UNSET; + while (bytesLeft != 0) { + throwExceptionIfInterruptedOrCancelled(isCanceled); + long blockLength = + cache.getCachedLength(key, position, lengthUnset ? Long.MAX_VALUE : bytesLeft); if (blockLength > 0) { - counters.alreadyCachedBytes += blockLength; + // Skip already cached data. } else { // There is a hole in the cache which is at least "-blockLength" long. blockLength = -blockLength; - if (dataSource != null && buffer != null) { - DataSpec subDataSpec = new DataSpec(dataSpec.uri, start, - blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength, key); - long read = readAndDiscard(subDataSpec, dataSource, buffer, priorityTaskManager, - priority); - counters.downloadedBytes += read; - if (read < blockLength) { - // Reached end of data. - break; + long length = blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength; + boolean isLastBlock = length == bytesLeft; + long read = + readAndDiscard( + dataSpec, + position, + length, + dataSource, + buffer, + priorityTaskManager, + priority, + progressNotifier, + isLastBlock, + isCanceled); + if (read < blockLength) { + // Reached to the end of the data. + if (enableEOFException && !lengthUnset) { + throw new EOFException(); } - } else if (blockLength == Long.MAX_VALUE) { - counters.downloadedBytes = C.LENGTH_UNSET; break; - } else { - counters.downloadedBytes += blockLength; } } - start += blockLength; - if (left != Long.MAX_VALUE) { - left -= blockLength; + position += blockLength; + if (!lengthUnset) { + bytesLeft -= blockLength; } } - return counters; + } + + private static long getRequestLength(DataSpec dataSpec, Cache cache, String key) { + if (dataSpec.length != C.LENGTH_UNSET) { + return dataSpec.length; + } else { + long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key)); + return contentLength == C.LENGTH_UNSET + ? C.LENGTH_UNSET + : contentLength - dataSpec.absoluteStreamPosition; + } } /** * Reads and discards all data specified by the {@code dataSpec}. * - * @param dataSpec Defines the data to be read. + * @param dataSpec Defines the data to be read. {@code absoluteStreamPosition} and {@code length} + * fields are overwritten by the following parameters. + * @param absoluteStreamPosition The absolute position of the data to be read. + * @param length Length of the data to be read, or {@link C#LENGTH_UNSET} if it is unknown. * @param dataSource The {@link DataSource} to read the data from. * @param buffer The buffer to be used while downloading. * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with * caching. * @param priority The priority of this task. + * @param progressNotifier A notifier through which to report progress updates, or {@code null}. + * @param isLastBlock Whether this read block is the last block of the content. + * @param isCanceled An optional flag that will interrupt caching if set to true. * @return Number of read bytes, or 0 if no data is available because the end of the opened range - * has been reached. + * has been reached. */ - private static long readAndDiscard(DataSpec dataSpec, DataSource dataSource, byte[] buffer, - PriorityTaskManager priorityTaskManager, int priority) + private static long readAndDiscard( + DataSpec dataSpec, + long absoluteStreamPosition, + long length, + DataSource dataSource, + byte[] buffer, + @Nullable PriorityTaskManager priorityTaskManager, + int priority, + @Nullable ProgressNotifier progressNotifier, + boolean isLastBlock, + @Nullable AtomicBoolean isCanceled) throws IOException, InterruptedException { + long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition; + long initialPositionOffset = positionOffset; + long endOffset = length != C.LENGTH_UNSET ? positionOffset + length : C.POSITION_UNSET; while (true) { if (priorityTaskManager != null) { // Wait for any other thread with higher priority to finish its job. priorityTaskManager.proceed(priority); } + throwExceptionIfInterruptedOrCancelled(isCanceled); try { - dataSource.open(dataSpec); - long totalRead = 0; - while (true) { - if (Thread.interrupted()) { - throw new InterruptedException(); + long resolvedLength = C.LENGTH_UNSET; + boolean isDataSourceOpen = false; + if (endOffset != C.POSITION_UNSET) { + // If a specific length is given, first try to open the data source for that length to + // avoid more data then required to be requested. If the given length exceeds the end of + // input we will get a "position out of range" error. In that case try to open the source + // again with unset length. + try { + resolvedLength = + dataSource.open(dataSpec.subrange(positionOffset, endOffset - positionOffset)); + isDataSourceOpen = true; + } catch (IOException exception) { + if (!isLastBlock || !isCausedByPositionOutOfRange(exception)) { + throw exception; + } + Util.closeQuietly(dataSource); } - int read = dataSource.read(buffer, 0, buffer.length); - if (read == C.RESULT_END_OF_INPUT) { - return totalRead; - } - totalRead += read; } + if (!isDataSourceOpen) { + resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, C.LENGTH_UNSET)); + } + if (isLastBlock && progressNotifier != null && resolvedLength != C.LENGTH_UNSET) { + progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength); + } + while (positionOffset != endOffset) { + throwExceptionIfInterruptedOrCancelled(isCanceled); + int bytesRead = + dataSource.read( + buffer, + 0, + endOffset != C.POSITION_UNSET + ? (int) Math.min(buffer.length, endOffset - positionOffset) + : buffer.length); + if (bytesRead == C.RESULT_END_OF_INPUT) { + if (progressNotifier != null) { + progressNotifier.onRequestLengthResolved(positionOffset); + } + break; + } + positionOffset += bytesRead; + if (progressNotifier != null) { + progressNotifier.onBytesCached(bytesRead); + } + } + return positionOffset - initialPositionOffset; } catch (PriorityTaskManager.PriorityTooLowException exception) { // catch and try again } finally { @@ -217,21 +337,98 @@ public final class CacheUtil { } } - /** Removes all of the data in the {@code cache} pointed by the {@code key}. */ + /** + * Removes all of the data specified by the {@code dataSpec}. + * + *

This methods blocks until the operation is complete. + * + * @param dataSpec Defines the data to be removed. + * @param cache A {@link Cache} to store the data. + * @param cacheKeyFactory An optional factory for cache keys. + */ + @WorkerThread + public static void remove( + DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) { + remove(cache, buildCacheKey(dataSpec, cacheKeyFactory)); + } + + /** + * Removes all of the data specified by the {@code key}. + * + *

This methods blocks until the operation is complete. + * + * @param cache A {@link Cache} to store the data. + * @param key The key whose data should be removed. + */ + @WorkerThread public static void remove(Cache cache, String key) { NavigableSet cachedSpans = cache.getCachedSpans(key); - if (cachedSpans == null) { - return; - } for (CacheSpan cachedSpan : cachedSpans) { try { cache.removeSpan(cachedSpan); } catch (Cache.CacheException e) { - // do nothing + // Do nothing. } } } + /* package */ static boolean isCausedByPositionOutOfRange(IOException e) { + Throwable cause = e; + while (cause != null) { + if (cause instanceof DataSourceException) { + int reason = ((DataSourceException) cause).reason; + if (reason == DataSourceException.POSITION_OUT_OF_RANGE) { + return true; + } + } + cause = cause.getCause(); + } + return false; + } + + private static String buildCacheKey( + DataSpec dataSpec, @Nullable CacheKeyFactory cacheKeyFactory) { + return (cacheKeyFactory != null ? cacheKeyFactory : DEFAULT_CACHE_KEY_FACTORY) + .buildCacheKey(dataSpec); + } + + private static void throwExceptionIfInterruptedOrCancelled(@Nullable AtomicBoolean isCanceled) + throws InterruptedException { + if (Thread.interrupted() || (isCanceled != null && isCanceled.get())) { + throw new InterruptedException(); + } + } + private CacheUtil() {} + private static final class ProgressNotifier { + /** The listener to notify when progress is made. */ + private final ProgressListener listener; + /** The length of the content being cached in bytes, or {@link C#LENGTH_UNSET} if unknown. */ + private long requestLength; + /** The number of bytes that are cached. */ + private long bytesCached; + + public ProgressNotifier(ProgressListener listener) { + this.listener = listener; + } + + public void init(long requestLength, long bytesCached) { + this.requestLength = requestLength; + this.bytesCached = bytesCached; + listener.onProgress(requestLength, bytesCached, /* newBytesCached= */ 0); + } + + public void onRequestLengthResolved(long requestLength) { + if (this.requestLength == C.LENGTH_UNSET && requestLength != C.LENGTH_UNSET) { + this.requestLength = requestLength; + listener.onProgress(requestLength, bytesCached, /* newBytesCached= */ 0); + } + } + + public void onBytesCached(long newBytesCached) { + bytesCached += newBytesCached; + listener.onProgress(requestLength, bytesCached, newBytesCached); + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContent.java index 5f7eb66f66c9..660a2a3cb369 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContent.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -15,80 +15,69 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; -import org.mozilla.thirdparty.com.google.android.exoplayer2.C; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.Cache.CacheException; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import java.io.File; import java.util.TreeSet; -/** - * Defines the cached content for a single stream. - */ -/*package*/ final class CachedContent { +/** Defines the cached content for a single stream. */ +/* package */ final class CachedContent { - /** - * The cache file id that uniquely identifies the original stream. - */ + private static final String TAG = "CachedContent"; + + /** The cache file id that uniquely identifies the original stream. */ public final int id; - /** - * The cache key that uniquely identifies the original stream. - */ + /** The cache key that uniquely identifies the original stream. */ public final String key; - /** - * The cached spans of this content. - */ + /** The cached spans of this content. */ private final TreeSet cachedSpans; - /** - * The length of the original stream, or {@link C#LENGTH_UNSET} if the length is unknown. - */ - private long length; - - /** - * Reads an instance from a {@link DataInputStream}. - * - * @param input Input stream containing values needed to initialize CachedContent instance. - * @throws IOException If an error occurs during reading values. - */ - public CachedContent(DataInputStream input) throws IOException { - this(input.readInt(), input.readUTF(), input.readLong()); - } + /** Metadata values. */ + private DefaultContentMetadata metadata; + /** Whether the content is locked. */ + private boolean locked; /** * Creates a CachedContent. * * @param id The cache file id. * @param key The cache stream key. - * @param length The length of the original stream. */ - public CachedContent(int id, String key, long length) { + public CachedContent(int id, String key) { + this(id, key, DefaultContentMetadata.EMPTY); + } + + public CachedContent(int id, String key, DefaultContentMetadata metadata) { this.id = id; this.key = key; - this.length = length; + this.metadata = metadata; this.cachedSpans = new TreeSet<>(); } + /** Returns the metadata. */ + public DefaultContentMetadata getMetadata() { + return metadata; + } + /** - * Writes the instance to a {@link DataOutputStream}. + * Applies {@code mutations} to the metadata. * - * @param output Output stream to store the values. - * @throws IOException If an error occurs during writing values to output. + * @return Whether {@code mutations} changed any metadata. */ - public void writeToStream(DataOutputStream output) throws IOException { - output.writeInt(id); - output.writeUTF(key); - output.writeLong(length); + public boolean applyMetadataMutations(ContentMetadataMutations mutations) { + DefaultContentMetadata oldMetadata = metadata; + metadata = metadata.copyWithMutationsApplied(mutations); + return !metadata.equals(oldMetadata); } - /** Returns the length of the content. */ - public long getLength() { - return length; + /** Returns whether the content is locked. */ + public boolean isLocked() { + return locked; } - /** Sets the length of the content. */ - public void setLength(long length) { - this.length = length; + /** Sets the locked state of the content. */ + public void setLocked(boolean locked) { + this.locked = locked; } /** Adds the given {@link SimpleCacheSpan} which contains a part of the content. */ @@ -125,7 +114,7 @@ import java.util.TreeSet; * @param length The maximum length of the data to be returned. * @return the length of the cached or not cached data block length. */ - public long getCachedBytes(long position, long length) { + public long getCachedBytesLength(long position, long length) { SimpleCacheSpan span = getSpan(position); if (span.isHoleSpan()) { // We don't have a span covering the start of the queried region. @@ -152,24 +141,30 @@ import java.util.TreeSet; } /** - * Copies the given span with an updated last access time. Passed span becomes invalid after this - * call. + * Sets the given span's last touch timestamp. The passed span becomes invalid after this call. * * @param cacheSpan Span to be copied and updated. - * @return a span with the updated last access time. - * @throws CacheException If renaming of the underlying span file failed. + * @param lastTouchTimestamp The new last touch timestamp. + * @param updateFile Whether the span file should be renamed to have its timestamp match the new + * last touch time. + * @return A span with the updated last touch timestamp. */ - public SimpleCacheSpan touch(SimpleCacheSpan cacheSpan) throws CacheException { - // Remove the old span from the in-memory representation. + public SimpleCacheSpan setLastTouchTimestamp( + SimpleCacheSpan cacheSpan, long lastTouchTimestamp, boolean updateFile) { Assertions.checkState(cachedSpans.remove(cacheSpan)); - // Obtain a new span with updated last access timestamp. - SimpleCacheSpan newCacheSpan = cacheSpan.copyWithUpdatedLastAccessTime(id); - // Rename the cache file - if (!cacheSpan.file.renameTo(newCacheSpan.file)) { - throw new CacheException("Renaming of " + cacheSpan.file + " to " + newCacheSpan.file - + " failed."); + File file = cacheSpan.file; + if (updateFile) { + File directory = file.getParentFile(); + long position = cacheSpan.position; + File newFile = SimpleCacheSpan.getCacheFile(directory, id, position, lastTouchTimestamp); + if (file.renameTo(newFile)) { + file = newFile; + } else { + Log.w(TAG, "Failed to rename " + file + " to " + newFile); + } } - // Add the updated span back into the in-memory representation. + SimpleCacheSpan newCacheSpan = + cacheSpan.copyWithFileAndLastTouchTimestamp(file, lastTouchTimestamp); cachedSpans.add(newCacheSpan); return newCacheSpan; } @@ -188,12 +183,26 @@ import java.util.TreeSet; return false; } - /** Calculates a hash code for the header of this {@code CachedContent}. */ - public int headerHashCode() { + @Override + public int hashCode() { int result = id; result = 31 * result + key.hashCode(); - result = 31 * result + (int) (length ^ (length >>> 32)); + result = 31 * result + metadata.hashCode(); return result; } + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CachedContent that = (CachedContent) o; + return id == that.id + && key.equals(that.key) + && cachedSpans.equals(that.cachedSpans) + && metadata.equals(that.metadata); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 0ea36617cb8d..ac31e492a287 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -15,28 +15,40 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; -import android.util.Log; +import android.annotation.SuppressLint; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; import android.util.SparseArray; -import org.mozilla.thirdparty.com.google.android.exoplayer2.C; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.Cache.CacheException; +import android.util.SparseBooleanArray; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; +import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseIOException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseProvider; +import org.mozilla.thirdparty.com.google.android.exoplayer2.database.VersionTable; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.AtomicFile; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ReusableBufferedOutputStream; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; -import java.util.LinkedList; +import java.util.Map; import java.util.Random; import java.util.Set; import javax.crypto.Cipher; @@ -45,79 +57,169 @@ import javax.crypto.CipherOutputStream; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; +import org.checkerframework.checker.nullness.compatqual.NullableType; -/** - * This class maintains the index of cached content. - */ -/*package*/ final class CachedContentIndex { +/** Maintains the index of cached content. */ +/* package */ class CachedContentIndex { - public static final String FILE_NAME = "cached_content_index.exi"; + /* package */ static final String FILE_NAME_ATOMIC = "cached_content_index.exi"; - private static final int VERSION = 1; - - private static final int FLAG_ENCRYPTED_INDEX = 1; - - private static final String TAG = "CachedContentIndex"; + private static final int INCREMENTAL_METADATA_READ_LENGTH = 10 * 1024 * 1024; private final HashMap keyToContent; - private final SparseArray idToKey; - private final AtomicFile atomicFile; - private final Cipher cipher; - private final SecretKeySpec secretKeySpec; - private boolean changed; - private ReusableBufferedOutputStream bufferedOutputStream; - /** - * Creates a CachedContentIndex which works on the index file in the given cacheDir. + * Maps assigned ids to their corresponding keys. Also contains (id -> null) entries for ids that + * have been removed from the index since it was last stored. This prevents reuse of these ids, + * which is necessary to avoid clashes that could otherwise occur as a result of the sequence: * - * @param cacheDir Directory where the index file is kept. + *

[1] (key1, id1) is removed from the in-memory index ... the index is not stored to disk ... + * [2] id1 is reused for a different key2 ... the index is not stored to disk ... [3] A file for + * key2 is partially written using a path corresponding to id1 ... the process is killed before + * the index is stored to disk ... [4] The index is read from disk, causing the partially written + * file to be incorrectly associated to key1 + * + *

By avoiding id reuse in step [2], a new id2 will be used instead. Step [4] will then delete + * the partially written file because the index does not contain an entry for id2. + * + *

When the index is next stored (id -> null) entries are removed, making the ids eligible for + * reuse. */ - public CachedContentIndex(File cacheDir) { - this(cacheDir, null); + private final SparseArray<@NullableType String> idToKey; + /** + * Tracks ids for which (id -> null) entries are present in idToKey, so that they can be removed + * efficiently when the index is next stored. + */ + private final SparseBooleanArray removedIds; + /** Tracks ids that are new since the index was last stored. */ + private final SparseBooleanArray newIds; + + private Storage storage; + @Nullable private Storage previousStorage; + + /** Returns whether the file is an index file. */ + public static boolean isIndexFile(String fileName) { + // Atomic file backups add additional suffixes to the file name. + return fileName.startsWith(FILE_NAME_ATOMIC); } /** - * Creates a CachedContentIndex which works on the index file in the given cacheDir. + * Deletes index data for the specified cache. * - * @param cacheDir Directory where the index file is kept. - * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC. - * The key must be 16 bytes long. + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @param databaseProvider Provides the database in which the index is stored. + * @param uid The cache UID. + * @throws DatabaseIOException If an error occurs deleting the index data. */ - public CachedContentIndex(File cacheDir, byte[] secretKey) { - if (secretKey != null) { - Assertions.checkArgument(secretKey.length == 16); - try { - cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); - secretKeySpec = new SecretKeySpec(secretKey, "AES"); - } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { - throw new IllegalStateException(e); // Should never happen. - } - } else { - cipher = null; - secretKeySpec = null; - } + @WorkerThread + public static void delete(DatabaseProvider databaseProvider, long uid) + throws DatabaseIOException { + DatabaseStorage.delete(databaseProvider, uid); + } + + /** + * Creates an instance supporting database storage only. + * + * @param databaseProvider Provides the database in which the index is stored. + */ + public CachedContentIndex(DatabaseProvider databaseProvider) { + this( + databaseProvider, + /* legacyStorageDir= */ null, + /* legacyStorageSecretKey= */ null, + /* legacyStorageEncrypt= */ false, + /* preferLegacyStorage= */ false); + } + + /** + * Creates an instance supporting either or both of database and legacy storage. + * + * @param databaseProvider Provides the database in which the index is stored, or {@code null} to + * use only legacy storage. + * @param legacyStorageDir The directory in which any legacy storage is stored, or {@code null} to + * use only database storage. + * @param legacyStorageSecretKey A 16 byte AES key for reading, and optionally writing, legacy + * storage. + * @param legacyStorageEncrypt Whether to encrypt when writing to legacy storage. Must be false if + * {@code legacyStorageSecretKey} is null. + * @param preferLegacyStorage Whether to use prefer legacy storage if both storage types are + * enabled. This option is only useful for downgrading from database storage back to legacy + * storage. + */ + public CachedContentIndex( + @Nullable DatabaseProvider databaseProvider, + @Nullable File legacyStorageDir, + @Nullable byte[] legacyStorageSecretKey, + boolean legacyStorageEncrypt, + boolean preferLegacyStorage) { + Assertions.checkState(databaseProvider != null || legacyStorageDir != null); keyToContent = new HashMap<>(); idToKey = new SparseArray<>(); - atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME)); - } - - /** Loads the index file. */ - public void load() { - Assertions.checkState(!changed); - if (!readFile()) { - atomicFile.delete(); - keyToContent.clear(); - idToKey.clear(); + removedIds = new SparseBooleanArray(); + newIds = new SparseBooleanArray(); + Storage databaseStorage = + databaseProvider != null ? new DatabaseStorage(databaseProvider) : null; + Storage legacyStorage = + legacyStorageDir != null + ? new LegacyStorage( + new File(legacyStorageDir, FILE_NAME_ATOMIC), + legacyStorageSecretKey, + legacyStorageEncrypt) + : null; + if (databaseStorage == null || (legacyStorage != null && preferLegacyStorage)) { + storage = legacyStorage; + previousStorage = databaseStorage; + } else { + storage = databaseStorage; + previousStorage = legacyStorage; } } - /** Stores the index data to index file if there is a change. */ - public void store() throws CacheException { - if (!changed) { - return; + /** + * Loads the index data for the given cache UID. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @param uid The UID of the cache whose index is to be loaded. + * @throws IOException If an error occurs initializing the index data. + */ + @WorkerThread + public void initialize(long uid) throws IOException { + storage.initialize(uid); + if (previousStorage != null) { + previousStorage.initialize(uid); } - writeFile(); - changed = false; + if (!storage.exists() && previousStorage != null && previousStorage.exists()) { + // Copy from previous storage into current storage. + previousStorage.load(keyToContent, idToKey); + storage.storeFully(keyToContent); + } else { + // Load from the current storage. + storage.load(keyToContent, idToKey); + } + if (previousStorage != null) { + previousStorage.delete(); + previousStorage = null; + } + } + + /** + * Stores the index data to index file if there is a change. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @throws IOException If an error occurs storing the index data. + */ + @WorkerThread + public void store() throws IOException { + storage.storeIncremental(keyToContent); + // Make ids that were removed since the index was last stored eligible for re-use. + int removedIdCount = removedIds.size(); + for (int i = 0; i < removedIdCount; i++) { + idToKey.remove(removedIds.keyAt(i)); + } + removedIds.clear(); + newIds.clear(); } /** @@ -126,12 +228,9 @@ import javax.crypto.spec.SecretKeySpec; * @param key The cache key that uniquely identifies the original stream. * @return A new or existing CachedContent instance with the given key. */ - public CachedContent add(String key) { + public CachedContent getOrAdd(String key) { CachedContent cachedContent = keyToContent.get(key); - if (cachedContent == null) { - cachedContent = addNew(key, C.LENGTH_UNSET); - } - return cachedContent; + return cachedContent == null ? addNew(key) : cachedContent; } /** Returns a CachedContent instance with the given key or null if there isn't one. */ @@ -152,7 +251,7 @@ import javax.crypto.spec.SecretKeySpec; /** Returns an existing or new id assigned to the given key. */ public int assignIdForKey(String key) { - return add(key).id; + return getOrAdd(key).id; } /** Returns the key which has the given id assigned. */ @@ -160,30 +259,33 @@ import javax.crypto.spec.SecretKeySpec; return idToKey.get(id); } - /** - * Removes {@link CachedContent} with the given key from index. It shouldn't contain any spans. - * - * @throws IllegalStateException If {@link CachedContent} isn't empty. - */ - public void removeEmpty(String key) { - CachedContent cachedContent = keyToContent.remove(key); - if (cachedContent != null) { - Assertions.checkState(cachedContent.isEmpty()); - idToKey.remove(cachedContent.id); - changed = true; + /** Removes {@link CachedContent} with the given key from index if it's empty and not locked. */ + public void maybeRemove(String key) { + CachedContent cachedContent = keyToContent.get(key); + if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) { + keyToContent.remove(key); + int id = cachedContent.id; + boolean neverStored = newIds.get(id); + storage.onRemove(cachedContent, neverStored); + if (neverStored) { + // The id can be reused immediately. + idToKey.remove(id); + newIds.delete(id); + } else { + // Keep an entry in idToKey to stop the id from being reused until the index is next stored, + // and add an entry to removedIds to track that it should be removed when this does happen. + idToKey.put(id, /* value= */ null); + removedIds.put(id, /* value= */ true); + } } } - /** Removes empty {@link CachedContent} instances from index. */ + /** Removes empty and not locked {@link CachedContent} instances from index. */ public void removeEmpty() { - LinkedList cachedContentToBeRemoved = new LinkedList<>(); - for (CachedContent cachedContent : keyToContent.values()) { - if (cachedContent.isEmpty()) { - cachedContentToBeRemoved.add(cachedContent.key); - } - } - for (String key : cachedContentToBeRemoved) { - removeEmpty(key); + String[] keys = new String[keyToContent.size()]; + keyToContent.keySet().toArray(keys); + for (String key : keys) { + maybeRemove(key); } } @@ -198,156 +300,52 @@ import javax.crypto.spec.SecretKeySpec; } /** - * Sets the content length for the given key. A new {@link CachedContent} is added if there isn't - * one already with the given key. + * Applies {@code mutations} to the {@link ContentMetadata} for the given key. A new {@link + * CachedContent} is added if there isn't one already with the given key. */ - public void setContentLength(String key, long length) { + public void applyContentMetadataMutations(String key, ContentMetadataMutations mutations) { + CachedContent cachedContent = getOrAdd(key); + if (cachedContent.applyMetadataMutations(mutations)) { + storage.onUpdate(cachedContent); + } + } + + /** Returns a {@link ContentMetadata} for the given key. */ + public ContentMetadata getContentMetadata(String key) { CachedContent cachedContent = get(key); - if (cachedContent != null) { - if (cachedContent.getLength() != length) { - cachedContent.setLength(length); - changed = true; - } - } else { - addNew(key, length); - } + return cachedContent != null ? cachedContent.getMetadata() : DefaultContentMetadata.EMPTY; } - /** - * Returns the content length for the given key if one set, or {@link - * org.mozilla.thirdparty.com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise. - */ - public long getContentLength(String key) { - CachedContent cachedContent = get(key); - return cachedContent == null ? C.LENGTH_UNSET : cachedContent.getLength(); - } - - private boolean readFile() { - DataInputStream input = null; - try { - InputStream inputStream = new BufferedInputStream(atomicFile.openRead()); - input = new DataInputStream(inputStream); - int version = input.readInt(); - if (version != VERSION) { - // Currently there is no other version - return false; - } - - int flags = input.readInt(); - if ((flags & FLAG_ENCRYPTED_INDEX) != 0) { - if (cipher == null) { - return false; - } - byte[] initializationVector = new byte[16]; - input.readFully(initializationVector); - IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); - try { - cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); - } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { - throw new IllegalStateException(e); - } - input = new DataInputStream(new CipherInputStream(inputStream, cipher)); - } else { - if (cipher != null) { - changed = true; // Force index to be rewritten encrypted after read. - } - } - - int count = input.readInt(); - int hashCode = 0; - for (int i = 0; i < count; i++) { - CachedContent cachedContent = new CachedContent(input); - add(cachedContent); - hashCode += cachedContent.headerHashCode(); - } - if (input.readInt() != hashCode) { - return false; - } - } catch (FileNotFoundException e) { - return false; - } catch (IOException e) { - Log.e(TAG, "Error reading cache content index file.", e); - return false; - } finally { - if (input != null) { - Util.closeQuietly(input); - } - } - return true; - } - - private void writeFile() throws CacheException { - DataOutputStream output = null; - try { - OutputStream outputStream = atomicFile.startWrite(); - if (bufferedOutputStream == null) { - bufferedOutputStream = new ReusableBufferedOutputStream(outputStream); - } else { - bufferedOutputStream.reset(outputStream); - } - output = new DataOutputStream(bufferedOutputStream); - output.writeInt(VERSION); - - int flags = cipher != null ? FLAG_ENCRYPTED_INDEX : 0; - output.writeInt(flags); - - if (cipher != null) { - byte[] initializationVector = new byte[16]; - new Random().nextBytes(initializationVector); - output.write(initializationVector); - IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); - try { - cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); - } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { - throw new IllegalStateException(e); // Should never happen. - } - output.flush(); - output = new DataOutputStream(new CipherOutputStream(bufferedOutputStream, cipher)); - } - - output.writeInt(keyToContent.size()); - int hashCode = 0; - for (CachedContent cachedContent : keyToContent.values()) { - cachedContent.writeToStream(output); - hashCode += cachedContent.headerHashCode(); - } - output.writeInt(hashCode); - atomicFile.endWrite(output); - // Avoid calling close twice. Duplicate CipherOutputStream.close calls did - // not used to be no-ops: https://android-review.googlesource.com/#/c/272799/ - output = null; - } catch (IOException e) { - throw new CacheException(e); - } finally { - Util.closeQuietly(output); - } - } - - private void add(CachedContent cachedContent) { - keyToContent.put(cachedContent.key, cachedContent); - idToKey.put(cachedContent.id, cachedContent.key); - } - - /** Adds the given CachedContent to the index. */ - /*package*/ void addNew(CachedContent cachedContent) { - add(cachedContent); - changed = true; - } - - private CachedContent addNew(String key, long length) { + private CachedContent addNew(String key) { int id = getNewId(idToKey); - CachedContent cachedContent = new CachedContent(id, key, length); - addNew(cachedContent); + CachedContent cachedContent = new CachedContent(id, key); + keyToContent.put(key, cachedContent); + idToKey.put(id, key); + newIds.put(id, true); + storage.onUpdate(cachedContent); return cachedContent; } + @SuppressLint("GetInstance") // Suppress warning about specifying "BC" as an explicit provider. + private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException { + // Workaround for https://issuetracker.google.com/issues/36976726 + if (Util.SDK_INT == 18) { + try { + return Cipher.getInstance("AES/CBC/PKCS5PADDING", "BC"); + } catch (Throwable ignored) { + // ignored + } + } + return Cipher.getInstance("AES/CBC/PKCS5PADDING"); + } + /** * Returns an id which isn't used in the given array. If the maximum id in the array is smaller * than {@link java.lang.Integer#MAX_VALUE} it just returns the next bigger integer. Otherwise it * returns the smallest unused non-negative integer. */ - //@VisibleForTesting - public static int getNewId(SparseArray idToKey) { + @VisibleForTesting + /* package */ static int getNewId(SparseArray idToKey) { int size = idToKey.size(); int id = size == 0 ? 0 : (idToKey.keyAt(size - 1) + 1); if (id < 0) { // In case if we pass max int value. @@ -361,4 +359,598 @@ import javax.crypto.spec.SecretKeySpec; return id; } + /** + * Deserializes a {@link DefaultContentMetadata} from the given input stream. + * + * @param input Input stream to read from. + * @return a {@link DefaultContentMetadata} instance. + * @throws IOException If an error occurs during reading from the input. + */ + private static DefaultContentMetadata readContentMetadata(DataInputStream input) + throws IOException { + int size = input.readInt(); + HashMap metadata = new HashMap<>(); + for (int i = 0; i < size; i++) { + String name = input.readUTF(); + int valueSize = input.readInt(); + if (valueSize < 0) { + throw new IOException("Invalid value size: " + valueSize); + } + // Grow the array incrementally to avoid OutOfMemoryError in the case that a corrupt (and very + // large) valueSize was read. In such cases the implementation below is expected to throw + // IOException from one of the readFully calls, due to the end of the input being reached. + int bytesRead = 0; + int nextBytesToRead = Math.min(valueSize, INCREMENTAL_METADATA_READ_LENGTH); + byte[] value = Util.EMPTY_BYTE_ARRAY; + while (bytesRead != valueSize) { + value = Arrays.copyOf(value, bytesRead + nextBytesToRead); + input.readFully(value, bytesRead, nextBytesToRead); + bytesRead += nextBytesToRead; + nextBytesToRead = Math.min(valueSize - bytesRead, INCREMENTAL_METADATA_READ_LENGTH); + } + metadata.put(name, value); + } + return new DefaultContentMetadata(metadata); + } + + /** + * Serializes itself to a {@link DataOutputStream}. + * + * @param output Output stream to store the values. + * @throws IOException If an error occurs writing to the output. + */ + private static void writeContentMetadata(DefaultContentMetadata metadata, DataOutputStream output) + throws IOException { + Set> entrySet = metadata.entrySet(); + output.writeInt(entrySet.size()); + for (Map.Entry entry : entrySet) { + output.writeUTF(entry.getKey()); + byte[] value = entry.getValue(); + output.writeInt(value.length); + output.write(value); + } + } + + /** Interface for the persistent index. */ + private interface Storage { + + /** Initializes the storage for the given cache UID. */ + void initialize(long uid); + + /** + * Returns whether the persisted index exists. + * + * @throws IOException If an error occurs determining whether the persisted index exists. + */ + boolean exists() throws IOException; + + /** + * Deletes the persisted index. + * + * @throws IOException If an error occurs deleting the index. + */ + void delete() throws IOException; + + /** + * Loads the persisted index into {@code content} and {@code idToKey}, creating it if it doesn't + * already exist. + * + *

If the persisted index is in a permanently bad state (i.e. all further attempts to load it + * are also expected to fail) then it will be deleted and the call will return successfully. For + * transient failures, {@link IOException} will be thrown. + * + * @param content The key to content map to populate with persisted data. + * @param idToKey The id to key map to populate with persisted data. + * @throws IOException If an error occurs loading the index. + */ + void load(HashMap content, SparseArray<@NullableType String> idToKey) + throws IOException; + + /** + * Writes the persisted index, creating it if it doesn't already exist and replacing any + * existing content if it does. + * + * @param content The key to content map to persist. + * @throws IOException If an error occurs persisting the index. + */ + void storeFully(HashMap content) throws IOException; + + /** + * Ensures incremental changes to the index since the initial {@link #initialize(long)} or last + * {@link #storeFully(HashMap)} are persisted. The storage will have been notified of all such + * changes via {@link #onUpdate(CachedContent)} and {@link #onRemove(CachedContent, boolean)}. + * + * @param content The key to content map to persist. + * @throws IOException If an error occurs persisting the index. + */ + void storeIncremental(HashMap content) throws IOException; + + /** + * Called when a {@link CachedContent} is added or updated. + * + * @param cachedContent The updated {@link CachedContent}. + */ + void onUpdate(CachedContent cachedContent); + + /** + * Called when a {@link CachedContent} is removed. + * + * @param cachedContent The removed {@link CachedContent}. + * @param neverStored True if the {@link CachedContent} was added more recently than when the + * index was last stored. + */ + void onRemove(CachedContent cachedContent, boolean neverStored); + } + + /** {@link Storage} implementation that uses an {@link AtomicFile}. */ + private static class LegacyStorage implements Storage { + + private static final int VERSION = 2; + private static final int VERSION_METADATA_INTRODUCED = 2; + private static final int FLAG_ENCRYPTED_INDEX = 1; + + private final boolean encrypt; + @Nullable private final Cipher cipher; + @Nullable private final SecretKeySpec secretKeySpec; + @Nullable private final Random random; + private final AtomicFile atomicFile; + + private boolean changed; + @Nullable private ReusableBufferedOutputStream bufferedOutputStream; + + public LegacyStorage(File file, @Nullable byte[] secretKey, boolean encrypt) { + Cipher cipher = null; + SecretKeySpec secretKeySpec = null; + if (secretKey != null) { + Assertions.checkArgument(secretKey.length == 16); + try { + cipher = getCipher(); + secretKeySpec = new SecretKeySpec(secretKey, "AES"); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalStateException(e); // Should never happen. + } + } else { + Assertions.checkArgument(!encrypt); + } + this.encrypt = encrypt; + this.cipher = cipher; + this.secretKeySpec = secretKeySpec; + random = encrypt ? new Random() : null; + atomicFile = new AtomicFile(file); + } + + @Override + public void initialize(long uid) { + // Do nothing. Legacy storage uses a separate file for each cache. + } + + @Override + public boolean exists() { + return atomicFile.exists(); + } + + @Override + public void delete() { + atomicFile.delete(); + } + + @Override + public void load( + HashMap content, SparseArray<@NullableType String> idToKey) { + Assertions.checkState(!changed); + if (!readFile(content, idToKey)) { + content.clear(); + idToKey.clear(); + atomicFile.delete(); + } + } + + @Override + public void storeFully(HashMap content) throws IOException { + writeFile(content); + changed = false; + } + + @Override + public void storeIncremental(HashMap content) throws IOException { + if (!changed) { + return; + } + storeFully(content); + } + + @Override + public void onUpdate(CachedContent cachedContent) { + changed = true; + } + + @Override + public void onRemove(CachedContent cachedContent, boolean neverStored) { + changed = true; + } + + private boolean readFile( + HashMap content, SparseArray<@NullableType String> idToKey) { + if (!atomicFile.exists()) { + return true; + } + + DataInputStream input = null; + try { + InputStream inputStream = new BufferedInputStream(atomicFile.openRead()); + input = new DataInputStream(inputStream); + int version = input.readInt(); + if (version < 0 || version > VERSION) { + return false; + } + + int flags = input.readInt(); + if ((flags & FLAG_ENCRYPTED_INDEX) != 0) { + if (cipher == null) { + return false; + } + byte[] initializationVector = new byte[16]; + input.readFully(initializationVector); + IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); + try { + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new IllegalStateException(e); + } + input = new DataInputStream(new CipherInputStream(inputStream, cipher)); + } else if (encrypt) { + changed = true; // Force index to be rewritten encrypted after read. + } + + int count = input.readInt(); + int hashCode = 0; + for (int i = 0; i < count; i++) { + CachedContent cachedContent = readCachedContent(version, input); + content.put(cachedContent.key, cachedContent); + idToKey.put(cachedContent.id, cachedContent.key); + hashCode += hashCachedContent(cachedContent, version); + } + int fileHashCode = input.readInt(); + boolean isEOF = input.read() == -1; + if (fileHashCode != hashCode || !isEOF) { + return false; + } + } catch (IOException e) { + return false; + } finally { + if (input != null) { + Util.closeQuietly(input); + } + } + return true; + } + + private void writeFile(HashMap content) throws IOException { + DataOutputStream output = null; + try { + OutputStream outputStream = atomicFile.startWrite(); + if (bufferedOutputStream == null) { + bufferedOutputStream = new ReusableBufferedOutputStream(outputStream); + } else { + bufferedOutputStream.reset(outputStream); + } + output = new DataOutputStream(bufferedOutputStream); + output.writeInt(VERSION); + + int flags = encrypt ? FLAG_ENCRYPTED_INDEX : 0; + output.writeInt(flags); + + if (encrypt) { + byte[] initializationVector = new byte[16]; + random.nextBytes(initializationVector); + output.write(initializationVector); + IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); + try { + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new IllegalStateException(e); // Should never happen. + } + output.flush(); + output = new DataOutputStream(new CipherOutputStream(bufferedOutputStream, cipher)); + } + + output.writeInt(content.size()); + int hashCode = 0; + for (CachedContent cachedContent : content.values()) { + writeCachedContent(cachedContent, output); + hashCode += hashCachedContent(cachedContent, VERSION); + } + output.writeInt(hashCode); + atomicFile.endWrite(output); + // Avoid calling close twice. Duplicate CipherOutputStream.close calls did + // not used to be no-ops: https://android-review.googlesource.com/#/c/272799/ + output = null; + } finally { + Util.closeQuietly(output); + } + } + + /** + * Calculates a hash code for a {@link CachedContent} which is compatible with a particular + * index version. + */ + private int hashCachedContent(CachedContent cachedContent, int version) { + int result = cachedContent.id; + result = 31 * result + cachedContent.key.hashCode(); + if (version < VERSION_METADATA_INTRODUCED) { + long length = ContentMetadata.getContentLength(cachedContent.getMetadata()); + result = 31 * result + (int) (length ^ (length >>> 32)); + } else { + result = 31 * result + cachedContent.getMetadata().hashCode(); + } + return result; + } + + /** + * Reads a {@link CachedContent} from a {@link DataInputStream}. + * + * @param version Version of the encoded data. + * @param input Input stream containing values needed to initialize CachedContent instance. + * @throws IOException If an error occurs during reading values. + */ + private CachedContent readCachedContent(int version, DataInputStream input) throws IOException { + int id = input.readInt(); + String key = input.readUTF(); + DefaultContentMetadata metadata; + if (version < VERSION_METADATA_INTRODUCED) { + long length = input.readLong(); + ContentMetadataMutations mutations = new ContentMetadataMutations(); + ContentMetadataMutations.setContentLength(mutations, length); + metadata = DefaultContentMetadata.EMPTY.copyWithMutationsApplied(mutations); + } else { + metadata = readContentMetadata(input); + } + return new CachedContent(id, key, metadata); + } + + /** + * Writes a {@link CachedContent} to a {@link DataOutputStream}. + * + * @param output Output stream to store the values. + * @throws IOException If an error occurs during writing values to output. + */ + private void writeCachedContent(CachedContent cachedContent, DataOutputStream output) + throws IOException { + output.writeInt(cachedContent.id); + output.writeUTF(cachedContent.key); + writeContentMetadata(cachedContent.getMetadata(), output); + } + } + + /** {@link Storage} implementation that uses an SQL database. */ + private static final class DatabaseStorage implements Storage { + + private static final String TABLE_PREFIX = DatabaseProvider.TABLE_PREFIX + "CacheIndex"; + private static final int TABLE_VERSION = 1; + + private static final String COLUMN_ID = "id"; + private static final String COLUMN_KEY = "key"; + private static final String COLUMN_METADATA = "metadata"; + + private static final int COLUMN_INDEX_ID = 0; + private static final int COLUMN_INDEX_KEY = 1; + private static final int COLUMN_INDEX_METADATA = 2; + + private static final String WHERE_ID_EQUALS = COLUMN_ID + " = ?"; + + private static final String[] COLUMNS = new String[] {COLUMN_ID, COLUMN_KEY, COLUMN_METADATA}; + private static final String TABLE_SCHEMA = + "(" + + COLUMN_ID + + " INTEGER PRIMARY KEY NOT NULL," + + COLUMN_KEY + + " TEXT NOT NULL," + + COLUMN_METADATA + + " BLOB NOT NULL)"; + + private final DatabaseProvider databaseProvider; + private final SparseArray pendingUpdates; + + private String hexUid; + private String tableName; + + public static void delete(DatabaseProvider databaseProvider, long uid) + throws DatabaseIOException { + delete(databaseProvider, Long.toHexString(uid)); + } + + public DatabaseStorage(DatabaseProvider databaseProvider) { + this.databaseProvider = databaseProvider; + pendingUpdates = new SparseArray<>(); + } + + @Override + public void initialize(long uid) { + hexUid = Long.toHexString(uid); + tableName = getTableName(hexUid); + } + + @Override + public boolean exists() throws DatabaseIOException { + return VersionTable.getVersion( + databaseProvider.getReadableDatabase(), + VersionTable.FEATURE_CACHE_CONTENT_METADATA, + hexUid) + != VersionTable.VERSION_UNSET; + } + + @Override + public void delete() throws DatabaseIOException { + delete(databaseProvider, hexUid); + } + + @Override + public void load( + HashMap content, SparseArray<@NullableType String> idToKey) + throws IOException { + Assertions.checkState(pendingUpdates.size() == 0); + try { + int version = + VersionTable.getVersion( + databaseProvider.getReadableDatabase(), + VersionTable.FEATURE_CACHE_CONTENT_METADATA, + hexUid); + if (version != TABLE_VERSION) { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransactionNonExclusive(); + try { + initializeTable(writableDatabase); + writableDatabase.setTransactionSuccessful(); + } finally { + writableDatabase.endTransaction(); + } + } + + try (Cursor cursor = getCursor()) { + while (cursor.moveToNext()) { + int id = cursor.getInt(COLUMN_INDEX_ID); + String key = cursor.getString(COLUMN_INDEX_KEY); + byte[] metadataBytes = cursor.getBlob(COLUMN_INDEX_METADATA); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(metadataBytes); + DataInputStream input = new DataInputStream(inputStream); + DefaultContentMetadata metadata = readContentMetadata(input); + + CachedContent cachedContent = new CachedContent(id, key, metadata); + content.put(cachedContent.key, cachedContent); + idToKey.put(cachedContent.id, cachedContent.key); + } + } + } catch (SQLiteException e) { + content.clear(); + idToKey.clear(); + throw new DatabaseIOException(e); + } + } + + @Override + public void storeFully(HashMap content) throws IOException { + try { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransactionNonExclusive(); + try { + initializeTable(writableDatabase); + for (CachedContent cachedContent : content.values()) { + addOrUpdateRow(writableDatabase, cachedContent); + } + writableDatabase.setTransactionSuccessful(); + pendingUpdates.clear(); + } finally { + writableDatabase.endTransaction(); + } + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + @Override + public void storeIncremental(HashMap content) throws IOException { + if (pendingUpdates.size() == 0) { + return; + } + try { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransactionNonExclusive(); + try { + for (int i = 0; i < pendingUpdates.size(); i++) { + CachedContent cachedContent = pendingUpdates.valueAt(i); + if (cachedContent == null) { + deleteRow(writableDatabase, pendingUpdates.keyAt(i)); + } else { + addOrUpdateRow(writableDatabase, cachedContent); + } + } + writableDatabase.setTransactionSuccessful(); + pendingUpdates.clear(); + } finally { + writableDatabase.endTransaction(); + } + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + @Override + public void onUpdate(CachedContent cachedContent) { + pendingUpdates.put(cachedContent.id, cachedContent); + } + + @Override + public void onRemove(CachedContent cachedContent, boolean neverStored) { + if (neverStored) { + pendingUpdates.delete(cachedContent.id); + } else { + pendingUpdates.put(cachedContent.id, null); + } + } + + private Cursor getCursor() { + return databaseProvider + .getReadableDatabase() + .query( + tableName, + COLUMNS, + /* selection= */ null, + /* selectionArgs= */ null, + /* groupBy= */ null, + /* having= */ null, + /* orderBy= */ null); + } + + private void initializeTable(SQLiteDatabase writableDatabase) throws DatabaseIOException { + VersionTable.setVersion( + writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid, TABLE_VERSION); + dropTable(writableDatabase, tableName); + writableDatabase.execSQL("CREATE TABLE " + tableName + " " + TABLE_SCHEMA); + } + + private void deleteRow(SQLiteDatabase writableDatabase, int key) { + writableDatabase.delete(tableName, WHERE_ID_EQUALS, new String[] {Integer.toString(key)}); + } + + private void addOrUpdateRow(SQLiteDatabase writableDatabase, CachedContent cachedContent) + throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + writeContentMetadata(cachedContent.getMetadata(), new DataOutputStream(outputStream)); + byte[] data = outputStream.toByteArray(); + + ContentValues values = new ContentValues(); + values.put(COLUMN_ID, cachedContent.id); + values.put(COLUMN_KEY, cachedContent.key); + values.put(COLUMN_METADATA, data); + writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values); + } + + private static void delete(DatabaseProvider databaseProvider, String hexUid) + throws DatabaseIOException { + try { + String tableName = getTableName(hexUid); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransactionNonExclusive(); + try { + VersionTable.removeVersion( + writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid); + dropTable(writableDatabase, tableName); + writableDatabase.setTransactionSuccessful(); + } finally { + writableDatabase.endTransaction(); + } + } catch (SQLException e) { + throw new DatabaseIOException(e); + } + } + + private static void dropTable(SQLiteDatabase writableDatabase, String tableName) { + writableDatabase.execSQL("DROP TABLE IF EXISTS " + tableName); + } + + private static String getTableName(String hexUid) { + return TABLE_PREFIX + hexUid; + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java index 665f6bb0e98d..9b08301ab884 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java @@ -15,9 +15,10 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; -import android.support.annotation.NonNull; -import android.util.Log; +import androidx.annotation.NonNull; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ChunkIndex; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.Iterator; import java.util.NavigableSet; @@ -50,14 +51,12 @@ public final class CachedRegionTracker implements Cache.Listener { synchronized (this) { NavigableSet cacheSpans = cache.addListener(cacheKey, this); - if (cacheSpans != null) { - // Merge the spans into regions. mergeSpan is more efficient when merging from high to low, - // which is why a descending iterator is used here. - Iterator spanIterator = cacheSpans.descendingIterator(); - while (spanIterator.hasNext()) { - CacheSpan span = spanIterator.next(); - mergeSpan(span); - } + // Merge the spans into regions. mergeSpan is more efficient when merging from high to low, + // which is why a descending iterator is used here. + Iterator spanIterator = cacheSpans.descendingIterator(); + while (spanIterator.hasNext()) { + CacheSpan span = spanIterator.next(); + mergeSpan(span); } } } @@ -197,8 +196,7 @@ public final class CachedRegionTracker implements Cache.Listener { @Override public int compareTo(@NonNull Region another) { - return startOffset < another.startOffset ? -1 - : startOffset == another.startOffset ? 0 : 1; + return Util.compareLong(startOffset, another.startOffset); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java new file mode 100644 index 000000000000..aa348230434f --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; + +import android.net.Uri; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; + +/** + * Interface for an immutable snapshot of keyed metadata. + */ +public interface ContentMetadata { + + /** + * Prefix for custom metadata keys. Applications can use keys starting with this prefix without + * any risk of their keys colliding with ones defined by the ExoPlayer library. + */ + @SuppressWarnings("unused") + String KEY_CUSTOM_PREFIX = "custom_"; + /** Key for redirected uri (type: String). */ + String KEY_REDIRECTED_URI = "exo_redir"; + /** Key for content length in bytes (type: long). */ + String KEY_CONTENT_LENGTH = "exo_len"; + + /** + * Returns a metadata value. + * + * @param key Key of the metadata to be returned. + * @param defaultValue Value to return if the metadata doesn't exist. + * @return The metadata value. + */ + @Nullable + byte[] get(String key, @Nullable byte[] defaultValue); + + /** + * Returns a metadata value. + * + * @param key Key of the metadata to be returned. + * @param defaultValue Value to return if the metadata doesn't exist. + * @return The metadata value. + */ + @Nullable + String get(String key, @Nullable String defaultValue); + + /** + * Returns a metadata value. + * + * @param key Key of the metadata to be returned. + * @param defaultValue Value to return if the metadata doesn't exist. + * @return The metadata value. + */ + long get(String key, long defaultValue); + + /** Returns whether the metadata is available. */ + boolean contains(String key); + + /** + * Returns the value stored under {@link #KEY_CONTENT_LENGTH}, or {@link C#LENGTH_UNSET} if not + * set. + */ + static long getContentLength(ContentMetadata contentMetadata) { + return contentMetadata.get(KEY_CONTENT_LENGTH, C.LENGTH_UNSET); + } + + /** + * Returns the value stored under {@link #KEY_REDIRECTED_URI} as a {@link Uri}, or {code null} if + * not set. + */ + @Nullable + static Uri getRedirectedUri(ContentMetadata contentMetadata) { + String redirectedUri = contentMetadata.get(KEY_REDIRECTED_URI, (String) null); + return redirectedUri == null ? null : Uri.parse(redirectedUri); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java new file mode 100644 index 000000000000..c7a8d9f71113 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; + +import android.net.Uri; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Defines multiple mutations on metadata value which are applied atomically. This class isn't + * thread safe. + */ +public class ContentMetadataMutations { + + /** + * Adds a mutation to set the {@link ContentMetadata#KEY_CONTENT_LENGTH} value, or to remove any + * existing value if {@link C#LENGTH_UNSET} is passed. + * + * @param mutations The mutations to modify. + * @param length The length value, or {@link C#LENGTH_UNSET} to remove any existing entry. + * @return The mutations instance, for convenience. + */ + public static ContentMetadataMutations setContentLength( + ContentMetadataMutations mutations, long length) { + return mutations.set(ContentMetadata.KEY_CONTENT_LENGTH, length); + } + + /** + * Adds a mutation to set the {@link ContentMetadata#KEY_REDIRECTED_URI} value, or to remove any + * existing entry if {@code null} is passed. + * + * @param mutations The mutations to modify. + * @param uri The {@link Uri} value, or {@code null} to remove any existing entry. + * @return The mutations instance, for convenience. + */ + public static ContentMetadataMutations setRedirectedUri( + ContentMetadataMutations mutations, @Nullable Uri uri) { + if (uri == null) { + return mutations.remove(ContentMetadata.KEY_REDIRECTED_URI); + } else { + return mutations.set(ContentMetadata.KEY_REDIRECTED_URI, uri.toString()); + } + } + + private final Map editedValues; + private final List removedValues; + + /** Constructs a DefaultMetadataMutations. */ + public ContentMetadataMutations() { + editedValues = new HashMap<>(); + removedValues = new ArrayList<>(); + } + + /** + * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} or {@code value} + * isn't allowed. + * + * @param name The name of the metadata value. + * @param value The value to be set. + * @return This instance, for convenience. + */ + public ContentMetadataMutations set(String name, String value) { + return checkAndSet(name, value); + } + + /** + * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} isn't allowed. + * + * @param name The name of the metadata value. + * @param value The value to be set. + * @return This instance, for convenience. + */ + public ContentMetadataMutations set(String name, long value) { + return checkAndSet(name, value); + } + + /** + * Adds a mutation to set a metadata value. Passing {@code null} as {@code name} or {@code value} + * isn't allowed. + * + * @param name The name of the metadata value. + * @param value The value to be set. + * @return This instance, for convenience. + */ + public ContentMetadataMutations set(String name, byte[] value) { + return checkAndSet(name, Arrays.copyOf(value, value.length)); + } + + /** + * Adds a mutation to remove a metadata value. + * + * @param name The name of the metadata value. + * @return This instance, for convenience. + */ + public ContentMetadataMutations remove(String name) { + removedValues.add(name); + editedValues.remove(name); + return this; + } + + /** Returns a list of names of metadata values to be removed. */ + public List getRemovedValues() { + return Collections.unmodifiableList(new ArrayList<>(removedValues)); + } + + /** Returns a map of metadata name, value pairs to be set. Values are copied. */ + public Map getEditedValues() { + HashMap hashMap = new HashMap<>(editedValues); + for (Entry entry : hashMap.entrySet()) { + Object value = entry.getValue(); + if (value instanceof byte[]) { + byte[] bytes = (byte[]) value; + entry.setValue(Arrays.copyOf(bytes, bytes.length)); + } + } + return Collections.unmodifiableMap(hashMap); + } + + private ContentMetadataMutations checkAndSet(String name, Object value) { + editedValues.put(Assertions.checkNotNull(name), Assertions.checkNotNull(value)); + removedValues.remove(name); + return this; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java new file mode 100644 index 000000000000..2602f834e7d9 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** Default implementation of {@link ContentMetadata}. Values are stored as byte arrays. */ +public final class DefaultContentMetadata implements ContentMetadata { + + /** An empty DefaultContentMetadata. */ + public static final DefaultContentMetadata EMPTY = + new DefaultContentMetadata(Collections.emptyMap()); + + private int hashCode; + + private final Map metadata; + + public DefaultContentMetadata() { + this(Collections.emptyMap()); + } + + /** @param metadata The metadata entries in their raw byte array form. */ + public DefaultContentMetadata(Map metadata) { + this.metadata = Collections.unmodifiableMap(metadata); + } + + /** + * Returns a copy {@link DefaultContentMetadata} with {@code mutations} applied. If {@code + * mutations} don't change anything, returns this instance. + */ + public DefaultContentMetadata copyWithMutationsApplied(ContentMetadataMutations mutations) { + Map mutatedMetadata = applyMutations(metadata, mutations); + if (isMetadataEqual(metadata, mutatedMetadata)) { + return this; + } + return new DefaultContentMetadata(mutatedMetadata); + } + + /** Returns the set of metadata entries in their raw byte array form. */ + public Set> entrySet() { + return metadata.entrySet(); + } + + @Override + @Nullable + public final byte[] get(String name, @Nullable byte[] defaultValue) { + if (metadata.containsKey(name)) { + byte[] bytes = metadata.get(name); + return Arrays.copyOf(bytes, bytes.length); + } else { + return defaultValue; + } + } + + @Override + @Nullable + public final String get(String name, @Nullable String defaultValue) { + if (metadata.containsKey(name)) { + byte[] bytes = metadata.get(name); + return new String(bytes, Charset.forName(C.UTF8_NAME)); + } else { + return defaultValue; + } + } + + @Override + public final long get(String name, long defaultValue) { + if (metadata.containsKey(name)) { + byte[] bytes = metadata.get(name); + return ByteBuffer.wrap(bytes).getLong(); + } else { + return defaultValue; + } + } + + @Override + public final boolean contains(String name) { + return metadata.containsKey(name); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return isMetadataEqual(metadata, ((DefaultContentMetadata) o).metadata); + } + + @Override + public int hashCode() { + if (hashCode == 0) { + int result = 0; + for (Entry entry : metadata.entrySet()) { + result += entry.getKey().hashCode() ^ Arrays.hashCode(entry.getValue()); + } + hashCode = result; + } + return hashCode; + } + + private static boolean isMetadataEqual(Map first, Map second) { + if (first.size() != second.size()) { + return false; + } + for (Entry entry : first.entrySet()) { + byte[] value = entry.getValue(); + byte[] otherValue = second.get(entry.getKey()); + if (!Arrays.equals(value, otherValue)) { + return false; + } + } + return true; + } + + private static Map applyMutations( + Map otherMetadata, ContentMetadataMutations mutations) { + HashMap metadata = new HashMap<>(otherMetadata); + removeValues(metadata, mutations.getRemovedValues()); + addValues(metadata, mutations.getEditedValues()); + return metadata; + } + + private static void removeValues(HashMap metadata, List names) { + for (int i = 0; i < names.size(); i++) { + metadata.remove(names.get(i)); + } + } + + private static void addValues(HashMap metadata, Map values) { + for (String name : values.keySet()) { + metadata.put(name, getBytes(values.get(name))); + } + } + + private static byte[] getBytes(Object value) { + if (value instanceof Long) { + return ByteBuffer.allocate(8).putLong((Long) value).array(); + } else if (value instanceof String) { + return ((String) value).getBytes(Charset.forName(C.UTF8_NAME)); + } else if (value instanceof byte[]) { + return (byte[]) value; + } else { + throw new IllegalArgumentException(); + } + } + +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java index d8eda181f083..56eff06b2515 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java @@ -15,14 +15,12 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache.Cache.CacheException; -import java.util.Comparator; import java.util.TreeSet; -/** - * Evicts least recently used cache files first. - */ -public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Comparator { +/** Evicts least recently used cache files first. */ +public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor { private final long maxBytes; private final TreeSet leastRecentlyUsed; @@ -31,7 +29,12 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar public LeastRecentlyUsedCacheEvictor(long maxBytes) { this.maxBytes = maxBytes; - this.leastRecentlyUsed = new TreeSet<>(this); + this.leastRecentlyUsed = new TreeSet<>(LeastRecentlyUsedCacheEvictor::compare); + } + + @Override + public boolean requiresCacheSpanTouches() { + return true; } @Override @@ -40,8 +43,10 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar } @Override - public void onStartFile(Cache cache, String key, long position, long maxLength) { - evictCache(cache, maxLength); + public void onStartFile(Cache cache, String key, long position, long length) { + if (length != C.LENGTH_UNSET) { + evictCache(cache, length); + } } @Override @@ -63,18 +68,8 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar onSpanAdded(cache, newSpan); } - @Override - public int compare(CacheSpan lhs, CacheSpan rhs) { - long lastAccessTimestampDelta = lhs.lastAccessTimestamp - rhs.lastAccessTimestamp; - if (lastAccessTimestampDelta == 0) { - // Use the standard compareTo method as a tie-break. - return lhs.compareTo(rhs); - } - return lhs.lastAccessTimestamp < rhs.lastAccessTimestamp ? -1 : 1; - } - private void evictCache(Cache cache, long requiredSpace) { - while (currentSize + requiredSpace > maxBytes) { + while (currentSize + requiredSpace > maxBytes && !leastRecentlyUsed.isEmpty()) { try { cache.removeSpan(leastRecentlyUsed.first()); } catch (CacheException e) { @@ -83,4 +78,12 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar } } + private static int compare(CacheSpan lhs, CacheSpan rhs) { + long lastTouchTimestampDelta = lhs.lastTouchTimestamp - rhs.lastTouchTimestamp; + if (lastTouchTimestampDelta == 0) { + // Use the standard compareTo method as a tie-break. + return lhs.compareTo(rhs); + } + return lhs.lastTouchTimestamp < rhs.lastTouchTimestamp ? -1 : 1; + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java index a6b7472abf17..75c1ad0a09d7 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java @@ -24,6 +24,11 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; */ public final class NoOpCacheEvictor implements CacheEvictor { + @Override + public boolean requiresCacheSpanTouches() { + return false; + } + @Override public void onCacheInitialized() { // Do nothing. diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 2f5b1056b530..9e36c48d88cf 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -16,39 +16,113 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; import android.os.ConditionVariable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseIOException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseProvider; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.io.File; +import java.io.IOException; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; +import java.util.Map; import java.util.NavigableSet; +import java.util.Random; import java.util.Set; import java.util.TreeSet; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link Cache} implementation that maintains an in-memory representation. + * + *

Only one instance of SimpleCache is allowed for a given directory at a given time. + * + *

To delete a SimpleCache, use {@link #delete(File, DatabaseProvider)} rather than deleting the + * directory and its contents directly. This is necessary to ensure that associated index data is + * also removed. */ public final class SimpleCache implements Cache { + private static final String TAG = "SimpleCache"; + /** + * Cache files are distributed between a number of subdirectories. This helps to avoid poor + * performance in cases where the performance of the underlying file system (e.g. FAT32) scales + * badly with the number of files per directory. See + * https://github.com/google/ExoPlayer/issues/4253. + */ + private static final int SUBDIRECTORY_COUNT = 10; + + private static final String UID_FILE_SUFFIX = ".uid"; + + private static final HashSet lockedCacheDirs = new HashSet<>(); + private final File cacheDir; private final CacheEvictor evictor; - private final HashMap lockedSpans; - private final CachedContentIndex index; + private final CachedContentIndex contentIndex; + @Nullable private final CacheFileMetadataIndex fileIndex; private final HashMap> listeners; - private long totalSpace = 0; - private CacheException initializationException; + private final Random random; + private final boolean touchCacheSpans; + + private long uid; + private long totalSpace; + private boolean released; + private @MonotonicNonNull CacheException initializationException; /** - * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence - * the directory cannot be used to store other files. - * - * @param cacheDir A dedicated cache directory. - * @param evictor The evictor to be used. + * Returns whether {@code cacheFolder} is locked by a {@link SimpleCache} instance. To unlock the + * folder the {@link SimpleCache} instance should be released. */ - public SimpleCache(File cacheDir, CacheEvictor evictor) { - this(cacheDir, evictor, null); + public static synchronized boolean isCacheFolderLocked(File cacheFolder) { + return lockedCacheDirs.contains(cacheFolder.getAbsoluteFile()); + } + + /** + * Deletes all content belonging to a cache instance. + * + *

This method may be slow and shouldn't normally be called on the main thread. + * + * @param cacheDir The cache directory. + * @param databaseProvider The database in which index data is stored, or {@code null} if the + * cache used a legacy index. + */ + @WorkerThread + public static void delete(File cacheDir, @Nullable DatabaseProvider databaseProvider) { + if (!cacheDir.exists()) { + return; + } + + File[] files = cacheDir.listFiles(); + if (files == null) { + cacheDir.delete(); + return; + } + + if (databaseProvider != null) { + // Make a best effort to read the cache UID and delete associated index data before deleting + // cache directory itself. + long uid = loadUid(files); + if (uid != UID_UNSET) { + try { + CacheFileMetadataIndex.delete(databaseProvider, uid); + } catch (DatabaseIOException e) { + Log.w(TAG, "Failed to delete file metadata: " + uid); + } + try { + CachedContentIndex.delete(databaseProvider, uid); + } catch (DatabaseIOException e) { + Log.w(TAG, "Failed to delete file metadata: " + uid); + } + } + } + + Util.recursiveDelete(cacheDir); } /** @@ -56,16 +130,134 @@ public final class SimpleCache implements Cache { * the directory cannot be used to store other files. * * @param cacheDir A dedicated cache directory. - * @param evictor The evictor to be used. + * @param evictor The evictor to be used. For download use cases where cache eviction should not + * occur, use {@link NoOpCacheEvictor}. + * @deprecated Use a constructor that takes a {@link DatabaseProvider} for improved performance. + */ + @Deprecated + public SimpleCache(File cacheDir, CacheEvictor evictor) { + this(cacheDir, evictor, null, false); + } + + /** + * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence + * the directory cannot be used to store other files. + * + * @param cacheDir A dedicated cache directory. + * @param evictor The evictor to be used. For download use cases where cache eviction should not + * occur, use {@link NoOpCacheEvictor}. * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC. * The key must be 16 bytes long. + * @deprecated Use a constructor that takes a {@link DatabaseProvider} for improved performance. */ - public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey) { + @Deprecated + @SuppressWarnings("deprecation") + public SimpleCache(File cacheDir, CacheEvictor evictor, @Nullable byte[] secretKey) { + this(cacheDir, evictor, secretKey, secretKey != null); + } + + /** + * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence + * the directory cannot be used to store other files. + * + * @param cacheDir A dedicated cache directory. + * @param evictor The evictor to be used. For download use cases where cache eviction should not + * occur, use {@link NoOpCacheEvictor}. + * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC. + * The key must be 16 bytes long. + * @param encrypt Whether the index will be encrypted when written. Must be false if {@code + * secretKey} is null. + * @deprecated Use a constructor that takes a {@link DatabaseProvider} for improved performance. + */ + @Deprecated + public SimpleCache( + File cacheDir, CacheEvictor evictor, @Nullable byte[] secretKey, boolean encrypt) { + this( + cacheDir, + evictor, + /* databaseProvider= */ null, + secretKey, + encrypt, + /* preferLegacyIndex= */ true); + } + + /** + * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence + * the directory cannot be used to store other files. + * + * @param cacheDir A dedicated cache directory. + * @param evictor The evictor to be used. For download use cases where cache eviction should not + * occur, use {@link NoOpCacheEvictor}. + * @param databaseProvider Provides the database in which the cache index is stored. + */ + public SimpleCache(File cacheDir, CacheEvictor evictor, DatabaseProvider databaseProvider) { + this( + cacheDir, + evictor, + databaseProvider, + /* legacyIndexSecretKey= */ null, + /* legacyIndexEncrypt= */ false, + /* preferLegacyIndex= */ false); + } + + /** + * Constructs the cache. The cache will delete any unrecognized files from the cache directory. + * Hence the directory cannot be used to store other files. + * + * @param cacheDir A dedicated cache directory. + * @param evictor The evictor to be used. For download use cases where cache eviction should not + * occur, use {@link NoOpCacheEvictor}. + * @param databaseProvider Provides the database in which the cache index is stored, or {@code + * null} to use a legacy index. Using a database index is highly recommended for performance + * reasons. + * @param legacyIndexSecretKey A 16 byte AES key for reading, and optionally writing, the legacy + * index. Not used by the database index, however should still be provided when using the + * database index in cases where upgrading from the legacy index may be necessary. + * @param legacyIndexEncrypt Whether to encrypt when writing to the legacy index. Must be {@code + * false} if {@code legacyIndexSecretKey} is {@code null}. Not used by the database index. + * @param preferLegacyIndex Whether to use the legacy index even if a {@code databaseProvider} is + * provided. Should be {@code false} in nearly all cases. Setting this to {@code true} is only + * useful for downgrading from the database index back to the legacy index. + */ + public SimpleCache( + File cacheDir, + CacheEvictor evictor, + @Nullable DatabaseProvider databaseProvider, + @Nullable byte[] legacyIndexSecretKey, + boolean legacyIndexEncrypt, + boolean preferLegacyIndex) { + this( + cacheDir, + evictor, + new CachedContentIndex( + databaseProvider, + cacheDir, + legacyIndexSecretKey, + legacyIndexEncrypt, + preferLegacyIndex), + databaseProvider != null && !preferLegacyIndex + ? new CacheFileMetadataIndex(databaseProvider) + : null); + } + + /* package */ SimpleCache( + File cacheDir, + CacheEvictor evictor, + CachedContentIndex contentIndex, + @Nullable CacheFileMetadataIndex fileIndex) { + if (!lockFolder(cacheDir)) { + throw new IllegalStateException("Another SimpleCache instance uses the folder: " + cacheDir); + } + this.cacheDir = cacheDir; this.evictor = evictor; - this.lockedSpans = new HashMap<>(); - this.index = new CachedContentIndex(cacheDir, secretKey); - this.listeners = new HashMap<>(); + this.contentIndex = contentIndex; + this.fileIndex = fileIndex; + listeners = new HashMap<>(); + random = new Random(); + touchCacheSpans = evictor.requiresCacheSpanTouches(); + uid = UID_UNSET; + // Start cache initialization. final ConditionVariable conditionVariable = new ConditionVariable(); new Thread("SimpleCache.initialize()") { @@ -73,11 +265,7 @@ public final class SimpleCache implements Cache { public void run() { synchronized (SimpleCache.this) { conditionVariable.open(); - try { - initialize(); - } catch (CacheException e) { - initializationException = e; - } + initialize(); SimpleCache.this.evictor.onCacheInitialized(); } } @@ -85,8 +273,42 @@ public final class SimpleCache implements Cache { conditionVariable.block(); } + /** + * Checks whether the cache was initialized successfully. + * + * @throws CacheException If an error occurred during initialization. + */ + public synchronized void checkInitialization() throws CacheException { + if (initializationException != null) { + throw initializationException; + } + } + + @Override + public synchronized long getUid() { + return uid; + } + + @Override + public synchronized void release() { + if (released) { + return; + } + listeners.clear(); + removeStaleSpans(); + try { + contentIndex.store(); + } catch (IOException e) { + Log.e(TAG, "Storing index file failed", e); + } finally { + unlockFolder(cacheDir); + released = true; + } + } + @Override public synchronized NavigableSet addListener(String key, Listener listener) { + Assertions.checkState(!released); ArrayList listenersForKey = listeners.get(key); if (listenersForKey == null) { listenersForKey = new ArrayList<>(); @@ -98,6 +320,9 @@ public final class SimpleCache implements Cache { @Override public synchronized void removeListener(String key, Listener listener) { + if (released) { + return; + } ArrayList listenersForKey = listeners.get(key); if (listenersForKey != null) { listenersForKey.remove(listener); @@ -107,218 +332,403 @@ public final class SimpleCache implements Cache { } } + @NonNull @Override public synchronized NavigableSet getCachedSpans(String key) { - CachedContent cachedContent = index.get(key); - return cachedContent == null ? null : new TreeSet(cachedContent.getSpans()); + Assertions.checkState(!released); + CachedContent cachedContent = contentIndex.get(key); + return cachedContent == null || cachedContent.isEmpty() + ? new TreeSet<>() + : new TreeSet(cachedContent.getSpans()); } @Override public synchronized Set getKeys() { - return new HashSet<>(index.getKeys()); + Assertions.checkState(!released); + return new HashSet<>(contentIndex.getKeys()); } @Override public synchronized long getCacheSpace() { + Assertions.checkState(!released); return totalSpace; } @Override - public synchronized SimpleCacheSpan startReadWrite(String key, long position) + public synchronized CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException { + Assertions.checkState(!released); + checkInitialization(); + while (true) { - SimpleCacheSpan span = startReadWriteNonBlocking(key, position); + CacheSpan span = startReadWriteNonBlocking(key, position); if (span != null) { return span; } else { - // Write case, lock not available. We'll be woken up when a locked span is released (if the - // released lock is for the requested key then we'll be able to make progress) or when a - // span is added to the cache (if the span is for the requested key and covers the requested - // position, then we'll become a read and be able to make progress). + // Lock not available. We'll be woken up when a span is added, or when a locked span is + // released. We'll be able to make progress when either: + // 1. A span is added for the requested key that covers the requested position, in which + // case a read can be started. + // 2. The lock for the requested key is released, in which case a write can be started. wait(); } } } @Override - public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position) + @Nullable + public synchronized CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException { - if (initializationException != null) { - throw initializationException; + Assertions.checkState(!released); + checkInitialization(); + + SimpleCacheSpan span = getSpan(key, position); + + if (span.isCached) { + // Read case. + return touchSpan(key, span); } - SimpleCacheSpan cacheSpan = getSpan(key, position); - - // Read case. - if (cacheSpan.isCached) { - // Obtain a new span with updated last access timestamp. - SimpleCacheSpan newCacheSpan = index.get(key).touch(cacheSpan); - notifySpanTouched(cacheSpan, newCacheSpan); - return newCacheSpan; + CachedContent cachedContent = contentIndex.getOrAdd(key); + if (!cachedContent.isLocked()) { + // Write case. + cachedContent.setLocked(true); + return span; } - // Write case, lock available. - if (!lockedSpans.containsKey(key)) { - lockedSpans.put(key, cacheSpan); - return cacheSpan; - } - - // Write case, lock not available. + // Lock not available. return null; } @Override - public synchronized File startFile(String key, long position, long maxLength) - throws CacheException { - Assertions.checkState(lockedSpans.containsKey(key)); + public synchronized File startFile(String key, long position, long length) throws CacheException { + Assertions.checkState(!released); + checkInitialization(); + + CachedContent cachedContent = contentIndex.get(key); + Assertions.checkNotNull(cachedContent); + Assertions.checkState(cachedContent.isLocked()); if (!cacheDir.exists()) { // For some reason the cache directory doesn't exist. Make a best effort to create it. - removeStaleSpansAndCachedContents(); cacheDir.mkdirs(); + removeStaleSpans(); } - evictor.onStartFile(this, key, position, maxLength); - return SimpleCacheSpan.getCacheFile(cacheDir, index.assignIdForKey(key), position, - System.currentTimeMillis()); + evictor.onStartFile(this, key, position, length); + // Randomly distribute files into subdirectories with a uniform distribution. + File fileDir = new File(cacheDir, Integer.toString(random.nextInt(SUBDIRECTORY_COUNT))); + if (!fileDir.exists()) { + fileDir.mkdir(); + } + long lastTouchTimestamp = System.currentTimeMillis(); + return SimpleCacheSpan.getCacheFile(fileDir, cachedContent.id, position, lastTouchTimestamp); } @Override - public synchronized void commitFile(File file) throws CacheException { - SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(file, index); - Assertions.checkState(span != null); - Assertions.checkState(lockedSpans.containsKey(span.key)); - // If the file doesn't exist, don't add it to the in-memory representation. + public synchronized void commitFile(File file, long length) throws CacheException { + Assertions.checkState(!released); if (!file.exists()) { return; } - // If the file has length 0, delete it and don't add it to the in-memory representation. - if (file.length() == 0) { + if (length == 0) { file.delete(); return; } + + SimpleCacheSpan span = + Assertions.checkNotNull(SimpleCacheSpan.createCacheEntry(file, length, contentIndex)); + CachedContent cachedContent = Assertions.checkNotNull(contentIndex.get(span.key)); + Assertions.checkState(cachedContent.isLocked()); + // Check if the span conflicts with the set content length - Long length = getContentLength(span.key); - if (length != C.LENGTH_UNSET) { - Assertions.checkState((span.position + span.length) <= length); + long contentLength = ContentMetadata.getContentLength(cachedContent.getMetadata()); + if (contentLength != C.LENGTH_UNSET) { + Assertions.checkState((span.position + span.length) <= contentLength); + } + + if (fileIndex != null) { + String fileName = file.getName(); + try { + fileIndex.set(fileName, span.length, span.lastTouchTimestamp); + } catch (IOException e) { + throw new CacheException(e); + } } addSpan(span); - index.store(); + try { + contentIndex.store(); + } catch (IOException e) { + throw new CacheException(e); + } notifyAll(); } @Override public synchronized void releaseHoleSpan(CacheSpan holeSpan) { - Assertions.checkState(holeSpan == lockedSpans.remove(holeSpan.key)); + Assertions.checkState(!released); + CachedContent cachedContent = contentIndex.get(holeSpan.key); + Assertions.checkNotNull(cachedContent); + Assertions.checkState(cachedContent.isLocked()); + cachedContent.setLocked(false); + contentIndex.maybeRemove(cachedContent.key); notifyAll(); } + @Override + public synchronized void removeSpan(CacheSpan span) { + Assertions.checkState(!released); + removeSpanInternal(span); + } + + @Override + public synchronized boolean isCached(String key, long position, long length) { + Assertions.checkState(!released); + CachedContent cachedContent = contentIndex.get(key); + return cachedContent != null && cachedContent.getCachedBytesLength(position, length) >= length; + } + + @Override + public synchronized long getCachedLength(String key, long position, long length) { + Assertions.checkState(!released); + CachedContent cachedContent = contentIndex.get(key); + return cachedContent != null ? cachedContent.getCachedBytesLength(position, length) : -length; + } + + @Override + public synchronized void applyContentMetadataMutations( + String key, ContentMetadataMutations mutations) throws CacheException { + Assertions.checkState(!released); + checkInitialization(); + + contentIndex.applyContentMetadataMutations(key, mutations); + try { + contentIndex.store(); + } catch (IOException e) { + throw new CacheException(e); + } + } + + @Override + public synchronized ContentMetadata getContentMetadata(String key) { + Assertions.checkState(!released); + return contentIndex.getContentMetadata(key); + } + + /** Ensures that the cache's in-memory representation has been initialized. */ + private void initialize() { + if (!cacheDir.exists()) { + if (!cacheDir.mkdirs()) { + String message = "Failed to create cache directory: " + cacheDir; + Log.e(TAG, message); + initializationException = new CacheException(message); + return; + } + } + + File[] files = cacheDir.listFiles(); + if (files == null) { + String message = "Failed to list cache directory files: " + cacheDir; + Log.e(TAG, message); + initializationException = new CacheException(message); + return; + } + + uid = loadUid(files); + if (uid == UID_UNSET) { + try { + uid = createUid(cacheDir); + } catch (IOException e) { + String message = "Failed to create cache UID: " + cacheDir; + Log.e(TAG, message, e); + initializationException = new CacheException(message, e); + return; + } + } + + try { + contentIndex.initialize(uid); + if (fileIndex != null) { + fileIndex.initialize(uid); + Map fileMetadata = fileIndex.getAll(); + loadDirectory(cacheDir, /* isRoot= */ true, files, fileMetadata); + fileIndex.removeAll(fileMetadata.keySet()); + } else { + loadDirectory(cacheDir, /* isRoot= */ true, files, /* fileMetadata= */ null); + } + } catch (IOException e) { + String message = "Failed to initialize cache indices: " + cacheDir; + Log.e(TAG, message, e); + initializationException = new CacheException(message, e); + return; + } + + contentIndex.removeEmpty(); + try { + contentIndex.store(); + } catch (IOException e) { + Log.e(TAG, "Storing index file failed", e); + } + } + /** - * Returns the cache {@link SimpleCacheSpan} corresponding to the provided lookup {@link - * SimpleCacheSpan}. + * Loads a cache directory. If the root directory is passed, also loads any subdirectories. + * + * @param directory The directory. + * @param isRoot Whether the directory is the root directory. + * @param files The files belonging to the directory. + * @param fileMetadata A mutable map containing cache file metadata, keyed by file name. The map + * is modified by removing entries for all loaded files. When the method call returns, the map + * will contain only metadata that was unused. May be null if no file metadata is available. + */ + private void loadDirectory( + File directory, + boolean isRoot, + @Nullable File[] files, + @Nullable Map fileMetadata) { + if (files == null || files.length == 0) { + // Either (a) directory isn't really a directory (b) it's empty, or (c) listing files failed. + if (!isRoot) { + // For (a) and (b) deletion is the desired result. For (c) it will be a no-op if the + // directory is non-empty, so there's no harm in trying. + directory.delete(); + } + return; + } + for (File file : files) { + String fileName = file.getName(); + if (isRoot && fileName.indexOf('.') == -1) { + loadDirectory(file, /* isRoot= */ false, file.listFiles(), fileMetadata); + } else { + if (isRoot + && (CachedContentIndex.isIndexFile(fileName) || fileName.endsWith(UID_FILE_SUFFIX))) { + // Skip expected UID and index files in the root directory. + continue; + } + long length = C.LENGTH_UNSET; + long lastTouchTimestamp = C.TIME_UNSET; + CacheFileMetadata metadata = fileMetadata != null ? fileMetadata.remove(fileName) : null; + if (metadata != null) { + length = metadata.length; + lastTouchTimestamp = metadata.lastTouchTimestamp; + } + SimpleCacheSpan span = + SimpleCacheSpan.createCacheEntry(file, length, lastTouchTimestamp, contentIndex); + if (span != null) { + addSpan(span); + } else { + file.delete(); + } + } + } + } + + /** + * Touches a cache span, returning the updated result. If the evictor does not require cache spans + * to be touched, then this method does nothing and the span is returned without modification. + * + * @param key The key of the span being touched. + * @param span The span being touched. + * @return The updated span. + */ + private SimpleCacheSpan touchSpan(String key, SimpleCacheSpan span) { + if (!touchCacheSpans) { + return span; + } + String fileName = Assertions.checkNotNull(span.file).getName(); + long length = span.length; + long lastTouchTimestamp = System.currentTimeMillis(); + boolean updateFile = false; + if (fileIndex != null) { + try { + fileIndex.set(fileName, length, lastTouchTimestamp); + } catch (IOException e) { + Log.w(TAG, "Failed to update index with new touch timestamp."); + } + } else { + // Updating the file itself to incorporate the new last touch timestamp is much slower than + // updating the file index. Hence we only update the file if we don't have a file index. + updateFile = true; + } + SimpleCacheSpan newSpan = + contentIndex.get(key).setLastTouchTimestamp(span, lastTouchTimestamp, updateFile); + notifySpanTouched(span, newSpan); + return newSpan; + } + + /** + * Returns the cache span corresponding to the provided lookup span. * *

If the lookup position is contained by an existing entry in the cache, then the returned - * {@link SimpleCacheSpan} defines the file in which the data is stored. If the lookup position is - * not contained by an existing entry, then the returned {@link SimpleCacheSpan} defines the - * maximum extents of the hole in the cache. + * span defines the file in which the data is stored. If the lookup position is not contained by + * an existing entry, then the returned span defines the maximum extents of the hole in the cache. * * @param key The key of the span being requested. * @param position The position of the span being requested. * @return The corresponding cache {@link SimpleCacheSpan}. */ - private SimpleCacheSpan getSpan(String key, long position) throws CacheException { - CachedContent cachedContent = index.get(key); + private SimpleCacheSpan getSpan(String key, long position) { + CachedContent cachedContent = contentIndex.get(key); if (cachedContent == null) { return SimpleCacheSpan.createOpenHole(key, position); } while (true) { SimpleCacheSpan span = cachedContent.getSpan(position); - if (span.isCached && !span.file.exists()) { - // The file has been deleted from under us. It's likely that other files will have been - // deleted too, so scan the whole in-memory representation. - removeStaleSpansAndCachedContents(); + if (span.isCached && span.file.length() != span.length) { + // The file has been modified or deleted underneath us. It's likely that other files will + // have been modified too, so scan the whole in-memory representation. + removeStaleSpans(); continue; } return span; } } - /** - * Ensures that the cache's in-memory representation has been initialized. - */ - private void initialize() throws CacheException { - if (!cacheDir.exists()) { - cacheDir.mkdirs(); - return; - } - - index.load(); - - File[] files = cacheDir.listFiles(); - if (files == null) { - return; - } - for (File file : files) { - if (file.getName().equals(CachedContentIndex.FILE_NAME)) { - continue; - } - SimpleCacheSpan span = file.length() > 0 - ? SimpleCacheSpan.createCacheEntry(file, index) : null; - if (span != null) { - addSpan(span); - } else { - file.delete(); - } - } - - index.removeEmpty(); - index.store(); - } - /** * Adds a cached span to the in-memory representation. * * @param span The span to be added. */ private void addSpan(SimpleCacheSpan span) { - index.add(span.key).addSpan(span); + contentIndex.getOrAdd(span.key).addSpan(span); totalSpace += span.length; notifySpanAdded(span); } - private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException { - CachedContent cachedContent = index.get(span.key); - Assertions.checkState(cachedContent.removeSpan(span)); - totalSpace -= span.length; - if (removeEmptyCachedContent && cachedContent.isEmpty()) { - index.removeEmpty(cachedContent.key); - index.store(); + private void removeSpanInternal(CacheSpan span) { + CachedContent cachedContent = contentIndex.get(span.key); + if (cachedContent == null || !cachedContent.removeSpan(span)) { + return; } + totalSpace -= span.length; + if (fileIndex != null) { + String fileName = span.file.getName(); + try { + fileIndex.remove(fileName); + } catch (IOException e) { + // This will leave a stale entry in the file index. It will be removed next time the cache + // is initialized. + Log.w(TAG, "Failed to remove file index entry for: " + fileName); + } + } + contentIndex.maybeRemove(cachedContent.key); notifySpanRemoved(span); } - @Override - public synchronized void removeSpan(CacheSpan span) throws CacheException { - removeSpan(span, true); - } - /** - * Scans all of the cached spans in the in-memory representation, removing any for which files - * no longer exist. + * Scans all of the cached spans in the in-memory representation, removing any for which the + * underlying file lengths no longer match. */ - private void removeStaleSpansAndCachedContents() throws CacheException { - LinkedList spansToBeRemoved = new LinkedList<>(); - for (CachedContent cachedContent : index.getAll()) { + private void removeStaleSpans() { + ArrayList spansToBeRemoved = new ArrayList<>(); + for (CachedContent cachedContent : contentIndex.getAll()) { for (CacheSpan span : cachedContent.getSpans()) { - if (!span.file.exists()) { + if (span.file.length() != span.length) { spansToBeRemoved.add(span); } } } - for (CacheSpan span : spansToBeRemoved) { - // Remove span but not CachedContent to prevent multiple index.store() calls. - removeSpan(span, false); + for (int i = 0; i < spansToBeRemoved.size(); i++) { + removeSpanInternal(spansToBeRemoved.get(i)); } - index.removeEmpty(); - index.store(); } private void notifySpanRemoved(CacheSpan span) { @@ -351,27 +761,52 @@ public final class SimpleCache implements Cache { evictor.onSpanTouched(this, oldSpan, newSpan); } - @Override - public synchronized boolean isCached(String key, long position, long length) { - CachedContent cachedContent = index.get(key); - return cachedContent != null && cachedContent.getCachedBytes(position, length) >= length; + /** + * Loads the cache UID from the files belonging to the root directory. + * + * @param files The files belonging to the root directory. + * @return The loaded UID, or {@link #UID_UNSET} if a UID has not yet been created. + */ + private static long loadUid(File[] files) { + for (File file : files) { + String fileName = file.getName(); + if (fileName.endsWith(UID_FILE_SUFFIX)) { + try { + return parseUid(fileName); + } catch (NumberFormatException e) { + // This should never happen, but if it does delete the malformed UID file and continue. + Log.e(TAG, "Malformed UID file: " + file); + file.delete(); + } + } + } + return UID_UNSET; } - @Override - public synchronized long getCachedBytes(String key, long position, long length) { - CachedContent cachedContent = index.get(key); - return cachedContent != null ? cachedContent.getCachedBytes(position, length) : -length; + @SuppressWarnings("TrulyRandom") + private static long createUid(File directory) throws IOException { + // Generate a non-negative UID. + long uid = new SecureRandom().nextLong(); + uid = uid == Long.MIN_VALUE ? 0 : Math.abs(uid); + // Persist it as a file. + String hexUid = Long.toString(uid, /* radix= */ 16); + File hexUidFile = new File(directory, hexUid + UID_FILE_SUFFIX); + if (!hexUidFile.createNewFile()) { + // False means that the file already exists, so this should never happen. + throw new IOException("Failed to create UID file: " + hexUidFile); + } + return uid; } - @Override - public synchronized void setContentLength(String key, long length) throws CacheException { - index.setContentLength(key, length); - index.store(); + private static long parseUid(String fileName) { + return Long.parseLong(fileName.substring(0, fileName.indexOf('.')), /* radix= */ 16); } - @Override - public synchronized long getContentLength(String key) { - return index.getContentLength(key); + private static synchronized boolean lockFolder(File cacheDir) { + return lockedCacheDirs.add(cacheDir.getAbsoluteFile()); } + private static synchronized void unlockFolder(File cacheDir) { + lockedCacheDirs.remove(cacheDir.getAbsoluteFile()); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java index d8d28060191b..6e7bec301f52 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java @@ -15,6 +15,7 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; @@ -22,12 +23,12 @@ import java.io.File; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** - * This class stores span metadata in filename. - */ -/*package*/ final class SimpleCacheSpan extends CacheSpan { +/** This class stores span metadata in filename. */ +/* package */ final class SimpleCacheSpan extends CacheSpan { - private static final String SUFFIX = ".v3.exo"; + /* package */ static final String COMMON_SUFFIX = ".exo"; + + private static final String SUFFIX = ".v3" + COMMON_SUFFIX; private static final Pattern CACHE_FILE_PATTERN_V1 = Pattern.compile( "^(.+)\\.(\\d+)\\.(\\d+)\\.v1\\.exo$", Pattern.DOTALL); private static final Pattern CACHE_FILE_PATTERN_V2 = Pattern.compile( @@ -35,19 +36,50 @@ import java.util.regex.Pattern; private static final Pattern CACHE_FILE_PATTERN_V3 = Pattern.compile( "^(\\d+)\\.(\\d+)\\.(\\d+)\\.v3\\.exo$", Pattern.DOTALL); - public static File getCacheFile(File cacheDir, int id, long position, - long lastAccessTimestamp) { - return new File(cacheDir, id + "." + position + "." + lastAccessTimestamp + SUFFIX); + /** + * Returns a new {@link File} instance from {@code cacheDir}, {@code id}, {@code position}, {@code + * timestamp}. + * + * @param cacheDir The parent abstract pathname. + * @param id The cache file id. + * @param position The position of the stored data in the original stream. + * @param timestamp The file timestamp. + * @return The cache file. + */ + public static File getCacheFile(File cacheDir, int id, long position, long timestamp) { + return new File(cacheDir, id + "." + position + "." + timestamp + SUFFIX); } + /** + * Creates a lookup span. + * + * @param key The cache key. + * @param position The position of the {@link CacheSpan} in the original stream. + * @return The span. + */ public static SimpleCacheSpan createLookup(String key, long position) { return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null); } + /** + * Creates an open hole span. + * + * @param key The cache key. + * @param position The position of the {@link CacheSpan} in the original stream. + * @return The span. + */ public static SimpleCacheSpan createOpenHole(String key, long position) { return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null); } + /** + * Creates a closed hole span. + * + * @param key The cache key. + * @param position The position of the {@link CacheSpan} in the original stream. + * @param length The length of the {@link CacheSpan}. + * @return The span. + */ public static SimpleCacheSpan createClosedHole(String key, long position, long length) { return new SimpleCacheSpan(key, position, length, C.TIME_UNSET, null); } @@ -56,17 +88,39 @@ import java.util.regex.Pattern; * Creates a cache span from an underlying cache file. Upgrades the file if necessary. * * @param file The cache file. - * @param index Cached content index. + * @param length The length of the cache file in bytes, or {@link C#LENGTH_UNSET} to query the + * underlying file system. Querying the underlying file system can be expensive, so callers + * that already know the length of the file should pass it explicitly. * @return The span, or null if the file name is not correctly formatted, or if the id is not - * present in the content index. + * present in the content index, or if the length is 0. */ - public static SimpleCacheSpan createCacheEntry(File file, CachedContentIndex index) { + @Nullable + public static SimpleCacheSpan createCacheEntry(File file, long length, CachedContentIndex index) { + return createCacheEntry(file, length, /* lastTouchTimestamp= */ C.TIME_UNSET, index); + } + + /** + * Creates a cache span from an underlying cache file. Upgrades the file if necessary. + * + * @param file The cache file. + * @param length The length of the cache file in bytes, or {@link C#LENGTH_UNSET} to query the + * underlying file system. Querying the underlying file system can be expensive, so callers + * that already know the length of the file should pass it explicitly. + * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} to use the file + * timestamp. + * @return The span, or null if the file name is not correctly formatted, or if the id is not + * present in the content index, or if the length is 0. + */ + @Nullable + public static SimpleCacheSpan createCacheEntry( + File file, long length, long lastTouchTimestamp, CachedContentIndex index) { String name = file.getName(); if (!name.endsWith(SUFFIX)) { - file = upgradeFile(file, index); - if (file == null) { + @Nullable File upgradedFile = upgradeFile(file, index); + if (upgradedFile == null) { return null; } + file = upgradedFile; name = file.getName(); } @@ -74,13 +128,36 @@ import java.util.regex.Pattern; if (!matcher.matches()) { return null; } - long length = file.length(); + int id = Integer.parseInt(matcher.group(1)); String key = index.getKeyForId(id); - return key == null ? null : new SimpleCacheSpan(key, Long.parseLong(matcher.group(2)), length, - Long.parseLong(matcher.group(3)), file); + if (key == null) { + return null; + } + + if (length == C.LENGTH_UNSET) { + length = file.length(); + } + if (length == 0) { + return null; + } + + long position = Long.parseLong(matcher.group(2)); + if (lastTouchTimestamp == C.TIME_UNSET) { + lastTouchTimestamp = Long.parseLong(matcher.group(3)); + } + return new SimpleCacheSpan(key, position, length, lastTouchTimestamp, file); } + /** + * Upgrades the cache file if it is created by an earlier version of {@link SimpleCache}. + * + * @param file The cache file. + * @param index Cached content index. + * @return Upgraded cache file or {@code null} if the file name is not correctly formatted or the + * file can not be renamed. + */ + @Nullable private static File upgradeFile(File file, CachedContentIndex index) { String key; String filename = file.getName(); @@ -98,32 +175,43 @@ import java.util.regex.Pattern; key = matcher.group(1); // Keys were not escaped in version 1. } - File newCacheFile = getCacheFile(file.getParentFile(), index.assignIdForKey(key), - Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3))); + File newCacheFile = + getCacheFile( + Assertions.checkStateNotNull(file.getParentFile()), + index.assignIdForKey(key), + Long.parseLong(matcher.group(2)), + Long.parseLong(matcher.group(3))); if (!file.renameTo(newCacheFile)) { return null; } return newCacheFile; } - private SimpleCacheSpan(String key, long position, long length, long lastAccessTimestamp, - File file) { - super(key, position, length, lastAccessTimestamp, file); + /** + * @param key The cache key. + * @param position The position of the {@link CacheSpan} in the original stream. + * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an + * open-ended hole. + * @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} if {@link + * #isCached} is false. + * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole. + */ + private SimpleCacheSpan( + String key, long position, long length, long lastTouchTimestamp, @Nullable File file) { + super(key, position, length, lastTouchTimestamp, file); } /** - * Returns a copy of this CacheSpan whose last access time stamp is set to current time. This - * doesn't copy or change the underlying cache file. + * Returns a copy of this CacheSpan with a new file and last touch timestamp. * - * @param id The cache file id. - * @return A {@link SimpleCacheSpan} with updated last access time stamp. + * @param file The new file. + * @param lastTouchTimestamp The new last touch time. + * @return A copy with the new file and last touch timestamp. * @throws IllegalStateException If called on a non-cached span (i.e. {@link #isCached} is false). */ - public SimpleCacheSpan copyWithUpdatedLastAccessTime(int id) { + public SimpleCacheSpan copyWithFileAndLastTouchTimestamp(File file, long lastTouchTimestamp) { Assertions.checkState(isCached); - long now = System.currentTimeMillis(); - File newCacheFile = getCacheFile(file.getParentFile(), id, position, now); - return new SimpleCacheSpan(key, position, length, now, newCacheFile); + return new SimpleCacheSpan(key, position, length, lastTouchTimestamp, file); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java index a1df85bc6f9c..4c6be9815718 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java @@ -15,6 +15,9 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.crypto; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSink; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; import java.io.IOException; @@ -27,9 +30,9 @@ public final class AesCipherDataSink implements DataSink { private final DataSink wrappedDataSink; private final byte[] secretKey; - private final byte[] scratch; + @Nullable private final byte[] scratch; - private AesFlushingCipher cipher; + @Nullable private AesFlushingCipher cipher; /** * Create an instance whose {@code write} methods have the side effect of overwriting the input @@ -49,12 +52,13 @@ public final class AesCipherDataSink implements DataSink { * * @param secretKey The key data. * @param wrappedDataSink The wrapped {@link DataSink}. - * @param scratch Scratch space. Data is decrypted into this array before being written to the + * @param scratch Scratch space. Data is encrypted into this array before being written to the * wrapped {@link DataSink}. It should be of appropriate size for the expected writes. If a * write is larger than the size of this array the write will still succeed, but multiple - * cipher calls will be required to complete the operation. + * cipher calls will be required to complete the operation. If {@code null} then encryption + * will overwrite the input {@code data}. */ - public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, byte[] scratch) { + public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, @Nullable byte[] scratch) { this.wrappedDataSink = wrappedDataSink; this.secretKey = secretKey; this.scratch = scratch; @@ -72,15 +76,16 @@ public final class AesCipherDataSink implements DataSink { public void write(byte[] data, int offset, int length) throws IOException { if (scratch == null) { // In-place mode. Writes over the input data. - cipher.updateInPlace(data, offset, length); + castNonNull(cipher).updateInPlace(data, offset, length); wrappedDataSink.write(data, offset, length); } else { // Use scratch space. The original data remains intact. int bytesProcessed = 0; while (bytesProcessed < length) { int bytesToProcess = Math.min(length - bytesProcessed, scratch.length); - cipher.update(data, offset + bytesProcessed, bytesToProcess, scratch, 0); - wrappedDataSink.write(scratch, 0, bytesToProcess); + castNonNull(cipher) + .update(data, offset + bytesProcessed, bytesToProcess, scratch, /* outOffset= */ 0); + wrappedDataSink.write(scratch, /* offset= */ 0, bytesToProcess); bytesProcessed += bytesToProcess; } } @@ -91,5 +96,4 @@ public final class AesCipherDataSink implements DataSink { cipher = null; wrappedDataSink.close(); } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java index 0dcd967bf700..0b0687b57ee7 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java @@ -15,11 +15,17 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.crypto; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.net.Uri; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.TransferListener; import java.io.IOException; +import java.util.List; +import java.util.Map; import javax.crypto.Cipher; /** @@ -30,13 +36,18 @@ public final class AesCipherDataSource implements DataSource { private final DataSource upstream; private final byte[] secretKey; - private AesFlushingCipher cipher; + @Nullable private AesFlushingCipher cipher; public AesCipherDataSource(byte[] secretKey, DataSource upstream) { this.upstream = upstream; this.secretKey = secretKey; } + @Override + public void addTransferListener(TransferListener transferListener) { + upstream.addTransferListener(transferListener); + } + @Override public long open(DataSpec dataSpec) throws IOException { long dataLength = upstream.open(dataSpec); @@ -55,19 +66,24 @@ public final class AesCipherDataSource implements DataSource { if (read == C.RESULT_END_OF_INPUT) { return C.RESULT_END_OF_INPUT; } - cipher.updateInPlace(data, offset, read); + castNonNull(cipher).updateInPlace(data, offset, read); return read; } + @Override + @Nullable + public Uri getUri() { + return upstream.getUri(); + } + + @Override + public Map> getResponseHeaders() { + return upstream.getResponseHeaders(); + } + @Override public void close() throws IOException { cipher = null; upstream.close(); } - - @Override - public Uri getUri() { - return upstream.getUri(); - } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java index 0db85d859cfb..985a6dcf24fc 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java @@ -16,6 +16,7 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.crypto; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -49,7 +50,9 @@ public final class AesFlushingCipher { flushedBlock = new byte[blockSize]; long counter = offset / blockSize; int startPadding = (int) (offset % blockSize); - cipher.init(mode, new SecretKeySpec(secretKey, cipher.getAlgorithm().split("/")[0]), + cipher.init( + mode, + new SecretKeySpec(secretKey, Util.splitAtFirst(cipher.getAlgorithm(), "/")[0]), new IvParameterSpec(getInitializationVector(nonce, counter))); if (startPadding != 0) { updateInPlace(new byte[startPadding], 0, startPadding); diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java index 347566900251..a4904b9285d4 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java @@ -15,6 +15,8 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.crypto; +import androidx.annotation.Nullable; + /** * Utility functions for the crypto package. */ @@ -24,10 +26,10 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.crypto; /** * Returns the hash value of the input as a long using the 64 bit FNV-1a hash function. The hash - * values produced by this function are less likely to collide than those produced by - * {@link #hashCode()}. + * values produced by this function are less likely to collide than those produced by {@link + * #hashCode()}. */ - public static long getFNV64Hash(String input) { + public static long getFNV64Hash(@Nullable String input) { if (input == null) { return 0; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Assertions.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Assertions.java index 35c416c2335b..361b8956959d 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Assertions.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Assertions.java @@ -17,7 +17,9 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.util; import android.os.Looper; import android.text.TextUtils; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; /** * Provides methods for asserting the truth of expressions and properties. @@ -94,6 +96,42 @@ public final class Assertions { } } + /** + * Throws {@link IllegalStateException} if {@code reference} is null. + * + * @param The type of the reference. + * @param reference The reference. + * @return The non-null reference that was validated. + * @throws IllegalStateException If {@code reference} is null. + */ + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) + @EnsuresNonNull({"#1"}) + public static T checkStateNotNull(@Nullable T reference) { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { + throw new IllegalStateException(); + } + return reference; + } + + /** + * Throws {@link IllegalStateException} if {@code reference} is null. + * + * @param The type of the reference. + * @param reference The reference. + * @param errorMessage The exception message to use if the check fails. The message is converted + * to a string using {@link String#valueOf(Object)}. + * @return The non-null reference that was validated. + * @throws IllegalStateException If {@code reference} is null. + */ + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) + @EnsuresNonNull({"#1"}) + public static T checkStateNotNull(@Nullable T reference, Object errorMessage) { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { + throw new IllegalStateException(String.valueOf(errorMessage)); + } + return reference; + } + /** * Throws {@link NullPointerException} if {@code reference} is null. * @@ -102,7 +140,9 @@ public final class Assertions { * @return The non-null reference that was validated. * @throws NullPointerException If {@code reference} is null. */ - public static T checkNotNull(T reference) { + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) + @EnsuresNonNull({"#1"}) + public static T checkNotNull(@Nullable T reference) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { throw new NullPointerException(); } @@ -119,7 +159,9 @@ public final class Assertions { * @return The non-null reference that was validated. * @throws NullPointerException If {@code reference} is null. */ - public static T checkNotNull(T reference, Object errorMessage) { + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) + @EnsuresNonNull({"#1"}) + public static T checkNotNull(@Nullable T reference, Object errorMessage) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { throw new NullPointerException(String.valueOf(errorMessage)); } @@ -133,7 +175,9 @@ public final class Assertions { * @return The non-null, non-empty string that was validated. * @throws IllegalArgumentException If {@code string} is null or 0-length. */ - public static String checkNotEmpty(String string) { + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) + @EnsuresNonNull({"#1"}) + public static String checkNotEmpty(@Nullable String string) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) { throw new IllegalArgumentException(); } @@ -149,7 +193,9 @@ public final class Assertions { * @return The non-null, non-empty string that was validated. * @throws IllegalArgumentException If {@code string} is null or 0-length. */ - public static String checkNotEmpty(String string, Object errorMessage) { + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) + @EnsuresNonNull({"#1"}) + public static String checkNotEmpty(@Nullable String string, Object errorMessage) { if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) { throw new IllegalArgumentException(String.valueOf(errorMessage)); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/AtomicFile.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/AtomicFile.java index ec526fd63890..d868a7d22a9a 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/AtomicFile.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/AtomicFile.java @@ -13,11 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.mozilla.thirdparty.com.google.android.exoplayer2.util; -import android.support.annotation.NonNull; -import android.util.Log; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -31,7 +28,7 @@ import java.io.OutputStream; * has successfully completed. * *

Atomic file guarantees file integrity by ensuring that a file has been completely written and - * sync'd to disk before removing its backup. As long as the backup file exists, the original file + * synced to disk before removing its backup. As long as the backup file exists, the original file * is considered to be invalid (left over from a previous attempt to write the file). * *

Atomic file does not confer any file locking semantics. Do not use this class when the file @@ -54,6 +51,11 @@ public final class AtomicFile { backupName = new File(baseName.getPath() + ".bak"); } + /** Returns whether the file or its backup exists. */ + public boolean exists() { + return baseName.exists() || backupName.exists(); + } + /** Delete the atomic file. This deletes both the base and backup files. */ public void delete() { baseName.delete(); @@ -104,13 +106,14 @@ public final class AtomicFile { str = new AtomicFileOutputStream(baseName); } catch (FileNotFoundException e) { File parent = baseName.getParentFile(); - if (!parent.mkdirs()) { - throw new IOException("Couldn't create directory " + baseName); + if (parent == null || !parent.mkdirs()) { + throw new IOException("Couldn't create " + baseName, e); } + // Try again now that we've created the parent directory. try { str = new AtomicFileOutputStream(baseName); } catch (FileNotFoundException e2) { - throw new IOException("Couldn't create " + baseName); + throw new IOException("Couldn't create " + baseName, e2); } } return str; @@ -186,12 +189,12 @@ public final class AtomicFile { } @Override - public void write(@NonNull byte[] b) throws IOException { + public void write(byte[] b) throws IOException { fileOutputStream.write(b); } @Override - public void write(@NonNull byte[] b, int off, int len) throws IOException { + public void write(byte[] b, int off, int len) throws IOException { fileOutputStream.write(b, off, len); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Clock.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Clock.java index 64aa0b5923f5..4247e1db7b72 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Clock.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Clock.java @@ -15,17 +15,35 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.util; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.Nullable; + /** - * An interface through which system clocks can be read. The {@link SystemClock} implementation - * must be used for all non-test cases. + * An interface through which system clocks can be read and {@link HandlerWrapper}s created. The + * {@link #DEFAULT} implementation must be used for all non-test cases. */ public interface Clock { /** - * Returns {@link android.os.SystemClock#elapsedRealtime}. - * - * @return Elapsed milliseconds since boot. + * Default {@link Clock} to use for all non-test cases. */ + Clock DEFAULT = new SystemClock(); + + /** @see android.os.SystemClock#elapsedRealtime() */ long elapsedRealtime(); + /** @see android.os.SystemClock#uptimeMillis() */ + long uptimeMillis(); + + /** @see android.os.SystemClock#sleep(long) */ + void sleep(long sleepTimeMs); + + /** + * Creates a {@link HandlerWrapper} using a specified looper and a specified callback for handling + * messages. + * + * @see Handler#Handler(Looper, Handler.Callback) + */ + HandlerWrapper createHandler(Looper looper, @Nullable Handler.Callback callback); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java index ae8bc216b114..9c821c47c89f 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java @@ -16,7 +16,9 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.util; import android.util.Pair; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import java.util.ArrayList; import java.util.List; @@ -81,13 +83,29 @@ public final class CodecSpecificDataUtil { private CodecSpecificDataUtil() {} /** - * Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 + * Parses an AAC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 * - * @param audioSpecificConfig The AudioSpecificConfig to parse. + * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse. * @return A pair consisting of the sample rate in Hz and the channel count. + * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported. */ - public static Pair parseAacAudioSpecificConfig(byte[] audioSpecificConfig) { - ParsableBitArray bitArray = new ParsableBitArray(audioSpecificConfig); + public static Pair parseAacAudioSpecificConfig(byte[] audioSpecificConfig) + throws ParserException { + return parseAacAudioSpecificConfig(new ParsableBitArray(audioSpecificConfig), false); + } + + /** + * Parses an AAC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 + * + * @param bitArray A {@link ParsableBitArray} containing the AudioSpecificConfig to parse. The + * position is advanced to the end of the AudioSpecificConfig. + * @param forceReadToEnd Whether the entire AudioSpecificConfig should be read. Required for + * knowing the length of the configuration payload. + * @return A pair consisting of the sample rate in Hz and the channel count. + * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported. + */ + public static Pair parseAacAudioSpecificConfig( + ParsableBitArray bitArray, boolean forceReadToEnd) throws ParserException { int audioObjectType = getAacAudioObjectType(bitArray); int sampleRate = getAacSamplingFrequency(bitArray); int channelConfiguration = bitArray.readBits(4); @@ -104,6 +122,41 @@ public final class CodecSpecificDataUtil { channelConfiguration = bitArray.readBits(4); } } + + if (forceReadToEnd) { + switch (audioObjectType) { + case 1: + case 2: + case 3: + case 4: + case 6: + case 7: + case 17: + case 19: + case 20: + case 21: + case 22: + case 23: + parseGaSpecificConfig(bitArray, audioObjectType, channelConfiguration); + break; + default: + throw new ParserException("Unsupported audio object type: " + audioObjectType); + } + switch (audioObjectType) { + case 17: + case 19: + case 20: + case 21: + case 22: + case 23: + int epConfig = bitArray.readBits(2); + if (epConfig == 2 || epConfig == 3) { + throw new ParserException("Unsupported epConfig: " + epConfig); + } + break; + } + } + // For supported containers, bits_to_decode() is always 0. int channelCount = AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[channelConfiguration]; Assertions.checkArgument(channelCount != AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID); return Pair.create(sampleRate, channelCount); @@ -113,10 +166,10 @@ public final class CodecSpecificDataUtil { * Builds a simple HE-AAC LC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 * * @param sampleRate The sample rate in Hz. - * @param numChannels The number of channels. + * @param channelCount The channel count. * @return The AudioSpecificConfig. */ - public static byte[] buildAacLcAudioSpecificConfig(int sampleRate, int numChannels) { + public static byte[] buildAacLcAudioSpecificConfig(int sampleRate, int channelCount) { int sampleRateIndex = C.INDEX_UNSET; for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE.length; ++i) { if (sampleRate == AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[i]) { @@ -125,13 +178,13 @@ public final class CodecSpecificDataUtil { } int channelConfig = C.INDEX_UNSET; for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE.length; ++i) { - if (numChannels == AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[i]) { + if (channelCount == AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[i]) { channelConfig = i; } } if (sampleRate == C.INDEX_UNSET || channelConfig == C.INDEX_UNSET) { - throw new IllegalArgumentException("Invalid sample rate or number of channels: " - + sampleRate + ", " + numChannels); + throw new IllegalArgumentException( + "Invalid sample rate or number of channels: " + sampleRate + ", " + channelCount); } return buildAacAudioSpecificConfig(AUDIO_OBJECT_TYPE_AAC_LC, sampleRateIndex, channelConfig); } @@ -152,6 +205,37 @@ public final class CodecSpecificDataUtil { return specificConfig; } + /** + * Parses an ALAC AudioSpecificConfig (i.e. an ALACSpecificConfig). + * + * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse. + * @return A pair consisting of the sample rate in Hz and the channel count. + */ + public static Pair parseAlacAudioSpecificConfig(byte[] audioSpecificConfig) { + ParsableByteArray byteArray = new ParsableByteArray(audioSpecificConfig); + byteArray.setPosition(9); + int channelCount = byteArray.readUnsignedByte(); + byteArray.setPosition(20); + int sampleRate = byteArray.readUnsignedIntToInt(); + return Pair.create(sampleRate, channelCount); + } + + /** + * Builds an RFC 6381 AVC codec string using the provided parameters. + * + * @param profileIdc The encoding profile. + * @param constraintsFlagsAndReservedZero2Bits The constraint flags followed by the reserved zero + * 2 bits, all contained in the least significant byte of the integer. + * @param levelIdc The encoding level. + * @return An RFC 6381 AVC codec string built using the provided parameters. + */ + public static String buildAvcCodecString( + int profileIdc, int constraintsFlagsAndReservedZero2Bits, int levelIdc) { + return String.format( + "avc1.%02X%02X%02X", profileIdc, constraintsFlagsAndReservedZero2Bits, levelIdc); + } + /** * Constructs a NAL unit consisting of the NAL start code followed by the specified data. * @@ -169,8 +253,8 @@ public final class CodecSpecificDataUtil { /** * Splits an array of NAL units. - *

- * If the input consists of NAL start code delimited units, then the returned array consists of + * + *

If the input consists of NAL start code delimited units, then the returned array consists of * the split NAL units, each of which is still prefixed with the NAL start code. For any other * input, null is returned. * @@ -178,7 +262,7 @@ public final class CodecSpecificDataUtil { * @return The individual NAL units, or null if the input did not consist of NAL start code * delimited units. */ - public static byte[][] splitNalUnits(byte[] data) { + public static @Nullable byte[][] splitNalUnits(byte[] data) { if (!isNalStartCode(data, 0)) { // data does not consist of NAL start code delimited units. return null; @@ -269,4 +353,32 @@ public final class CodecSpecificDataUtil { return samplingFrequency; } + private static void parseGaSpecificConfig(ParsableBitArray bitArray, int audioObjectType, + int channelConfiguration) { + bitArray.skipBits(1); // frameLengthFlag. + boolean dependsOnCoreDecoder = bitArray.readBit(); + if (dependsOnCoreDecoder) { + bitArray.skipBits(14); // coreCoderDelay. + } + boolean extensionFlag = bitArray.readBit(); + if (channelConfiguration == 0) { + throw new UnsupportedOperationException(); // TODO: Implement programConfigElement(); + } + if (audioObjectType == 6 || audioObjectType == 20) { + bitArray.skipBits(3); // layerNr. + } + if (extensionFlag) { + if (audioObjectType == 22) { + bitArray.skipBits(16); // numOfSubFrame (5), layer_length(11). + } + if (audioObjectType == 17 || audioObjectType == 19 || audioObjectType == 20 + || audioObjectType == 23) { + // aacSectionDataResilienceFlag, aacScalefactorDataResilienceFlag, + // aacSpectralDataResilienceFlag. + bitArray.skipBits(3); + } + bitArray.skipBits(1); // extensionFlag3. + } + } + } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ColorParser.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ColorParser.java index a10daf599040..31b81fe16f40 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ColorParser.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ColorParser.java @@ -26,7 +26,7 @@ import java.util.regex.Pattern; * * @see WebVTT CSS Styling * @see Timed Text Markup Language 2 (TTML2) - 10.3.5 - **/ + */ public final class ColorParser { private static final String RGB = "rgb"; @@ -271,4 +271,7 @@ public final class ColorParser { COLOR_MAP.put("yellowgreen", 0xFF9ACD32); } + private ColorParser() { + // Prevent instantiation. + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ConditionVariable.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ConditionVariable.java index 095e55636ed4..3866edced17e 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ConditionVariable.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ConditionVariable.java @@ -16,8 +16,8 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.util; /** - * A condition variable whose {@link #open()} and {@link #close()} methods return whether they - * resulted in a change of state. + * An interruptible condition variable whose {@link #open()} and {@link #close()} methods return + * whether they resulted in a change of state. */ public final class ConditionVariable { @@ -59,4 +59,25 @@ public final class ConditionVariable { } } + /** + * Blocks until the condition is opened or until {@code timeout} milliseconds have passed. + * + * @param timeout The maximum time to wait in milliseconds. + * @return True if the condition was opened, false if the call returns because of the timeout. + * @throws InterruptedException If the thread is interrupted. + */ + public synchronized boolean block(long timeout) throws InterruptedException { + long now = android.os.SystemClock.elapsedRealtime(); + long end = now + timeout; + while (!isOpen && now < end) { + wait(end - now); + now = android.os.SystemClock.elapsedRealtime(); + } + return isOpen; + } + + /** Returns whether the condition is opened. */ + public synchronized boolean isOpen() { + return isOpen; + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EGLSurfaceTexture.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EGLSurfaceTexture.java new file mode 100644 index 000000000000..1f48f718b7e9 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EGLSurfaceTexture.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util; + +import android.annotation.TargetApi; +import android.graphics.SurfaceTexture; +import android.opengl.EGL14; +import android.opengl.EGLConfig; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLSurface; +import android.opengl.GLES20; +import android.os.Handler; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Generates a {@link SurfaceTexture} using EGL/GLES functions. */ +@TargetApi(17) +public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableListener, Runnable { + + /** Listener to be called when the texture image on {@link SurfaceTexture} has been updated. */ + public interface TextureImageListener { + /** Called when the {@link SurfaceTexture} receives a new frame from its image producer. */ + void onFrameAvailable(); + } + + /** + * Secure mode to be used by the EGL surface and context. One of {@link #SECURE_MODE_NONE}, {@link + * #SECURE_MODE_SURFACELESS_CONTEXT} or {@link #SECURE_MODE_PROTECTED_PBUFFER}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER}) + public @interface SecureMode {} + + /** No secure EGL surface and context required. */ + public static final int SECURE_MODE_NONE = 0; + /** Creating a surfaceless, secured EGL context. */ + public static final int SECURE_MODE_SURFACELESS_CONTEXT = 1; + /** Creating a secure surface backed by a pixel buffer. */ + public static final int SECURE_MODE_PROTECTED_PBUFFER = 2; + + private static final int EGL_SURFACE_WIDTH = 1; + private static final int EGL_SURFACE_HEIGHT = 1; + + private static final int[] EGL_CONFIG_ATTRIBUTES = + new int[] { + EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, + EGL14.EGL_RED_SIZE, 8, + EGL14.EGL_GREEN_SIZE, 8, + EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_ALPHA_SIZE, 8, + EGL14.EGL_DEPTH_SIZE, 0, + EGL14.EGL_CONFIG_CAVEAT, EGL14.EGL_NONE, + EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT, + EGL14.EGL_NONE + }; + + private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; + + /** A runtime exception to be thrown if some EGL operations failed. */ + public static final class GlException extends RuntimeException { + private GlException(String msg) { + super(msg); + } + } + + private final Handler handler; + private final int[] textureIdHolder; + @Nullable private final TextureImageListener callback; + + @Nullable private EGLDisplay display; + @Nullable private EGLContext context; + @Nullable private EGLSurface surface; + @Nullable private SurfaceTexture texture; + + /** + * @param handler The {@link Handler} that will be used to call {@link + * SurfaceTexture#updateTexImage()} to update images on the {@link SurfaceTexture}. Note that + * {@link #init(int)} has to be called on the same looper thread as the {@link Handler}'s + * looper. + */ + public EGLSurfaceTexture(Handler handler) { + this(handler, /* callback= */ null); + } + + /** + * @param handler The {@link Handler} that will be used to call {@link + * SurfaceTexture#updateTexImage()} to update images on the {@link SurfaceTexture}. Note that + * {@link #init(int)} has to be called on the same looper thread as the looper of the {@link + * Handler}. + * @param callback The {@link TextureImageListener} to be called when the texture image on {@link + * SurfaceTexture} has been updated. This callback will be called on the same handler thread + * as the {@code handler}. + */ + public EGLSurfaceTexture(Handler handler, @Nullable TextureImageListener callback) { + this.handler = handler; + this.callback = callback; + textureIdHolder = new int[1]; + } + + /** + * Initializes required EGL parameters and creates the {@link SurfaceTexture}. + * + * @param secureMode The {@link SecureMode} to be used for EGL surface. + */ + public void init(@SecureMode int secureMode) { + display = getDefaultDisplay(); + EGLConfig config = chooseEGLConfig(display); + context = createEGLContext(display, config, secureMode); + surface = createEGLSurface(display, config, context, secureMode); + generateTextureIds(textureIdHolder); + texture = new SurfaceTexture(textureIdHolder[0]); + texture.setOnFrameAvailableListener(this); + } + + /** Releases all allocated resources. */ + @SuppressWarnings({"nullness:argument.type.incompatible"}) + public void release() { + handler.removeCallbacks(this); + try { + if (texture != null) { + texture.release(); + GLES20.glDeleteTextures(1, textureIdHolder, 0); + } + } finally { + if (display != null && !display.equals(EGL14.EGL_NO_DISPLAY)) { + EGL14.eglMakeCurrent( + display, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); + } + if (surface != null && !surface.equals(EGL14.EGL_NO_SURFACE)) { + EGL14.eglDestroySurface(display, surface); + } + if (context != null) { + EGL14.eglDestroyContext(display, context); + } + // EGL14.eglReleaseThread could crash before Android K (see [internal: b/11327779]). + if (Util.SDK_INT >= 19) { + EGL14.eglReleaseThread(); + } + if (display != null && !display.equals(EGL14.EGL_NO_DISPLAY)) { + // Android is unusual in that it uses a reference-counted EGLDisplay. So for + // every eglInitialize() we need an eglTerminate(). + EGL14.eglTerminate(display); + } + display = null; + context = null; + surface = null; + texture = null; + } + } + + /** + * Returns the wrapped {@link SurfaceTexture}. This can only be called after {@link #init(int)}. + */ + public SurfaceTexture getSurfaceTexture() { + return Assertions.checkNotNull(texture); + } + + // SurfaceTexture.OnFrameAvailableListener + + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + handler.post(this); + } + + // Runnable + + @Override + public void run() { + // Run on the provided handler thread when a new image frame is available. + dispatchOnFrameAvailable(); + if (texture != null) { + try { + texture.updateTexImage(); + } catch (RuntimeException e) { + // Ignore + } + } + } + + private void dispatchOnFrameAvailable() { + if (callback != null) { + callback.onFrameAvailable(); + } + } + + private static EGLDisplay getDefaultDisplay() { + EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + if (display == null) { + throw new GlException("eglGetDisplay failed"); + } + + int[] version = new int[2]; + boolean eglInitialized = + EGL14.eglInitialize(display, version, /* majorOffset= */ 0, version, /* minorOffset= */ 1); + if (!eglInitialized) { + throw new GlException("eglInitialize failed"); + } + return display; + } + + private static EGLConfig chooseEGLConfig(EGLDisplay display) { + EGLConfig[] configs = new EGLConfig[1]; + int[] numConfigs = new int[1]; + boolean success = + EGL14.eglChooseConfig( + display, + EGL_CONFIG_ATTRIBUTES, + /* attrib_listOffset= */ 0, + configs, + /* configsOffset= */ 0, + /* config_size= */ 1, + numConfigs, + /* num_configOffset= */ 0); + if (!success || numConfigs[0] <= 0 || configs[0] == null) { + throw new GlException( + Util.formatInvariant( + /* format= */ "eglChooseConfig failed: success=%b, numConfigs[0]=%d, configs[0]=%s", + success, numConfigs[0], configs[0])); + } + + return configs[0]; + } + + private static EGLContext createEGLContext( + EGLDisplay display, EGLConfig config, @SecureMode int secureMode) { + int[] glAttributes; + if (secureMode == SECURE_MODE_NONE) { + glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE}; + } else { + glAttributes = + new int[] { + EGL14.EGL_CONTEXT_CLIENT_VERSION, + 2, + EGL_PROTECTED_CONTENT_EXT, + EGL14.EGL_TRUE, + EGL14.EGL_NONE + }; + } + EGLContext context = + EGL14.eglCreateContext( + display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0); + if (context == null) { + throw new GlException("eglCreateContext failed"); + } + return context; + } + + private static EGLSurface createEGLSurface( + EGLDisplay display, EGLConfig config, EGLContext context, @SecureMode int secureMode) { + EGLSurface surface; + if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) { + surface = EGL14.EGL_NO_SURFACE; + } else { + int[] pbufferAttributes; + if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) { + pbufferAttributes = + new int[] { + EGL14.EGL_WIDTH, + EGL_SURFACE_WIDTH, + EGL14.EGL_HEIGHT, + EGL_SURFACE_HEIGHT, + EGL_PROTECTED_CONTENT_EXT, + EGL14.EGL_TRUE, + EGL14.EGL_NONE + }; + } else { + pbufferAttributes = + new int[] { + EGL14.EGL_WIDTH, + EGL_SURFACE_WIDTH, + EGL14.EGL_HEIGHT, + EGL_SURFACE_HEIGHT, + EGL14.EGL_NONE + }; + } + surface = EGL14.eglCreatePbufferSurface(display, config, pbufferAttributes, /* offset= */ 0); + if (surface == null) { + throw new GlException("eglCreatePbufferSurface failed"); + } + } + + boolean eglMadeCurrent = + EGL14.eglMakeCurrent(display, /* draw= */ surface, /* read= */ surface, context); + if (!eglMadeCurrent) { + throw new GlException("eglMakeCurrent failed"); + } + return surface; + } + + private static void generateTextureIds(int[] textureIdHolder) { + GLES20.glGenTextures(/* n= */ 1, textureIdHolder, /* offset= */ 0); + GlUtil.checkGlError(); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ErrorMessageProvider.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ErrorMessageProvider.java new file mode 100644 index 000000000000..0eca418cd8ef --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ErrorMessageProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util; + +import android.util.Pair; + +/** Converts throwables into error codes and user readable error messages. */ +public interface ErrorMessageProvider { + + /** + * Returns a pair consisting of an error code and a user readable error message for the given + * throwable. + * + * @param throwable The throwable for which an error code and message should be generated. + * @return A pair consisting of an error code and a user readable error message. + */ + Pair getErrorMessage(T throwable); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventDispatcher.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventDispatcher.java new file mode 100644 index 000000000000..6e9a3798bf23 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventDispatcher.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util; + +import android.os.Handler; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Event dispatcher which allows listener registration. + * + * @param The type of listener. + */ +public final class EventDispatcher { + + /** Functional interface to send an event. */ + public interface Event { + + /** + * Sends the event to a listener. + * + * @param listener The listener to send the event to. + */ + void sendTo(T listener); + } + + /** The list of listeners and handlers. */ + private final CopyOnWriteArrayList> listeners; + + /** Creates an event dispatcher. */ + public EventDispatcher() { + listeners = new CopyOnWriteArrayList<>(); + } + + /** Adds a listener to the event dispatcher. */ + public void addListener(Handler handler, T eventListener) { + Assertions.checkArgument(handler != null && eventListener != null); + removeListener(eventListener); + listeners.add(new HandlerAndListener<>(handler, eventListener)); + } + + /** Removes a listener from the event dispatcher. */ + public void removeListener(T eventListener) { + for (HandlerAndListener handlerAndListener : listeners) { + if (handlerAndListener.listener == eventListener) { + handlerAndListener.release(); + listeners.remove(handlerAndListener); + } + } + } + + /** + * Dispatches an event to all registered listeners. + * + * @param event The {@link Event}. + */ + public void dispatch(Event event) { + for (HandlerAndListener handlerAndListener : listeners) { + handlerAndListener.dispatch(event); + } + } + + private static final class HandlerAndListener { + + private final Handler handler; + private final T listener; + + private boolean released; + + public HandlerAndListener(Handler handler, T eventListener) { + this.handler = handler; + this.listener = eventListener; + } + + public void release() { + released = true; + } + + public void dispatch(Event event) { + handler.post( + () -> { + if (!released) { + event.sendTo(listener); + } + }); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventLogger.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventLogger.java new file mode 100644 index 000000000000..0c2a6abcf165 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventLogger.java @@ -0,0 +1,651 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util; + +import android.os.SystemClock; +import android.text.TextUtils; +import android.view.Surface; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.PlaybackSuppressionReason; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline; +import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioAttributes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderCounters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import java.io.IOException; +import java.text.NumberFormat; +import java.util.Locale; + +/** Logs events from {@link Player} and other core components using {@link Log}. */ +@SuppressWarnings("UngroupedOverloads") +public class EventLogger implements AnalyticsListener { + + private static final String DEFAULT_TAG = "EventLogger"; + private static final int MAX_TIMELINE_ITEM_LINES = 3; + private static final NumberFormat TIME_FORMAT; + static { + TIME_FORMAT = NumberFormat.getInstance(Locale.US); + TIME_FORMAT.setMinimumFractionDigits(2); + TIME_FORMAT.setMaximumFractionDigits(2); + TIME_FORMAT.setGroupingUsed(false); + } + + @Nullable private final MappingTrackSelector trackSelector; + private final String tag; + private final Timeline.Window window; + private final Timeline.Period period; + private final long startTimeMs; + + /** + * Creates event logger. + * + * @param trackSelector The mapping track selector used by the player. May be null if detailed + * logging of track mapping is not required. + */ + public EventLogger(@Nullable MappingTrackSelector trackSelector) { + this(trackSelector, DEFAULT_TAG); + } + + /** + * Creates event logger. + * + * @param trackSelector The mapping track selector used by the player. May be null if detailed + * logging of track mapping is not required. + * @param tag The tag used for logging. + */ + public EventLogger(@Nullable MappingTrackSelector trackSelector, String tag) { + this.trackSelector = trackSelector; + this.tag = tag; + window = new Timeline.Window(); + period = new Timeline.Period(); + startTimeMs = SystemClock.elapsedRealtime(); + } + + // AnalyticsListener + + @Override + public void onLoadingChanged(EventTime eventTime, boolean isLoading) { + logd(eventTime, "loading", Boolean.toString(isLoading)); + } + + @Override + public void onPlayerStateChanged( + EventTime eventTime, boolean playWhenReady, @Player.State int state) { + logd(eventTime, "state", playWhenReady + ", " + getStateString(state)); + } + + @Override + public void onPlaybackSuppressionReasonChanged( + EventTime eventTime, @PlaybackSuppressionReason int playbackSuppressionReason) { + logd( + eventTime, + "playbackSuppressionReason", + getPlaybackSuppressionReasonString(playbackSuppressionReason)); + } + + @Override + public void onIsPlayingChanged(EventTime eventTime, boolean isPlaying) { + logd(eventTime, "isPlaying", Boolean.toString(isPlaying)); + } + + @Override + public void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode) { + logd(eventTime, "repeatMode", getRepeatModeString(repeatMode)); + } + + @Override + public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) { + logd(eventTime, "shuffleModeEnabled", Boolean.toString(shuffleModeEnabled)); + } + + @Override + public void onPositionDiscontinuity(EventTime eventTime, @Player.DiscontinuityReason int reason) { + logd(eventTime, "positionDiscontinuity", getDiscontinuityReasonString(reason)); + } + + @Override + public void onSeekStarted(EventTime eventTime) { + logd(eventTime, "seekStarted"); + } + + @Override + public void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) { + logd( + eventTime, + "playbackParameters", + Util.formatInvariant( + "speed=%.2f, pitch=%.2f, skipSilence=%s", + playbackParameters.speed, playbackParameters.pitch, playbackParameters.skipSilence)); + } + + @Override + public void onTimelineChanged(EventTime eventTime, @Player.TimelineChangeReason int reason) { + int periodCount = eventTime.timeline.getPeriodCount(); + int windowCount = eventTime.timeline.getWindowCount(); + logd( + "timeline [" + + getEventTimeString(eventTime) + + ", periodCount=" + + periodCount + + ", windowCount=" + + windowCount + + ", reason=" + + getTimelineChangeReasonString(reason)); + for (int i = 0; i < Math.min(periodCount, MAX_TIMELINE_ITEM_LINES); i++) { + eventTime.timeline.getPeriod(i, period); + logd(" " + "period [" + getTimeString(period.getDurationMs()) + "]"); + } + if (periodCount > MAX_TIMELINE_ITEM_LINES) { + logd(" ..."); + } + for (int i = 0; i < Math.min(windowCount, MAX_TIMELINE_ITEM_LINES); i++) { + eventTime.timeline.getWindow(i, window); + logd( + " " + + "window [" + + getTimeString(window.getDurationMs()) + + ", " + + window.isSeekable + + ", " + + window.isDynamic + + "]"); + } + if (windowCount > MAX_TIMELINE_ITEM_LINES) { + logd(" ..."); + } + logd("]"); + } + + @Override + public void onPlayerError(EventTime eventTime, ExoPlaybackException e) { + loge(eventTime, "playerFailed", e); + } + + @Override + public void onTracksChanged( + EventTime eventTime, TrackGroupArray ignored, TrackSelectionArray trackSelections) { + MappedTrackInfo mappedTrackInfo = + trackSelector != null ? trackSelector.getCurrentMappedTrackInfo() : null; + if (mappedTrackInfo == null) { + logd(eventTime, "tracks", "[]"); + return; + } + logd("tracks [" + getEventTimeString(eventTime)); + // Log tracks associated to renderers. + int rendererCount = mappedTrackInfo.getRendererCount(); + for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { + TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); + TrackSelection trackSelection = trackSelections.get(rendererIndex); + if (rendererTrackGroups.length > 0) { + logd(" Renderer:" + rendererIndex + " ["); + for (int groupIndex = 0; groupIndex < rendererTrackGroups.length; groupIndex++) { + TrackGroup trackGroup = rendererTrackGroups.get(groupIndex); + String adaptiveSupport = + getAdaptiveSupportString( + trackGroup.length, + mappedTrackInfo.getAdaptiveSupport( + rendererIndex, groupIndex, /* includeCapabilitiesExceededTracks= */ false)); + logd(" Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " ["); + for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { + String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); + String formatSupport = + RendererCapabilities.getFormatSupportString( + mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)); + logd( + " " + + status + + " Track:" + + trackIndex + + ", " + + Format.toLogString(trackGroup.getFormat(trackIndex)) + + ", supported=" + + formatSupport); + } + logd(" ]"); + } + // Log metadata for at most one of the tracks selected for the renderer. + if (trackSelection != null) { + for (int selectionIndex = 0; selectionIndex < trackSelection.length(); selectionIndex++) { + Metadata metadata = trackSelection.getFormat(selectionIndex).metadata; + if (metadata != null) { + logd(" Metadata ["); + printMetadata(metadata, " "); + logd(" ]"); + break; + } + } + } + logd(" ]"); + } + } + // Log tracks not associated with a renderer. + TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnmappedTrackGroups(); + if (unassociatedTrackGroups.length > 0) { + logd(" Renderer:None ["); + for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) { + logd(" Group:" + groupIndex + " ["); + TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex); + for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { + String status = getTrackStatusString(false); + String formatSupport = + RendererCapabilities.getFormatSupportString( + RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); + logd( + " " + + status + + " Track:" + + trackIndex + + ", " + + Format.toLogString(trackGroup.getFormat(trackIndex)) + + ", supported=" + + formatSupport); + } + logd(" ]"); + } + logd(" ]"); + } + logd("]"); + } + + @Override + public void onSeekProcessed(EventTime eventTime) { + logd(eventTime, "seekProcessed"); + } + + @Override + public void onMetadata(EventTime eventTime, Metadata metadata) { + logd("metadata [" + getEventTimeString(eventTime)); + printMetadata(metadata, " "); + logd("]"); + } + + @Override + public void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters counters) { + logd(eventTime, "decoderEnabled", Util.getTrackTypeString(trackType)); + } + + @Override + public void onAudioSessionId(EventTime eventTime, int audioSessionId) { + logd(eventTime, "audioSessionId", Integer.toString(audioSessionId)); + } + + @Override + public void onAudioAttributesChanged(EventTime eventTime, AudioAttributes audioAttributes) { + logd( + eventTime, + "audioAttributes", + audioAttributes.contentType + + "," + + audioAttributes.flags + + "," + + audioAttributes.usage + + "," + + audioAttributes.allowedCapturePolicy); + } + + @Override + public void onVolumeChanged(EventTime eventTime, float volume) { + logd(eventTime, "volume", Float.toString(volume)); + } + + @Override + public void onDecoderInitialized( + EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) { + logd(eventTime, "decoderInitialized", Util.getTrackTypeString(trackType) + ", " + decoderName); + } + + @Override + public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) { + logd( + eventTime, + "decoderInputFormat", + Util.getTrackTypeString(trackType) + ", " + Format.toLogString(format)); + } + + @Override + public void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters counters) { + logd(eventTime, "decoderDisabled", Util.getTrackTypeString(trackType)); + } + + @Override + public void onAudioUnderrun( + EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + loge( + eventTime, + "audioTrackUnderrun", + bufferSize + ", " + bufferSizeMs + ", " + elapsedSinceLastFeedMs + "]", + null); + } + + @Override + public void onDroppedVideoFrames(EventTime eventTime, int count, long elapsedMs) { + logd(eventTime, "droppedFrames", Integer.toString(count)); + } + + @Override + public void onVideoSizeChanged( + EventTime eventTime, + int width, + int height, + int unappliedRotationDegrees, + float pixelWidthHeightRatio) { + logd(eventTime, "videoSize", width + ", " + height); + } + + @Override + public void onRenderedFirstFrame(EventTime eventTime, @Nullable Surface surface) { + logd(eventTime, "renderedFirstFrame", String.valueOf(surface)); + } + + @Override + public void onMediaPeriodCreated(EventTime eventTime) { + logd(eventTime, "mediaPeriodCreated"); + } + + @Override + public void onMediaPeriodReleased(EventTime eventTime) { + logd(eventTime, "mediaPeriodReleased"); + } + + @Override + public void onLoadStarted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + // Do nothing. + } + + @Override + public void onLoadError( + EventTime eventTime, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + printInternalError(eventTime, "loadError", error); + } + + @Override + public void onLoadCanceled( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + // Do nothing. + } + + @Override + public void onLoadCompleted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + // Do nothing. + } + + @Override + public void onReadingStarted(EventTime eventTime) { + logd(eventTime, "mediaPeriodReadingStarted"); + } + + @Override + public void onBandwidthEstimate( + EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) { + // Do nothing. + } + + @Override + public void onSurfaceSizeChanged(EventTime eventTime, int width, int height) { + logd(eventTime, "surfaceSize", width + ", " + height); + } + + @Override + public void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) { + logd(eventTime, "upstreamDiscarded", Format.toLogString(mediaLoadData.trackFormat)); + } + + @Override + public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { + logd(eventTime, "downstreamFormat", Format.toLogString(mediaLoadData.trackFormat)); + } + + @Override + public void onDrmSessionAcquired(EventTime eventTime) { + logd(eventTime, "drmSessionAcquired"); + } + + @Override + public void onDrmSessionManagerError(EventTime eventTime, Exception e) { + printInternalError(eventTime, "drmSessionManagerError", e); + } + + @Override + public void onDrmKeysRestored(EventTime eventTime) { + logd(eventTime, "drmKeysRestored"); + } + + @Override + public void onDrmKeysRemoved(EventTime eventTime) { + logd(eventTime, "drmKeysRemoved"); + } + + @Override + public void onDrmKeysLoaded(EventTime eventTime) { + logd(eventTime, "drmKeysLoaded"); + } + + @Override + public void onDrmSessionReleased(EventTime eventTime) { + logd(eventTime, "drmSessionReleased"); + } + + /** + * Logs a debug message. + * + * @param msg The message to log. + */ + protected void logd(String msg) { + Log.d(tag, msg); + } + + /** + * Logs an error message. + * + * @param msg The message to log. + */ + protected void loge(String msg) { + Log.e(tag, msg); + } + + // Internal methods + + private void logd(EventTime eventTime, String eventName) { + logd(getEventString(eventTime, eventName, /* eventDescription= */ null, /* throwable= */ null)); + } + + private void logd(EventTime eventTime, String eventName, String eventDescription) { + logd(getEventString(eventTime, eventName, eventDescription, /* throwable= */ null)); + } + + private void loge(EventTime eventTime, String eventName, @Nullable Throwable throwable) { + loge(getEventString(eventTime, eventName, /* eventDescription= */ null, throwable)); + } + + private void loge( + EventTime eventTime, + String eventName, + String eventDescription, + @Nullable Throwable throwable) { + loge(getEventString(eventTime, eventName, eventDescription, throwable)); + } + + private void printInternalError(EventTime eventTime, String type, Exception e) { + loge(eventTime, "internalError", type, e); + } + + private void printMetadata(Metadata metadata, String prefix) { + for (int i = 0; i < metadata.length(); i++) { + logd(prefix + metadata.get(i)); + } + } + + private String getEventString( + EventTime eventTime, + String eventName, + @Nullable String eventDescription, + @Nullable Throwable throwable) { + String eventString = eventName + " [" + getEventTimeString(eventTime); + if (eventDescription != null) { + eventString += ", " + eventDescription; + } + @Nullable String throwableString = Log.getThrowableString(throwable); + if (!TextUtils.isEmpty(throwableString)) { + eventString += "\n " + throwableString.replace("\n", "\n ") + '\n'; + } + eventString += "]"; + return eventString; + } + + private String getEventTimeString(EventTime eventTime) { + String windowPeriodString = "window=" + eventTime.windowIndex; + if (eventTime.mediaPeriodId != null) { + windowPeriodString += + ", period=" + eventTime.timeline.getIndexOfPeriod(eventTime.mediaPeriodId.periodUid); + if (eventTime.mediaPeriodId.isAd()) { + windowPeriodString += ", adGroup=" + eventTime.mediaPeriodId.adGroupIndex; + windowPeriodString += ", ad=" + eventTime.mediaPeriodId.adIndexInAdGroup; + } + } + return "eventTime=" + + getTimeString(eventTime.realtimeMs - startTimeMs) + + ", mediaPos=" + + getTimeString(eventTime.currentPlaybackPositionMs) + + ", " + + windowPeriodString; + } + + private static String getTimeString(long timeMs) { + return timeMs == C.TIME_UNSET ? "?" : TIME_FORMAT.format((timeMs) / 1000f); + } + + private static String getStateString(int state) { + switch (state) { + case Player.STATE_BUFFERING: + return "BUFFERING"; + case Player.STATE_ENDED: + return "ENDED"; + case Player.STATE_IDLE: + return "IDLE"; + case Player.STATE_READY: + return "READY"; + default: + return "?"; + } + } + + private static String getAdaptiveSupportString( + int trackCount, @AdaptiveSupport int adaptiveSupport) { + if (trackCount < 2) { + return "N/A"; + } + switch (adaptiveSupport) { + case RendererCapabilities.ADAPTIVE_SEAMLESS: + return "YES"; + case RendererCapabilities.ADAPTIVE_NOT_SEAMLESS: + return "YES_NOT_SEAMLESS"; + case RendererCapabilities.ADAPTIVE_NOT_SUPPORTED: + return "NO"; + default: + throw new IllegalStateException(); + } + } + + // Suppressing reference equality warning because the track group stored in the track selection + // must point to the exact track group object to be considered part of it. + @SuppressWarnings("ReferenceEquality") + private static String getTrackStatusString( + @Nullable TrackSelection selection, TrackGroup group, int trackIndex) { + return getTrackStatusString(selection != null && selection.getTrackGroup() == group + && selection.indexOf(trackIndex) != C.INDEX_UNSET); + } + + private static String getTrackStatusString(boolean enabled) { + return enabled ? "[X]" : "[ ]"; + } + + private static String getRepeatModeString(@Player.RepeatMode int repeatMode) { + switch (repeatMode) { + case Player.REPEAT_MODE_OFF: + return "OFF"; + case Player.REPEAT_MODE_ONE: + return "ONE"; + case Player.REPEAT_MODE_ALL: + return "ALL"; + default: + return "?"; + } + } + + private static String getDiscontinuityReasonString(@Player.DiscontinuityReason int reason) { + switch (reason) { + case Player.DISCONTINUITY_REASON_PERIOD_TRANSITION: + return "PERIOD_TRANSITION"; + case Player.DISCONTINUITY_REASON_SEEK: + return "SEEK"; + case Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT: + return "SEEK_ADJUSTMENT"; + case Player.DISCONTINUITY_REASON_AD_INSERTION: + return "AD_INSERTION"; + case Player.DISCONTINUITY_REASON_INTERNAL: + return "INTERNAL"; + default: + return "?"; + } + } + + private static String getTimelineChangeReasonString(@Player.TimelineChangeReason int reason) { + switch (reason) { + case Player.TIMELINE_CHANGE_REASON_PREPARED: + return "PREPARED"; + case Player.TIMELINE_CHANGE_REASON_RESET: + return "RESET"; + case Player.TIMELINE_CHANGE_REASON_DYNAMIC: + return "DYNAMIC"; + default: + return "?"; + } + } + + private static String getPlaybackSuppressionReasonString( + @PlaybackSuppressionReason int playbackSuppressionReason) { + switch (playbackSuppressionReason) { + case Player.PLAYBACK_SUPPRESSION_REASON_NONE: + return "NONE"; + case Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS: + return "TRANSIENT_AUDIO_FOCUS_LOSS"; + default: + return "?"; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacConstants.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacConstants.java new file mode 100644 index 000000000000..faa917fab82b --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacConstants.java @@ -0,0 +1,42 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util; + +/** Defines constants used by the FLAC extractor. */ +public final class FlacConstants { + + /** Size of the FLAC stream marker in bytes. */ + public static final int STREAM_MARKER_SIZE = 4; + /** Size of the header of a FLAC metadata block in bytes. */ + public static final int METADATA_BLOCK_HEADER_SIZE = 4; + /** Size of the FLAC stream info block (header included) in bytes. */ + public static final int STREAM_INFO_BLOCK_SIZE = 38; + /** Minimum size of a FLAC frame header in bytes. */ + public static final int MIN_FRAME_HEADER_SIZE = 6; + /** Maximum size of a FLAC frame header in bytes. */ + public static final int MAX_FRAME_HEADER_SIZE = 16; + + /** Stream info metadata block type. */ + public static final int METADATA_TYPE_STREAM_INFO = 0; + /** Seek table metadata block type. */ + public static final int METADATA_TYPE_SEEK_TABLE = 3; + /** Vorbis comment metadata block type. */ + public static final int METADATA_TYPE_VORBIS_COMMENT = 4; + /** Picture metadata block type. */ + public static final int METADATA_TYPE_PICTURE = 6; + + private FlacConstants() {} +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacStreamInfo.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacStreamInfo.java deleted file mode 100644 index 96c0b3343cc9..000000000000 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacStreamInfo.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util; - -/** - * Holder for FLAC stream info. - */ -public final class FlacStreamInfo { - - public final int minBlockSize; - public final int maxBlockSize; - public final int minFrameSize; - public final int maxFrameSize; - public final int sampleRate; - public final int channels; - public final int bitsPerSample; - public final long totalSamples; - - /** - * Constructs a FlacStreamInfo parsing the given binary FLAC stream info metadata structure. - * - * @param data An array holding FLAC stream info metadata structure - * @param offset Offset of the structure in the array - * @see FLAC format - * METADATA_BLOCK_STREAMINFO - */ - public FlacStreamInfo(byte[] data, int offset) { - ParsableBitArray scratch = new ParsableBitArray(data); - scratch.setPosition(offset * 8); - this.minBlockSize = scratch.readBits(16); - this.maxBlockSize = scratch.readBits(16); - this.minFrameSize = scratch.readBits(24); - this.maxFrameSize = scratch.readBits(24); - this.sampleRate = scratch.readBits(20); - this.channels = scratch.readBits(3) + 1; - this.bitsPerSample = scratch.readBits(5) + 1; - this.totalSamples = scratch.readBits(36); - // Remaining 16 bytes is md5 value - } - - public FlacStreamInfo(int minBlockSize, int maxBlockSize, int minFrameSize, int maxFrameSize, - int sampleRate, int channels, int bitsPerSample, long totalSamples) { - this.minBlockSize = minBlockSize; - this.maxBlockSize = maxBlockSize; - this.minFrameSize = minFrameSize; - this.maxFrameSize = maxFrameSize; - this.sampleRate = sampleRate; - this.channels = channels; - this.bitsPerSample = bitsPerSample; - this.totalSamples = totalSamples; - } - - public int maxDecodedFrameSize() { - return maxBlockSize * channels * 2; - } - - public int bitRate() { - return bitsPerSample * sampleRate; - } - - public long durationUs() { - return (totalSamples * 1000000L) / sampleRate; - } - -} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacStreamMetadata.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacStreamMetadata.java new file mode 100644 index 000000000000..893481d8da2d --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacStreamMetadata.java @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.flac.PictureFrame; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.flac.VorbisComment; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Holder for FLAC metadata. + * + * @see FLAC format + * METADATA_BLOCK_STREAMINFO + * @see FLAC format + * METADATA_BLOCK_SEEKTABLE + * @see FLAC format + * METADATA_BLOCK_VORBIS_COMMENT + * @see FLAC format + * METADATA_BLOCK_PICTURE + */ +public final class FlacStreamMetadata { + + /** A FLAC seek table. */ + public static class SeekTable { + /** Seek points sample numbers. */ + public final long[] pointSampleNumbers; + /** Seek points byte offsets from the first frame. */ + public final long[] pointOffsets; + + public SeekTable(long[] pointSampleNumbers, long[] pointOffsets) { + this.pointSampleNumbers = pointSampleNumbers; + this.pointOffsets = pointOffsets; + } + } + + private static final String TAG = "FlacStreamMetadata"; + + /** Indicates that a value is not in the corresponding lookup table. */ + public static final int NOT_IN_LOOKUP_TABLE = -1; + /** Separator between the field name of a Vorbis comment and the corresponding value. */ + private static final String SEPARATOR = "="; + + /** Minimum number of samples per block. */ + public final int minBlockSizeSamples; + /** Maximum number of samples per block. */ + public final int maxBlockSizeSamples; + /** Minimum frame size in bytes, or 0 if the value is unknown. */ + public final int minFrameSize; + /** Maximum frame size in bytes, or 0 if the value is unknown. */ + public final int maxFrameSize; + /** Sample rate in Hertz. */ + public final int sampleRate; + /** + * Lookup key corresponding to the stream sample rate, or {@link #NOT_IN_LOOKUP_TABLE} if it is + * not in the lookup table. + * + *

This key is used to indicate the sample rate in the frame header for the most common values. + * + *

The sample rate lookup table is described in https://xiph.org/flac/format.html#frame_header. + */ + public final int sampleRateLookupKey; + /** Number of audio channels. */ + public final int channels; + /** Number of bits per sample. */ + public final int bitsPerSample; + /** + * Lookup key corresponding to the number of bits per sample of the stream, or {@link + * #NOT_IN_LOOKUP_TABLE} if it is not in the lookup table. + * + *

This key is used to indicate the number of bits per sample in the frame header for the most + * common values. + * + *

The sample size lookup table is described in https://xiph.org/flac/format.html#frame_header. + */ + public final int bitsPerSampleLookupKey; + /** Total number of samples, or 0 if the value is unknown. */ + public final long totalSamples; + /** Seek table, or {@code null} if it is not provided. */ + @Nullable public final SeekTable seekTable; + /** Content metadata, or {@code null} if it is not provided. */ + @Nullable private final Metadata metadata; + + /** + * Parses binary FLAC stream info metadata. + * + * @param data An array containing binary FLAC stream info block. + * @param offset The offset of the stream info block in {@code data}, excluding the header (i.e. + * the offset points to the first byte of the minimum block size). + */ + public FlacStreamMetadata(byte[] data, int offset) { + ParsableBitArray scratch = new ParsableBitArray(data); + scratch.setPosition(offset * 8); + minBlockSizeSamples = scratch.readBits(16); + maxBlockSizeSamples = scratch.readBits(16); + minFrameSize = scratch.readBits(24); + maxFrameSize = scratch.readBits(24); + sampleRate = scratch.readBits(20); + sampleRateLookupKey = getSampleRateLookupKey(sampleRate); + channels = scratch.readBits(3) + 1; + bitsPerSample = scratch.readBits(5) + 1; + bitsPerSampleLookupKey = getBitsPerSampleLookupKey(bitsPerSample); + totalSamples = scratch.readBitsToLong(36); + seekTable = null; + metadata = null; + } + + // Used in native code. + public FlacStreamMetadata( + int minBlockSizeSamples, + int maxBlockSizeSamples, + int minFrameSize, + int maxFrameSize, + int sampleRate, + int channels, + int bitsPerSample, + long totalSamples, + ArrayList vorbisComments, + ArrayList pictureFrames) { + this( + minBlockSizeSamples, + maxBlockSizeSamples, + minFrameSize, + maxFrameSize, + sampleRate, + channels, + bitsPerSample, + totalSamples, + /* seekTable= */ null, + buildMetadata(vorbisComments, pictureFrames)); + } + + private FlacStreamMetadata( + int minBlockSizeSamples, + int maxBlockSizeSamples, + int minFrameSize, + int maxFrameSize, + int sampleRate, + int channels, + int bitsPerSample, + long totalSamples, + @Nullable SeekTable seekTable, + @Nullable Metadata metadata) { + this.minBlockSizeSamples = minBlockSizeSamples; + this.maxBlockSizeSamples = maxBlockSizeSamples; + this.minFrameSize = minFrameSize; + this.maxFrameSize = maxFrameSize; + this.sampleRate = sampleRate; + this.sampleRateLookupKey = getSampleRateLookupKey(sampleRate); + this.channels = channels; + this.bitsPerSample = bitsPerSample; + this.bitsPerSampleLookupKey = getBitsPerSampleLookupKey(bitsPerSample); + this.totalSamples = totalSamples; + this.seekTable = seekTable; + this.metadata = metadata; + } + + /** Returns the maximum size for a decoded frame from the FLAC stream. */ + public int getMaxDecodedFrameSize() { + return maxBlockSizeSamples * channels * (bitsPerSample / 8); + } + + /** Returns the bit-rate of the FLAC stream. */ + public int getBitRate() { + return bitsPerSample * sampleRate * channels; + } + + /** + * Returns the duration of the FLAC stream in microseconds, or {@link C#TIME_UNSET} if the total + * number of samples if unknown. + */ + public long getDurationUs() { + return totalSamples == 0 ? C.TIME_UNSET : totalSamples * C.MICROS_PER_SECOND / sampleRate; + } + + /** + * Returns the sample number of the sample at a given time. + * + * @param timeUs Time position in microseconds in the FLAC stream. + * @return The sample number corresponding to the time position. + */ + public long getSampleNumber(long timeUs) { + long sampleNumber = (timeUs * sampleRate) / C.MICROS_PER_SECOND; + return Util.constrainValue(sampleNumber, /* min= */ 0, totalSamples - 1); + } + + /** Returns the approximate number of bytes per frame for the current FLAC stream. */ + public long getApproxBytesPerFrame() { + long approxBytesPerFrame; + if (maxFrameSize > 0) { + approxBytesPerFrame = ((long) maxFrameSize + minFrameSize) / 2 + 1; + } else { + // Uses the stream's block-size if it's a known fixed block-size stream, otherwise uses the + // default value for FLAC block-size, which is 4096. + long blockSizeSamples = + (minBlockSizeSamples == maxBlockSizeSamples && minBlockSizeSamples > 0) + ? minBlockSizeSamples + : 4096; + approxBytesPerFrame = (blockSizeSamples * channels * bitsPerSample) / 8 + 64; + } + return approxBytesPerFrame; + } + + /** + * Returns a {@link Format} extracted from the FLAC stream metadata. + * + *

{@code streamMarkerAndInfoBlock} is updated to set the bit corresponding to the stream info + * last metadata block flag to true. + * + * @param streamMarkerAndInfoBlock An array containing the FLAC stream marker followed by the + * stream info block. + * @param id3Metadata The ID3 metadata of the stream, or {@code null} if there is no such data. + * @return The extracted {@link Format}. + */ + public Format getFormat(byte[] streamMarkerAndInfoBlock, @Nullable Metadata id3Metadata) { + // Set the last metadata block flag, ignore the other blocks. + streamMarkerAndInfoBlock[4] = (byte) 0x80; + int maxInputSize = maxFrameSize > 0 ? maxFrameSize : Format.NO_VALUE; + @Nullable Metadata metadataWithId3 = getMetadataCopyWithAppendedEntriesFrom(id3Metadata); + + return Format.createAudioSampleFormat( + /* id= */ null, + MimeTypes.AUDIO_FLAC, + /* codecs= */ null, + getBitRate(), + maxInputSize, + channels, + sampleRate, + /* pcmEncoding= */ Format.NO_VALUE, + /* encoderDelay= */ 0, + /* encoderPadding= */ 0, + /* initializationData= */ Collections.singletonList(streamMarkerAndInfoBlock), + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null, + metadataWithId3); + } + + /** Returns a copy of the content metadata with entries from {@code other} appended. */ + @Nullable + public Metadata getMetadataCopyWithAppendedEntriesFrom(@Nullable Metadata other) { + return metadata == null ? other : metadata.copyWithAppendedEntriesFrom(other); + } + + /** Returns a copy of {@code this} with the seek table replaced by the one given. */ + public FlacStreamMetadata copyWithSeekTable(@Nullable SeekTable seekTable) { + return new FlacStreamMetadata( + minBlockSizeSamples, + maxBlockSizeSamples, + minFrameSize, + maxFrameSize, + sampleRate, + channels, + bitsPerSample, + totalSamples, + seekTable, + metadata); + } + + /** Returns a copy of {@code this} with the given Vorbis comments added to the metadata. */ + public FlacStreamMetadata copyWithVorbisComments(List vorbisComments) { + @Nullable + Metadata appendedMetadata = + getMetadataCopyWithAppendedEntriesFrom( + buildMetadata(vorbisComments, Collections.emptyList())); + return new FlacStreamMetadata( + minBlockSizeSamples, + maxBlockSizeSamples, + minFrameSize, + maxFrameSize, + sampleRate, + channels, + bitsPerSample, + totalSamples, + seekTable, + appendedMetadata); + } + + /** Returns a copy of {@code this} with the given picture frames added to the metadata. */ + public FlacStreamMetadata copyWithPictureFrames(List pictureFrames) { + @Nullable + Metadata appendedMetadata = + getMetadataCopyWithAppendedEntriesFrom( + buildMetadata(Collections.emptyList(), pictureFrames)); + return new FlacStreamMetadata( + minBlockSizeSamples, + maxBlockSizeSamples, + minFrameSize, + maxFrameSize, + sampleRate, + channels, + bitsPerSample, + totalSamples, + seekTable, + appendedMetadata); + } + + private static int getSampleRateLookupKey(int sampleRate) { + switch (sampleRate) { + case 88200: + return 1; + case 176400: + return 2; + case 192000: + return 3; + case 8000: + return 4; + case 16000: + return 5; + case 22050: + return 6; + case 24000: + return 7; + case 32000: + return 8; + case 44100: + return 9; + case 48000: + return 10; + case 96000: + return 11; + default: + return NOT_IN_LOOKUP_TABLE; + } + } + + private static int getBitsPerSampleLookupKey(int bitsPerSample) { + switch (bitsPerSample) { + case 8: + return 1; + case 12: + return 2; + case 16: + return 4; + case 20: + return 5; + case 24: + return 6; + default: + return NOT_IN_LOOKUP_TABLE; + } + } + + @Nullable + private static Metadata buildMetadata( + List vorbisComments, List pictureFrames) { + if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) { + return null; + } + + ArrayList metadataEntries = new ArrayList<>(); + for (int i = 0; i < vorbisComments.size(); i++) { + String vorbisComment = vorbisComments.get(i); + String[] keyAndValue = Util.splitAtFirst(vorbisComment, SEPARATOR); + if (keyAndValue.length != 2) { + Log.w(TAG, "Failed to parse Vorbis comment: " + vorbisComment); + } else { + VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]); + metadataEntries.add(entry); + } + } + metadataEntries.addAll(pictureFrames); + + return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/GlUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/GlUtil.java new file mode 100644 index 000000000000..a34cee48f9ef --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/GlUtil.java @@ -0,0 +1,404 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util; + +import static android.opengl.GLU.gluErrorString; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageManager; +import android.opengl.EGL14; +import android.opengl.EGLDisplay; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import android.text.TextUtils; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import javax.microedition.khronos.egl.EGL10; + +/** GL utilities. */ +public final class GlUtil { + + /** + * GL attribute, which can be attached to a buffer with {@link Attribute#setBuffer(float[], int)}. + */ + public static final class Attribute { + + /** The name of the attribute in the GLSL sources. */ + public final String name; + + private final int index; + private final int location; + + @Nullable private Buffer buffer; + private int size; + + /** + * Creates a new GL attribute. + * + * @param program The identifier of a compiled and linked GLSL shader program. + * @param index The index of the attribute. After this instance has been constructed, the name + * of the attribute is available via the {@link #name} field. + */ + public Attribute(int program, int index) { + int[] len = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, len, 0); + + int[] type = new int[1]; + int[] size = new int[1]; + byte[] nameBytes = new byte[len[0]]; + int[] ignore = new int[1]; + + GLES20.glGetActiveAttrib(program, index, len[0], ignore, 0, size, 0, type, 0, nameBytes, 0); + name = new String(nameBytes, 0, strlen(nameBytes)); + location = GLES20.glGetAttribLocation(program, name); + this.index = index; + } + + /** + * Configures {@link #bind()} to attach vertices in {@code buffer} (each of size {@code size} + * elements) to this {@link Attribute}. + * + * @param buffer Buffer to bind to this attribute. + * @param size Number of elements per vertex. + */ + public void setBuffer(float[] buffer, int size) { + this.buffer = createBuffer(buffer); + this.size = size; + } + + /** + * Sets the vertex attribute to whatever was attached via {@link #setBuffer(float[], int)}. + * + *

Should be called before each drawing call. + */ + public void bind() { + Buffer buffer = Assertions.checkNotNull(this.buffer, "call setBuffer before bind"); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + GLES20.glVertexAttribPointer( + location, + size, // count + GLES20.GL_FLOAT, // type + false, // normalize + 0, // stride + buffer); + GLES20.glEnableVertexAttribArray(index); + checkGlError(); + } + } + + /** + * GL uniform, which can be attached to a sampler using {@link Uniform#setSamplerTexId(int, int)}. + */ + public static final class Uniform { + + /** The name of the uniform in the GLSL sources. */ + public final String name; + + private final int location; + private final int type; + private final float[] value; + + private int texId; + private int unit; + + /** + * Creates a new GL uniform. + * + * @param program The identifier of a compiled and linked GLSL shader program. + * @param index The index of the uniform. After this instance has been constructed, the name of + * the uniform is available via the {@link #name} field. + */ + public Uniform(int program, int index) { + int[] len = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_UNIFORM_MAX_LENGTH, len, 0); + + int[] type = new int[1]; + int[] size = new int[1]; + byte[] name = new byte[len[0]]; + int[] ignore = new int[1]; + + GLES20.glGetActiveUniform(program, index, len[0], ignore, 0, size, 0, type, 0, name, 0); + this.name = new String(name, 0, strlen(name)); + location = GLES20.glGetUniformLocation(program, this.name); + this.type = type[0]; + + value = new float[1]; + } + + /** + * Configures {@link #bind()} to use the specified {@code texId} for this sampler uniform. + * + * @param texId The GL texture identifier from which to sample. + * @param unit The GL texture unit index. + */ + public void setSamplerTexId(int texId, int unit) { + this.texId = texId; + this.unit = unit; + } + + /** Configures {@link #bind()} to use the specified float {@code value} for this uniform. */ + public void setFloat(float value) { + this.value[0] = value; + } + + /** + * Sets the uniform to whatever value was passed via {@link #setSamplerTexId(int, int)} or + * {@link #setFloat(float)}. + * + *

Should be called before each drawing call. + */ + public void bind() { + if (type == GLES20.GL_FLOAT) { + GLES20.glUniform1fv(location, 1, value, 0); + checkGlError(); + return; + } + + if (texId == 0) { + throw new IllegalStateException("call setSamplerTexId before bind"); + } + GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + unit); + if (type == GLES11Ext.GL_SAMPLER_EXTERNAL_OES) { + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId); + } else if (type == GLES20.GL_SAMPLER_2D) { + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId); + } else { + throw new IllegalStateException("unexpected uniform type: " + type); + } + GLES20.glUniform1i(location, unit); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + checkGlError(); + } + } + + private static final String TAG = "GlUtil"; + + private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content"; + private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context"; + + /** Class only contains static methods. */ + private GlUtil() {} + + /** + * Returns whether creating a GL context with {@value EXTENSION_PROTECTED_CONTENT} is possible. If + * {@code true}, the device supports a protected output path for DRM content when using GL. + */ + @TargetApi(24) + public static boolean isProtectedContentExtensionSupported(Context context) { + if (Util.SDK_INT < 24) { + return false; + } + if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) { + // Samsung devices running Nougat are known to be broken. See + // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802]. + // Moto Z XT1650 is also affected. See + // https://github.com/google/ExoPlayer/issues/3215. + return false; + } + if (Util.SDK_INT < 26 + && !context + .getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) { + // Pre API level 26 devices were not well tested unless they supported VR mode. + return false; + } + + EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + @Nullable String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); + return eglExtensions != null && eglExtensions.contains(EXTENSION_PROTECTED_CONTENT); + } + + /** + * Returns whether creating a GL context with {@value EXTENSION_SURFACELESS_CONTEXT} is possible. + */ + @TargetApi(17) + public static boolean isSurfacelessContextExtensionSupported() { + if (Util.SDK_INT < 17) { + return false; + } + EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + @Nullable String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); + return eglExtensions != null && eglExtensions.contains(EXTENSION_SURFACELESS_CONTEXT); + } + + /** + * If there is an OpenGl error, logs the error and if {@link + * ExoPlayerLibraryInfo#GL_ASSERTIONS_ENABLED} is true throws a {@link RuntimeException}. + */ + public static void checkGlError() { + int lastError = GLES20.GL_NO_ERROR; + int error; + while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { + Log.e(TAG, "glError " + gluErrorString(error)); + lastError = error; + } + if (ExoPlayerLibraryInfo.GL_ASSERTIONS_ENABLED && lastError != GLES20.GL_NO_ERROR) { + throw new RuntimeException("glError " + gluErrorString(lastError)); + } + } + + /** + * Builds a GL shader program from vertex and fragment shader code. + * + * @param vertexCode GLES20 vertex shader program as arrays of strings. Strings are joined by + * adding a new line character in between each of them. + * @param fragmentCode GLES20 fragment shader program as arrays of strings. Strings are joined by + * adding a new line character in between each of them. + * @return GLES20 program id. + */ + public static int compileProgram(String[] vertexCode, String[] fragmentCode) { + return compileProgram(TextUtils.join("\n", vertexCode), TextUtils.join("\n", fragmentCode)); + } + + /** + * Builds a GL shader program from vertex and fragment shader code. + * + * @param vertexCode GLES20 vertex shader program. + * @param fragmentCode GLES20 fragment shader program. + * @return GLES20 program id. + */ + public static int compileProgram(String vertexCode, String fragmentCode) { + int program = GLES20.glCreateProgram(); + checkGlError(); + + // Add the vertex and fragment shaders. + addShader(GLES20.GL_VERTEX_SHADER, vertexCode, program); + addShader(GLES20.GL_FRAGMENT_SHADER, fragmentCode, program); + + // Link and check for errors. + GLES20.glLinkProgram(program); + int[] linkStatus = new int[] {GLES20.GL_FALSE}; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] != GLES20.GL_TRUE) { + throwGlError("Unable to link shader program: \n" + GLES20.glGetProgramInfoLog(program)); + } + checkGlError(); + + return program; + } + + /** Returns the {@link Attribute}s in the specified {@code program}. */ + public static Attribute[] getAttributes(int program) { + int[] attributeCount = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_ATTRIBUTES, attributeCount, 0); + if (attributeCount[0] != 2) { + throw new IllegalStateException("expected two attributes"); + } + + Attribute[] attributes = new Attribute[attributeCount[0]]; + for (int i = 0; i < attributeCount[0]; i++) { + attributes[i] = new Attribute(program, i); + } + return attributes; + } + + /** Returns the {@link Uniform}s in the specified {@code program}. */ + public static Uniform[] getUniforms(int program) { + int[] uniformCount = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_UNIFORMS, uniformCount, 0); + + Uniform[] uniforms = new Uniform[uniformCount[0]]; + for (int i = 0; i < uniformCount[0]; i++) { + uniforms[i] = new Uniform(program, i); + } + + return uniforms; + } + + /** + * Allocates a FloatBuffer with the given data. + * + * @param data Used to initialize the new buffer. + */ + public static FloatBuffer createBuffer(float[] data) { + return (FloatBuffer) createBuffer(data.length).put(data).flip(); + } + + /** + * Allocates a FloatBuffer. + * + * @param capacity The new buffer's capacity, in floats. + */ + public static FloatBuffer createBuffer(int capacity) { + ByteBuffer byteBuffer = ByteBuffer.allocateDirect(capacity * C.BYTES_PER_FLOAT); + return byteBuffer.order(ByteOrder.nativeOrder()).asFloatBuffer(); + } + + /** + * Creates a GL_TEXTURE_EXTERNAL_OES with default configuration of GL_LINEAR filtering and + * GL_CLAMP_TO_EDGE wrapping. + */ + public static int createExternalTexture() { + int[] texId = new int[1]; + GLES20.glGenTextures(1, IntBuffer.wrap(texId)); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId[0]); + GLES20.glTexParameteri( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + checkGlError(); + return texId[0]; + } + + private static void addShader(int type, String source, int program) { + int shader = GLES20.glCreateShader(type); + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + + int[] result = new int[] {GLES20.GL_FALSE}; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, result, 0); + if (result[0] != GLES20.GL_TRUE) { + throwGlError(GLES20.glGetShaderInfoLog(shader) + ", source: " + source); + } + + GLES20.glAttachShader(program, shader); + GLES20.glDeleteShader(shader); + checkGlError(); + } + + private static void throwGlError(String errorMsg) { + Log.e(TAG, errorMsg); + if (ExoPlayerLibraryInfo.GL_ASSERTIONS_ENABLED) { + throw new RuntimeException(errorMsg); + } + } + + /** Returns the length of the null-terminated string in {@code strVal}. */ + private static int strlen(byte[] strVal) { + for (int i = 0; i < strVal.length; ++i) { + if (strVal[i] == '\0') { + return i; + } + } + return strVal.length; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/HandlerWrapper.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/HandlerWrapper.java new file mode 100644 index 000000000000..2e412fa10fa9 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/HandlerWrapper.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import androidx.annotation.Nullable; + +/** + * An interface to call through to a {@link Handler}. Instances must be created by calling {@link + * Clock#createHandler(Looper, Handler.Callback)} on {@link Clock#DEFAULT} for all non-test cases. + */ +public interface HandlerWrapper { + + /** @see Handler#getLooper() */ + Looper getLooper(); + + /** @see Handler#obtainMessage(int) */ + Message obtainMessage(int what); + + /** @see Handler#obtainMessage(int, Object) */ + Message obtainMessage(int what, @Nullable Object obj); + + /** @see Handler#obtainMessage(int, int, int) */ + Message obtainMessage(int what, int arg1, int arg2); + + /** @see Handler#obtainMessage(int, int, int, Object) */ + Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj); + + /** @see Handler#sendEmptyMessage(int) */ + boolean sendEmptyMessage(int what); + + /** @see Handler#sendEmptyMessageAtTime(int, long) */ + boolean sendEmptyMessageAtTime(int what, long uptimeMs); + + /** @see Handler#removeMessages(int) */ + void removeMessages(int what); + + /** @see Handler#removeCallbacksAndMessages(Object) */ + void removeCallbacksAndMessages(@Nullable Object token); + + /** @see Handler#post(Runnable) */ + boolean post(Runnable runnable); + + /** @see Handler#postDelayed(Runnable, long) */ + boolean postDelayed(Runnable runnable, long delayMs); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LibraryLoader.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LibraryLoader.java index b1a4fbf5b014..31e582aac516 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LibraryLoader.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LibraryLoader.java @@ -15,11 +15,15 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.util; +import java.util.Arrays; + /** * Configurable loader for native libraries. */ public final class LibraryLoader { + private static final String TAG = "LibraryLoader"; + private String[] nativeLibraries; private boolean loadAttempted; private boolean isAvailable; @@ -54,7 +58,9 @@ public final class LibraryLoader { } isAvailable = true; } catch (UnsatisfiedLinkError exception) { - // Do nothing. + // Log a warning as an attempt to check for the library indicates that the app depends on an + // extension and generally would expect its native libraries to be available. + Log.w(TAG, "Failed to load " + Arrays.toString(nativeLibraries)); } return isAvailable; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Log.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Log.java new file mode 100644 index 000000000000..b6e4a259355a --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Log.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util; + +import android.text.TextUtils; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.UnknownHostException; + +/** Wrapper around {@link android.util.Log} which allows to set the log level. */ +public final class Log { + + /** + * Log level for ExoPlayer logcat logging. One of {@link #LOG_LEVEL_ALL}, {@link #LOG_LEVEL_INFO}, + * {@link #LOG_LEVEL_WARNING}, {@link #LOG_LEVEL_ERROR} or {@link #LOG_LEVEL_OFF}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({LOG_LEVEL_ALL, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR, LOG_LEVEL_OFF}) + @interface LogLevel {} + /** Log level to log all messages. */ + public static final int LOG_LEVEL_ALL = 0; + /** Log level to only log informative, warning and error messages. */ + public static final int LOG_LEVEL_INFO = 1; + /** Log level to only log warning and error messages. */ + public static final int LOG_LEVEL_WARNING = 2; + /** Log level to only log error messages. */ + public static final int LOG_LEVEL_ERROR = 3; + /** Log level to disable all logging. */ + public static final int LOG_LEVEL_OFF = Integer.MAX_VALUE; + + private static int logLevel = LOG_LEVEL_ALL; + private static boolean logStackTraces = true; + + private Log() {} + + /** Returns current {@link LogLevel} for ExoPlayer logcat logging. */ + public static @LogLevel int getLogLevel() { + return logLevel; + } + + /** Returns whether stack traces of {@link Throwable}s will be logged to logcat. */ + public boolean getLogStackTraces() { + return logStackTraces; + } + + /** + * Sets the {@link LogLevel} for ExoPlayer logcat logging. + * + * @param logLevel The new {@link LogLevel}. + */ + public static void setLogLevel(@LogLevel int logLevel) { + Log.logLevel = logLevel; + } + + /** + * Sets whether stack traces of {@link Throwable}s will be logged to logcat. Stack trace logging + * is enabled by default. + * + * @param logStackTraces Whether stack traces will be logged. + */ + public static void setLogStackTraces(boolean logStackTraces) { + Log.logStackTraces = logStackTraces; + } + + /** @see android.util.Log#d(String, String) */ + public static void d(String tag, String message) { + if (logLevel == LOG_LEVEL_ALL) { + android.util.Log.d(tag, message); + } + } + + /** @see android.util.Log#d(String, String, Throwable) */ + public static void d(String tag, String message, @Nullable Throwable throwable) { + d(tag, appendThrowableString(message, throwable)); + } + + /** @see android.util.Log#i(String, String) */ + public static void i(String tag, String message) { + if (logLevel <= LOG_LEVEL_INFO) { + android.util.Log.i(tag, message); + } + } + + /** @see android.util.Log#i(String, String, Throwable) */ + public static void i(String tag, String message, @Nullable Throwable throwable) { + i(tag, appendThrowableString(message, throwable)); + } + + /** @see android.util.Log#w(String, String) */ + public static void w(String tag, String message) { + if (logLevel <= LOG_LEVEL_WARNING) { + android.util.Log.w(tag, message); + } + } + + /** @see android.util.Log#w(String, String, Throwable) */ + public static void w(String tag, String message, @Nullable Throwable throwable) { + w(tag, appendThrowableString(message, throwable)); + } + + /** @see android.util.Log#e(String, String) */ + public static void e(String tag, String message) { + if (logLevel <= LOG_LEVEL_ERROR) { + android.util.Log.e(tag, message); + } + } + + /** @see android.util.Log#e(String, String, Throwable) */ + public static void e(String tag, String message, @Nullable Throwable throwable) { + e(tag, appendThrowableString(message, throwable)); + } + + /** + * Returns a string representation of a {@link Throwable} suitable for logging, taking into + * account whether {@link #setLogStackTraces(boolean)} stack trace logging} is enabled. + * + *

Stack trace logging may be unconditionally suppressed for some expected failure modes (e.g., + * {@link Throwable Throwables} that are expected if the device doesn't have network connectivity) + * to avoid log spam. + * + * @param throwable The {@link Throwable}. + * @return The string representation of the {@link Throwable}. + */ + @Nullable + public static String getThrowableString(@Nullable Throwable throwable) { + if (throwable == null) { + return null; + } else if (isCausedByUnknownHostException(throwable)) { + // UnknownHostException implies the device doesn't have network connectivity. + // UnknownHostException.getMessage() may return a string that's more verbose than desired for + // logging an expected failure mode. Conversely, android.util.Log.getStackTraceString has + // special handling to return the empty string, which can result in logging that doesn't + // indicate the failure mode at all. Hence we special case this exception to always return a + // concise but useful message. + return "UnknownHostException (no network)"; + } else if (!logStackTraces) { + return throwable.getMessage(); + } else { + return android.util.Log.getStackTraceString(throwable).trim().replace("\t", " "); + } + } + + private static String appendThrowableString(String message, @Nullable Throwable throwable) { + @Nullable String throwableString = getThrowableString(throwable); + if (!TextUtils.isEmpty(throwableString)) { + message += "\n " + throwableString.replace("\n", "\n ") + '\n'; + } + return message; + } + + private static boolean isCausedByUnknownHostException(@Nullable Throwable throwable) { + while (throwable != null) { + if (throwable instanceof UnknownHostException) { + return true; + } + throwable = throwable.getCause(); + } + return false; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MediaClock.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MediaClock.java index 974013aa980f..029f3aa8f58c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MediaClock.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MediaClock.java @@ -28,13 +28,12 @@ public interface MediaClock { long getPositionUs(); /** - * Attempts to set the playback parameters and returns the active playback parameters, which may - * differ from those passed in. + * Attempts to set the playback parameters. The media clock may override these parameters if they + * are not supported. * - * @param playbackParameters The playback parameters. - * @return The active playback parameters. + * @param playbackParameters The playback parameters to attempt to set. */ - PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters); + void setPlaybackParameters(PlaybackParameters playbackParameters); /** * Returns the active playback parameters. diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MimeTypes.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MimeTypes.java index 8e3bbd54d135..594a62d63a7c 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MimeTypes.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MimeTypes.java @@ -16,7 +16,9 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.util; import android.text.TextUtils; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import java.util.ArrayList; /** * Defines common MIME types and helper methods. @@ -35,9 +37,13 @@ public final class MimeTypes { public static final String VIDEO_H265 = BASE_TYPE_VIDEO + "/hevc"; public static final String VIDEO_VP8 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp8"; public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9"; + public static final String VIDEO_AV1 = BASE_TYPE_VIDEO + "/av01"; public static final String VIDEO_MP4V = BASE_TYPE_VIDEO + "/mp4v-es"; + public static final String VIDEO_MPEG = BASE_TYPE_VIDEO + "/mpeg"; public static final String VIDEO_MPEG2 = BASE_TYPE_VIDEO + "/mpeg2"; public static final String VIDEO_VC1 = BASE_TYPE_VIDEO + "/wvc1"; + public static final String VIDEO_DIVX = BASE_TYPE_VIDEO + "/divx"; + public static final String VIDEO_DOLBY_VISION = BASE_TYPE_VIDEO + "/dolby-vision"; public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown"; public static final String AUDIO_MP4 = BASE_TYPE_AUDIO + "/mp4"; @@ -48,9 +54,11 @@ public final class MimeTypes { public static final String AUDIO_MPEG_L2 = BASE_TYPE_AUDIO + "/mpeg-L2"; public static final String AUDIO_RAW = BASE_TYPE_AUDIO + "/raw"; public static final String AUDIO_ALAW = BASE_TYPE_AUDIO + "/g711-alaw"; - public static final String AUDIO_ULAW = BASE_TYPE_AUDIO + "/g711-mlaw"; + public static final String AUDIO_MLAW = BASE_TYPE_AUDIO + "/g711-mlaw"; public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + "/eac3"; + public static final String AUDIO_E_AC3_JOC = BASE_TYPE_AUDIO + "/eac3-joc"; + public static final String AUDIO_AC4 = BASE_TYPE_AUDIO + "/ac4"; public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd"; public static final String AUDIO_DTS = BASE_TYPE_AUDIO + "/vnd.dts"; public static final String AUDIO_DTS_HD = BASE_TYPE_AUDIO + "/vnd.dts.hd"; @@ -59,14 +67,19 @@ public final class MimeTypes { public static final String AUDIO_OPUS = BASE_TYPE_AUDIO + "/opus"; public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp"; public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb"; - public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/x-flac"; + public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/flac"; public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac"; + public static final String AUDIO_MSGSM = BASE_TYPE_AUDIO + "/gsm"; + public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; + public static final String TEXT_SSA = BASE_TYPE_TEXT + "/x-ssa"; public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4"; public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm"; + public static final String APPLICATION_MPD = BASE_TYPE_APPLICATION + "/dash+xml"; public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; + public static final String APPLICATION_SS = BASE_TYPE_APPLICATION + "/vnd.ms-sstr+xml"; public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3"; public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608"; public static final String APPLICATION_CEA708 = BASE_TYPE_APPLICATION + "/cea-708"; @@ -82,49 +95,77 @@ public final class MimeTypes { public static final String APPLICATION_CAMERA_MOTION = BASE_TYPE_APPLICATION + "/x-camera-motion"; public static final String APPLICATION_EMSG = BASE_TYPE_APPLICATION + "/x-emsg"; public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs"; + public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif"; + public static final String APPLICATION_ICY = BASE_TYPE_APPLICATION + "/x-icy"; - private MimeTypes() {} + private static final ArrayList customMimeTypes = new ArrayList<>(); /** - * Whether the top-level type of {@code mimeType} is audio. + * Registers a custom MIME type. Most applications do not need to call this method, as handling of + * standard MIME types is built in. These built-in MIME types take precedence over any registered + * via this method. If this method is used, it must be called before creating any player(s). * - * @param mimeType The mimeType to test. - * @return Whether the top level type is audio. + * @param mimeType The custom MIME type to register. + * @param codecPrefix The RFC 6381-style codec string prefix associated with the MIME type. + * @param trackType The {@link C}{@code .TRACK_TYPE_*} constant associated with the MIME type. + * This value is ignored if the top-level type of {@code mimeType} is audio, video or text. */ - public static boolean isAudio(String mimeType) { + public static void registerCustomMimeType(String mimeType, String codecPrefix, int trackType) { + CustomMimeType customMimeType = new CustomMimeType(mimeType, codecPrefix, trackType); + int customMimeTypeCount = customMimeTypes.size(); + for (int i = 0; i < customMimeTypeCount; i++) { + if (mimeType.equals(customMimeTypes.get(i).mimeType)) { + customMimeTypes.remove(i); + break; + } + } + customMimeTypes.add(customMimeType); + } + + /** Returns whether the given string is an audio MIME type. */ + public static boolean isAudio(@Nullable String mimeType) { return BASE_TYPE_AUDIO.equals(getTopLevelType(mimeType)); } - /** - * Whether the top-level type of {@code mimeType} is video. - * - * @param mimeType The mimeType to test. - * @return Whether the top level type is video. - */ - public static boolean isVideo(String mimeType) { + /** Returns whether the given string is a video MIME type. */ + public static boolean isVideo(@Nullable String mimeType) { return BASE_TYPE_VIDEO.equals(getTopLevelType(mimeType)); } - /** - * Whether the top-level type of {@code mimeType} is text. - * - * @param mimeType The mimeType to test. - * @return Whether the top level type is text. - */ - public static boolean isText(String mimeType) { + /** Returns whether the given string is a text MIME type. */ + public static boolean isText(@Nullable String mimeType) { return BASE_TYPE_TEXT.equals(getTopLevelType(mimeType)); } - /** - * Whether the top-level type of {@code mimeType} is application. - * - * @param mimeType The mimeType to test. - * @return Whether the top level type is application. - */ - public static boolean isApplication(String mimeType) { + /** Returns whether the given string is an application MIME type. */ + public static boolean isApplication(@Nullable String mimeType) { return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType)); } + /** + * Returns true if it is known that all samples in a stream of the given sample MIME type are + * guaranteed to be sync samples (i.e., {@link C#BUFFER_FLAG_KEY_FRAME} is guaranteed to be set on + * every sample). + * + * @param mimeType The sample MIME type. + * @return True if it is known that all samples in a stream of the given sample MIME type are + * guaranteed to be sync samples. False otherwise, including if {@code null} is passed. + */ + public static boolean allSamplesAreSyncSamples(@Nullable String mimeType) { + if (mimeType == null) { + return false; + } + // TODO: Consider adding additional audio MIME types here. + switch (mimeType) { + case AUDIO_AAC: + case AUDIO_MPEG: + case AUDIO_MPEG_L1: + case AUDIO_MPEG_L2: + return true; + default: + return false; + } + } /** * Derives a video sample mimeType from a codecs attribute. @@ -132,13 +173,14 @@ public final class MimeTypes { * @param codecs The codecs attribute. * @return The derived video mimeType, or null if it could not be derived. */ - public static String getVideoMediaMimeType(String codecs) { + @Nullable + public static String getVideoMediaMimeType(@Nullable String codecs) { if (codecs == null) { return null; } - String[] codecList = codecs.split(","); + String[] codecList = Util.splitCodecs(codecs); for (String codec : codecList) { - String mimeType = getMediaMimeType(codec); + @Nullable String mimeType = getMediaMimeType(codec); if (mimeType != null && isVideo(mimeType)) { return mimeType; } @@ -152,13 +194,14 @@ public final class MimeTypes { * @param codecs The codecs attribute. * @return The derived audio mimeType, or null if it could not be derived. */ - public static String getAudioMediaMimeType(String codecs) { + @Nullable + public static String getAudioMediaMimeType(@Nullable String codecs) { if (codecs == null) { return null; } - String[] codecList = codecs.split(","); + String[] codecList = Util.splitCodecs(codecs); for (String codec : codecList) { - String mimeType = getMediaMimeType(codec); + @Nullable String mimeType = getMediaMimeType(codec); if (mimeType != null && isAudio(mimeType)) { return mimeType; } @@ -172,25 +215,50 @@ public final class MimeTypes { * @param codec The codec identifier to derive. * @return The mimeType, or null if it could not be derived. */ - public static String getMediaMimeType(String codec) { + @Nullable + public static String getMediaMimeType(@Nullable String codec) { if (codec == null) { return null; } - codec = codec.trim(); + codec = Util.toLowerInvariant(codec.trim()); if (codec.startsWith("avc1") || codec.startsWith("avc3")) { return MimeTypes.VIDEO_H264; } else if (codec.startsWith("hev1") || codec.startsWith("hvc1")) { return MimeTypes.VIDEO_H265; - } else if (codec.startsWith("vp9")) { + } else if (codec.startsWith("dvav") + || codec.startsWith("dva1") + || codec.startsWith("dvhe") + || codec.startsWith("dvh1")) { + return MimeTypes.VIDEO_DOLBY_VISION; + } else if (codec.startsWith("av01")) { + return MimeTypes.VIDEO_AV1; + } else if (codec.startsWith("vp9") || codec.startsWith("vp09")) { return MimeTypes.VIDEO_VP9; - } else if (codec.startsWith("vp8")) { + } else if (codec.startsWith("vp8") || codec.startsWith("vp08")) { return MimeTypes.VIDEO_VP8; } else if (codec.startsWith("mp4a")) { - return MimeTypes.AUDIO_AAC; + @Nullable String mimeType = null; + if (codec.startsWith("mp4a.")) { + String objectTypeString = codec.substring(5); // remove the 'mp4a.' prefix + if (objectTypeString.length() >= 2) { + try { + String objectTypeHexString = Util.toUpperInvariant(objectTypeString.substring(0, 2)); + int objectTypeInt = Integer.parseInt(objectTypeHexString, 16); + mimeType = getMimeTypeFromMp4ObjectType(objectTypeInt); + } catch (NumberFormatException ignored) { + // Ignored. + } + } + } + return mimeType == null ? MimeTypes.AUDIO_AAC : mimeType; } else if (codec.startsWith("ac-3") || codec.startsWith("dac3")) { return MimeTypes.AUDIO_AC3; } else if (codec.startsWith("ec-3") || codec.startsWith("dec3")) { return MimeTypes.AUDIO_E_AC3; + } else if (codec.startsWith("ec+3")) { + return MimeTypes.AUDIO_E_AC3_JOC; + } else if (codec.startsWith("ac-4") || codec.startsWith("dac4")) { + return MimeTypes.AUDIO_AC4; } else if (codec.startsWith("dtsc") || codec.startsWith("dtse")) { return MimeTypes.AUDIO_DTS; } else if (codec.startsWith("dtsh") || codec.startsWith("dtsl")) { @@ -199,19 +267,82 @@ public final class MimeTypes { return MimeTypes.AUDIO_OPUS; } else if (codec.startsWith("vorbis")) { return MimeTypes.AUDIO_VORBIS; + } else if (codec.startsWith("flac")) { + return MimeTypes.AUDIO_FLAC; + } else if (codec.startsWith("stpp")) { + return MimeTypes.APPLICATION_TTML; + } else if (codec.startsWith("wvtt")) { + return MimeTypes.TEXT_VTT; + } else { + return getCustomMimeTypeForCodec(codec); } - return null; } /** - * Returns the {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified mime type. - * {@link C#TRACK_TYPE_UNKNOWN} if the mime type is not known or the mapping cannot be + * Derives a mimeType from MP4 object type identifier, as defined in RFC 6381 and + * https://mp4ra.org/#/object_types. + * + * @param objectType The objectType identifier to derive. + * @return The mimeType, or null if it could not be derived. + */ + @Nullable + public static String getMimeTypeFromMp4ObjectType(int objectType) { + switch (objectType) { + case 0x20: + return MimeTypes.VIDEO_MP4V; + case 0x21: + return MimeTypes.VIDEO_H264; + case 0x23: + return MimeTypes.VIDEO_H265; + case 0x60: + case 0x61: + case 0x62: + case 0x63: + case 0x64: + case 0x65: + return MimeTypes.VIDEO_MPEG2; + case 0x6A: + return MimeTypes.VIDEO_MPEG; + case 0x69: + case 0x6B: + return MimeTypes.AUDIO_MPEG; + case 0xA3: + return MimeTypes.VIDEO_VC1; + case 0xB1: + return MimeTypes.VIDEO_VP9; + case 0x40: + case 0x66: + case 0x67: + case 0x68: + return MimeTypes.AUDIO_AAC; + case 0xA5: + return MimeTypes.AUDIO_AC3; + case 0xA6: + return MimeTypes.AUDIO_E_AC3; + case 0xA9: + case 0xAC: + return MimeTypes.AUDIO_DTS; + case 0xAA: + case 0xAB: + return MimeTypes.AUDIO_DTS_HD; + case 0xAD: + return MimeTypes.AUDIO_OPUS; + case 0xAE: + return MimeTypes.AUDIO_AC4; + default: + return null; + } + } + + /** + * Returns the {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified MIME type. + * {@link C#TRACK_TYPE_UNKNOWN} if the MIME type is not known or the mapping cannot be * established. * - * @param mimeType The mimeType. - * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified mime type. + * @param mimeType The MIME type. + * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified MIME type. */ - public static int getTrackType(String mimeType) { + public static int getTrackType(@Nullable String mimeType) { if (TextUtils.isEmpty(mimeType)) { return C.TRACK_TYPE_UNKNOWN; } else if (isAudio(mimeType)) { @@ -227,11 +358,43 @@ public final class MimeTypes { return C.TRACK_TYPE_TEXT; } else if (APPLICATION_ID3.equals(mimeType) || APPLICATION_EMSG.equals(mimeType) - || APPLICATION_SCTE35.equals(mimeType) - || APPLICATION_CAMERA_MOTION.equals(mimeType)) { + || APPLICATION_SCTE35.equals(mimeType)) { return C.TRACK_TYPE_METADATA; + } else if (APPLICATION_CAMERA_MOTION.equals(mimeType)) { + return C.TRACK_TYPE_CAMERA_MOTION; } else { - return C.TRACK_TYPE_UNKNOWN; + return getTrackTypeForCustomMimeType(mimeType); + } + } + + /** + * Returns the {@link C}{@code .ENCODING_*} constant that corresponds to specified MIME type, if + * it is an encoded (non-PCM) audio format, or {@link C#ENCODING_INVALID} otherwise. + * + * @param mimeType The MIME type. + * @return The {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type, or + * {@link C#ENCODING_INVALID}. + */ + public static @C.Encoding int getEncoding(String mimeType) { + switch (mimeType) { + case MimeTypes.AUDIO_MPEG: + return C.ENCODING_MP3; + case MimeTypes.AUDIO_AC3: + return C.ENCODING_AC3; + case MimeTypes.AUDIO_E_AC3: + return C.ENCODING_E_AC3; + case MimeTypes.AUDIO_E_AC3_JOC: + return C.ENCODING_E_AC3_JOC; + case MimeTypes.AUDIO_AC4: + return C.ENCODING_AC4; + case MimeTypes.AUDIO_DTS: + return C.ENCODING_DTS; + case MimeTypes.AUDIO_DTS_HD: + return C.ENCODING_DTS_HD; + case MimeTypes.AUDIO_TRUEHD: + return C.ENCODING_DOLBY_TRUEHD; + default: + return C.ENCODING_INVALID; } } @@ -246,20 +409,57 @@ public final class MimeTypes { } /** - * Returns the top-level type of {@code mimeType}. - * - * @param mimeType The mimeType whose top-level type is required. - * @return The top-level type, or null if the mimeType is null. + * Returns the top-level type of {@code mimeType}, or null if {@code mimeType} is null or does not + * contain a forward slash character ({@code '/'}). */ - private static String getTopLevelType(String mimeType) { + @Nullable + private static String getTopLevelType(@Nullable String mimeType) { if (mimeType == null) { return null; } int indexOfSlash = mimeType.indexOf('/'); if (indexOfSlash == -1) { - throw new IllegalArgumentException("Invalid mime type: " + mimeType); + return null; } return mimeType.substring(0, indexOfSlash); } + @Nullable + private static String getCustomMimeTypeForCodec(String codec) { + int customMimeTypeCount = customMimeTypes.size(); + for (int i = 0; i < customMimeTypeCount; i++) { + CustomMimeType customMimeType = customMimeTypes.get(i); + if (codec.startsWith(customMimeType.codecPrefix)) { + return customMimeType.mimeType; + } + } + return null; + } + + private static int getTrackTypeForCustomMimeType(String mimeType) { + int customMimeTypeCount = customMimeTypes.size(); + for (int i = 0; i < customMimeTypeCount; i++) { + CustomMimeType customMimeType = customMimeTypes.get(i); + if (mimeType.equals(customMimeType.mimeType)) { + return customMimeType.trackType; + } + } + return C.TRACK_TYPE_UNKNOWN; + } + + private MimeTypes() { + // Prevent instantiation. + } + + private static final class CustomMimeType { + public final String mimeType; + public final String codecPrefix; + public final int trackType; + + public CustomMimeType(String mimeType, String codecPrefix, int trackType) { + this.mimeType = mimeType; + this.codecPrefix = codecPrefix; + this.trackType = trackType; + } + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NalUnitUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NalUnitUtil.java index 95a03bf7e9f2..d7409daa66d8 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NalUnitUtil.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NalUnitUtil.java @@ -15,7 +15,6 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.util; -import android.util.Log; import java.nio.ByteBuffer; import java.util.Arrays; @@ -31,6 +30,9 @@ public final class NalUnitUtil { */ public static final class SpsData { + public final int profileIdc; + public final int constraintsFlagsAndReservedZero2Bits; + public final int levelIdc; public final int seqParameterSetId; public final int width; public final int height; @@ -42,9 +44,23 @@ public final class NalUnitUtil { public final int picOrderCntLsbLength; public final boolean deltaPicOrderAlwaysZeroFlag; - public SpsData(int seqParameterSetId, int width, int height, float pixelWidthAspectRatio, - boolean separateColorPlaneFlag, boolean frameMbsOnlyFlag, int frameNumLength, - int picOrderCountType, int picOrderCntLsbLength, boolean deltaPicOrderAlwaysZeroFlag) { + public SpsData( + int profileIdc, + int constraintsFlagsAndReservedZero2Bits, + int levelIdc, + int seqParameterSetId, + int width, + int height, + float pixelWidthAspectRatio, + boolean separateColorPlaneFlag, + boolean frameMbsOnlyFlag, + int frameNumLength, + int picOrderCountType, + int picOrderCntLsbLength, + boolean deltaPicOrderAlwaysZeroFlag) { + this.profileIdc = profileIdc; + this.constraintsFlagsAndReservedZero2Bits = constraintsFlagsAndReservedZero2Bits; + this.levelIdc = levelIdc; this.seqParameterSetId = seqParameterSetId; this.width = width; this.height = height; @@ -251,7 +267,8 @@ public final class NalUnitUtil { ParsableNalUnitBitArray data = new ParsableNalUnitBitArray(nalData, nalOffset, nalLimit); data.skipBits(8); // nal_unit int profileIdc = data.readBits(8); - data.skipBits(16); // constraint bits (6), reserved (2) and level_idc (8) + int constraintsFlagsAndReservedZero2Bits = data.readBits(8); + int levelIdc = data.readBits(8); int seqParameterSetId = data.readUnsignedExpGolombCodedInt(); int chromaFormatIdc = 1; // Default is 4:2:0 @@ -265,7 +282,7 @@ public final class NalUnitUtil { } data.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8 data.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8 - data.skipBits(1); // qpprime_y_zero_transform_bypass_flag + data.skipBit(); // qpprime_y_zero_transform_bypass_flag boolean seqScalingMatrixPresentFlag = data.readBit(); if (seqScalingMatrixPresentFlag) { int limit = (chromaFormatIdc != 3) ? 8 : 12; @@ -295,17 +312,17 @@ public final class NalUnitUtil { } } data.readUnsignedExpGolombCodedInt(); // max_num_ref_frames - data.skipBits(1); // gaps_in_frame_num_value_allowed_flag + data.skipBit(); // gaps_in_frame_num_value_allowed_flag int picWidthInMbs = data.readUnsignedExpGolombCodedInt() + 1; int picHeightInMapUnits = data.readUnsignedExpGolombCodedInt() + 1; boolean frameMbsOnlyFlag = data.readBit(); int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits; if (!frameMbsOnlyFlag) { - data.skipBits(1); // mb_adaptive_frame_field_flag + data.skipBit(); // mb_adaptive_frame_field_flag } - data.skipBits(1); // direct_8x8_inference_flag + data.skipBit(); // direct_8x8_inference_flag int frameWidth = picWidthInMbs * 16; int frameHeight = frameHeightInMbs * 16; boolean frameCroppingFlag = data.readBit(); @@ -349,9 +366,20 @@ public final class NalUnitUtil { } } - return new SpsData(seqParameterSetId, frameWidth, frameHeight, pixelWidthHeightRatio, - separateColorPlaneFlag, frameMbsOnlyFlag, frameNumLength, picOrderCntType, - picOrderCntLsbLength, deltaPicOrderAlwaysZeroFlag); + return new SpsData( + profileIdc, + constraintsFlagsAndReservedZero2Bits, + levelIdc, + seqParameterSetId, + frameWidth, + frameHeight, + pixelWidthHeightRatio, + separateColorPlaneFlag, + frameMbsOnlyFlag, + frameNumLength, + picOrderCntType, + picOrderCntLsbLength, + deltaPicOrderAlwaysZeroFlag); } /** @@ -368,7 +396,7 @@ public final class NalUnitUtil { data.skipBits(8); // nal_unit int picParameterSetId = data.readUnsignedExpGolombCodedInt(); int seqParameterSetId = data.readUnsignedExpGolombCodedInt(); - data.skipBits(1); // entropy_coding_mode_flag + data.skipBit(); // entropy_coding_mode_flag boolean bottomFieldPicOrderInFramePresentFlag = data.readBit(); return new PpsData(picParameterSetId, seqParameterSetId, bottomFieldPicOrderInFramePresentFlag); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NonNullApi.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NonNullApi.java new file mode 100644 index 000000000000..0c9b9b21824e --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NonNullApi.java @@ -0,0 +1,34 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import javax.annotation.Nonnull; +import javax.annotation.meta.TypeQualifierDefault; +import kotlin.annotations.jvm.MigrationStatus; +import kotlin.annotations.jvm.UnderMigration; + +/** + * Annotation to declare all type usages in the annotated instance as {@link Nonnull}, unless + * explicitly marked with a nullable annotation. + */ +@Nonnull +@TypeQualifierDefault(ElementType.TYPE_USE) +@UnderMigration(status = MigrationStatus.STRICT) +@Retention(RetentionPolicy.CLASS) +public @interface NonNullApi {} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NotificationUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NotificationUtil.java new file mode 100644 index 000000000000..df68c8fe59b8 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NotificationUtil.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Utility methods for displaying {@link Notification Notifications}. */ +@SuppressLint("InlinedApi") +public final class NotificationUtil { + + /** + * Notification channel importance levels. One of {@link #IMPORTANCE_UNSPECIFIED}, {@link + * #IMPORTANCE_NONE}, {@link #IMPORTANCE_MIN}, {@link #IMPORTANCE_LOW}, {@link + * #IMPORTANCE_DEFAULT} or {@link #IMPORTANCE_HIGH}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + IMPORTANCE_UNSPECIFIED, + IMPORTANCE_NONE, + IMPORTANCE_MIN, + IMPORTANCE_LOW, + IMPORTANCE_DEFAULT, + IMPORTANCE_HIGH + }) + public @interface Importance {} + /** @see NotificationManager#IMPORTANCE_UNSPECIFIED */ + public static final int IMPORTANCE_UNSPECIFIED = NotificationManager.IMPORTANCE_UNSPECIFIED; + /** @see NotificationManager#IMPORTANCE_NONE */ + public static final int IMPORTANCE_NONE = NotificationManager.IMPORTANCE_NONE; + /** @see NotificationManager#IMPORTANCE_MIN */ + public static final int IMPORTANCE_MIN = NotificationManager.IMPORTANCE_MIN; + /** @see NotificationManager#IMPORTANCE_LOW */ + public static final int IMPORTANCE_LOW = NotificationManager.IMPORTANCE_LOW; + /** @see NotificationManager#IMPORTANCE_DEFAULT */ + public static final int IMPORTANCE_DEFAULT = NotificationManager.IMPORTANCE_DEFAULT; + /** @see NotificationManager#IMPORTANCE_HIGH */ + public static final int IMPORTANCE_HIGH = NotificationManager.IMPORTANCE_HIGH; + + /** @deprecated Use {@link #createNotificationChannel(Context, String, int, int, int)}. */ + @Deprecated + public static void createNotificationChannel( + Context context, String id, @StringRes int nameResourceId, @Importance int importance) { + createNotificationChannel( + context, id, nameResourceId, /* descriptionResourceId= */ 0, importance); + } + + /** + * Creates a notification channel that notifications can be posted to. See {@link + * NotificationChannel} and {@link + * NotificationManager#createNotificationChannel(NotificationChannel)} for details. + * + * @param context A {@link Context}. + * @param id The id of the channel. Must be unique per package. The value may be truncated if it's + * too long. + * @param nameResourceId A string resource identifier for the user visible name of the channel. + * The recommended maximum length is 40 characters. The string may be truncated if it's too + * long. You can rename the channel when the system locale changes by listening for the {@link + * Intent#ACTION_LOCALE_CHANGED} broadcast. + * @param descriptionResourceId A string resource identifier for the user visible description of + * the channel, or 0 if no description is provided. The recommended maximum length is 300 + * characters. The value may be truncated if it is too long. You can change the description of + * the channel when the system locale changes by listening for the {@link + * Intent#ACTION_LOCALE_CHANGED} broadcast. + * @param importance The importance of the channel. This controls how interruptive notifications + * posted to this channel are. One of {@link #IMPORTANCE_UNSPECIFIED}, {@link + * #IMPORTANCE_NONE}, {@link #IMPORTANCE_MIN}, {@link #IMPORTANCE_LOW}, {@link + * #IMPORTANCE_DEFAULT} and {@link #IMPORTANCE_HIGH}. + */ + public static void createNotificationChannel( + Context context, + String id, + @StringRes int nameResourceId, + @StringRes int descriptionResourceId, + @Importance int importance) { + if (Util.SDK_INT >= 26) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + NotificationChannel channel = + new NotificationChannel(id, context.getString(nameResourceId), importance); + if (descriptionResourceId != 0) { + channel.setDescription(context.getString(descriptionResourceId)); + } + notificationManager.createNotificationChannel(channel); + } + } + + /** + * Post a notification to be shown in the status bar. If a notification with the same id has + * already been posted by your application and has not yet been canceled, it will be replaced by + * the updated information. If {@code notification} is {@code null} then any notification + * previously shown with the specified id will be cancelled. + * + * @param context A {@link Context}. + * @param id The notification id. + * @param notification The {@link Notification} to post, or {@code null} to cancel a previously + * shown notification. + */ + public static void setNotification(Context context, int id, @Nullable Notification notification) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (notification != null) { + notificationManager.notify(id, notification); + } else { + notificationManager.cancel(id); + } + } + + private NotificationUtil() {} +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableBitArray.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableBitArray.java index d257323f03f2..3d6a7027232b 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableBitArray.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableBitArray.java @@ -28,10 +28,10 @@ public final class ParsableBitArray { private int bitOffset; private int byteLimit; - /** - * Creates a new instance that initially has no backing data. - */ - public ParsableBitArray() {} + /** Creates a new instance that initially has no backing data. */ + public ParsableBitArray() { + data = Util.EMPTY_BYTE_ARRAY; + } /** * Creates a new instance that wraps an existing array. @@ -62,6 +62,17 @@ public final class ParsableBitArray { reset(data, data.length); } + /** + * Sets this instance's data, position and limit to match the provided {@code parsableByteArray}. + * Any modifications to the underlying data array will be visible in both instances + * + * @param parsableByteArray The {@link ParsableByteArray}. + */ + public void reset(ParsableByteArray parsableByteArray) { + reset(parsableByteArray.data, parsableByteArray.limit()); + setPosition(parsableByteArray.getPosition() * 8); + } + /** * Updates the instance to wrap {@code data}, and resets the position to zero. * @@ -110,14 +121,26 @@ public final class ParsableBitArray { assertValidOffset(); } + /** + * Skips a single bit. + */ + public void skipBit() { + if (++bitOffset == 8) { + bitOffset = 0; + byteOffset++; + } + assertValidOffset(); + } + /** * Skips bits and moves current reading position forward. * - * @param n The number of bits to skip. + * @param numBits The number of bits to skip. */ - public void skipBits(int n) { - byteOffset += (n / 8); - bitOffset += (n % 8); + public void skipBits(int numBits) { + int numBytes = numBits / 8; + byteOffset += numBytes; + bitOffset += numBits - (numBytes * 8); if (bitOffset > 7) { byteOffset++; bitOffset -= 8; @@ -131,62 +154,88 @@ public final class ParsableBitArray { * @return Whether the bit is set. */ public boolean readBit() { - return readBits(1) == 1; + boolean returnValue = (data[byteOffset] & (0x80 >> bitOffset)) != 0; + skipBit(); + return returnValue; } /** * Reads up to 32 bits. * * @param numBits The number of bits to read. - * @return An integer whose bottom n bits hold the read data. + * @return An integer whose bottom {@code numBits} bits hold the read data. */ public int readBits(int numBits) { if (numBits == 0) { return 0; } - int returnValue = 0; - - // Read as many whole bytes as we can. - int wholeBytes = (numBits / 8); - for (int i = 0; i < wholeBytes; i++) { - int byteValue; - if (bitOffset != 0) { - byteValue = ((data[byteOffset] & 0xFF) << bitOffset) - | ((data[byteOffset + 1] & 0xFF) >>> (8 - bitOffset)); - } else { - byteValue = data[byteOffset]; - } - numBits -= 8; - returnValue |= (byteValue & 0xFF) << numBits; + bitOffset += numBits; + while (bitOffset > 8) { + bitOffset -= 8; + returnValue |= (data[byteOffset++] & 0xFF) << bitOffset; + } + returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset); + returnValue &= 0xFFFFFFFF >>> (32 - numBits); + if (bitOffset == 8) { + bitOffset = 0; byteOffset++; } - - // Read any remaining bits. - if (numBits > 0) { - int nextBit = bitOffset + numBits; - byte writeMask = (byte) (0xFF >> (8 - numBits)); - - if (nextBit > 8) { - // Combine bits from current byte and next byte. - returnValue |= ((((data[byteOffset] & 0xFF) << (nextBit - 8) - | ((data[byteOffset + 1] & 0xFF) >> (16 - nextBit))) & writeMask)); - byteOffset++; - } else { - // Bits to be read only within current byte. - returnValue |= (((data[byteOffset] & 0xFF) >> (8 - nextBit)) & writeMask); - if (nextBit == 8) { - byteOffset++; - } - } - - bitOffset = nextBit % 8; - } - assertValidOffset(); return returnValue; } + /** + * Reads up to 64 bits. + * + * @param numBits The number of bits to read. + * @return A long whose bottom {@code numBits} bits hold the read data. + */ + public long readBitsToLong(int numBits) { + if (numBits <= 32) { + return Util.toUnsignedLong(readBits(numBits)); + } + return Util.toLong(readBits(numBits - 32), readBits(32)); + } + + /** + * Reads {@code numBits} bits into {@code buffer}. + * + * @param buffer The array into which the read data should be written. The trailing {@code numBits + * % 8} bits are written into the most significant bits of the last modified {@code buffer} + * byte. The remaining ones are unmodified. + * @param offset The offset in {@code buffer} at which the read data should be written. + * @param numBits The number of bits to read. + */ + public void readBits(byte[] buffer, int offset, int numBits) { + // Whole bytes. + int to = offset + (numBits >> 3) /* numBits / 8 */; + for (int i = offset; i < to; i++) { + buffer[i] = (byte) (data[byteOffset++] << bitOffset); + buffer[i] = (byte) (buffer[i] | ((data[byteOffset] & 0xFF) >> (8 - bitOffset))); + } + // Trailing bits. + int bitsLeft = numBits & 7 /* numBits % 8 */; + if (bitsLeft == 0) { + return; + } + // Set bits that are going to be overwritten to 0. + buffer[to] = (byte) (buffer[to] & (0xFF >> bitsLeft)); + if (bitOffset + bitsLeft > 8) { + // We read the rest of data[byteOffset] and increase byteOffset. + buffer[to] = (byte) (buffer[to] | ((data[byteOffset++] & 0xFF) << bitOffset)); + bitOffset -= 8; + } + bitOffset += bitsLeft; + int lastDataByteTrailingBits = (data[byteOffset] & 0xFF) >> (8 - bitOffset); + buffer[to] |= (byte) (lastDataByteTrailingBits << (8 - bitsLeft)); + if (bitOffset == 8) { + bitOffset = 0; + byteOffset++; + } + assertValidOffset(); + } + /** * Aligns the position to the next byte boundary. Does nothing if the position is already aligned. */ @@ -228,10 +277,46 @@ public final class ParsableBitArray { assertValidOffset(); } + /** + * Overwrites {@code numBits} from this array using the {@code numBits} least significant bits + * from {@code value}. Bits are written in order from most significant to least significant. The + * read position is advanced by {@code numBits}. + * + * @param value The integer whose {@code numBits} least significant bits are written into {@link + * #data}. + * @param numBits The number of bits to write. + */ + public void putInt(int value, int numBits) { + int remainingBitsToRead = numBits; + if (numBits < 32) { + value &= (1 << numBits) - 1; + } + int firstByteReadSize = Math.min(8 - bitOffset, numBits); + int firstByteRightPaddingSize = 8 - bitOffset - firstByteReadSize; + int firstByteBitmask = (0xFF00 >> bitOffset) | ((1 << firstByteRightPaddingSize) - 1); + data[byteOffset] = (byte) (data[byteOffset] & firstByteBitmask); + int firstByteInputBits = value >>> (numBits - firstByteReadSize); + data[byteOffset] = + (byte) (data[byteOffset] | (firstByteInputBits << firstByteRightPaddingSize)); + remainingBitsToRead -= firstByteReadSize; + int currentByteIndex = byteOffset + 1; + while (remainingBitsToRead > 8) { + data[currentByteIndex++] = (byte) (value >>> (remainingBitsToRead - 8)); + remainingBitsToRead -= 8; + } + int lastByteRightPaddingSize = 8 - remainingBitsToRead; + data[currentByteIndex] = + (byte) (data[currentByteIndex] & ((1 << lastByteRightPaddingSize) - 1)); + int lastByteInput = value & ((1 << remainingBitsToRead) - 1); + data[currentByteIndex] = + (byte) (data[currentByteIndex] | (lastByteInput << lastByteRightPaddingSize)); + skipBits(numBits); + assertValidOffset(); + } + private void assertValidOffset() { // It is fine for position to be at the end of the array, but no further. Assertions.checkState(byteOffset >= 0 - && (bitOffset >= 0 && bitOffset < 8) && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0))); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableByteArray.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableByteArray.java index cc1806741b6a..9ad9dd1aa7f8 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -15,6 +15,8 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.util; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -29,10 +31,10 @@ public final class ParsableByteArray { private int position; private int limit; - /** - * Creates a new instance that initially has no backing data. - */ - public ParsableByteArray() {} + /** Creates a new instance that initially has no backing data. */ + public ParsableByteArray() { + data = Util.EMPTY_BYTE_ARRAY; + } /** * Creates a new instance with {@code limit} bytes and sets the limit. @@ -65,6 +67,12 @@ public final class ParsableByteArray { this.limit = limit; } + /** Sets the position and limit to zero. */ + public void reset() { + position = 0; + limit = 0; + } + /** * Resets the position to zero and the limit to the specified value. If the limit exceeds the * capacity, {@code data} is replaced with a new array of sufficient size. @@ -75,6 +83,16 @@ public final class ParsableByteArray { reset(capacity() < limit ? new byte[limit] : data, limit); } + /** + * Updates the instance to wrap {@code data}, and resets the position to zero and the limit to + * {@code data.length}. + * + * @param data The array to wrap. + */ + public void reset(byte[] data) { + reset(data, data.length); + } + /** * Updates the instance to wrap {@code data}, and resets the position to zero. * @@ -87,14 +105,6 @@ public final class ParsableByteArray { position = 0; } - /** - * Sets the position and limit to zero. - */ - public void reset() { - position = 0; - limit = 0; - } - /** * Returns the number of bytes yet to be read. */ @@ -130,7 +140,7 @@ public final class ParsableByteArray { * Returns the capacity of the array, which may be larger than the limit. */ public int capacity() { - return data == null ? 0 : data.length; + return data.length; } /** @@ -232,7 +242,7 @@ public final class ParsableByteArray { } /** - * Reads the next two bytes as an signed value. + * Reads the next two bytes as a signed value. */ public short readShort() { return (short) ((data[position++] & 0xFF) << 8 @@ -255,6 +265,15 @@ public final class ParsableByteArray { | (data[position++] & 0xFF); } + /** + * Reads the next three bytes as a signed value. + */ + public int readInt24() { + return ((data[position++] & 0xFF) << 24) >> 8 + | (data[position++] & 0xFF) << 8 + | (data[position++] & 0xFF); + } + /** * Reads the next three bytes as a signed value in little endian order. */ @@ -304,7 +323,7 @@ public final class ParsableByteArray { } /** - * Reads the next four bytes as an signed value in little endian order. + * Reads the next four bytes as a signed value in little endian order. */ public int readLittleEndianInt() { return (data[position++] & 0xFF) @@ -428,7 +447,7 @@ public final class ParsableByteArray { * @return The string encoded by the bytes. */ public String readString(int length) { - return readString(length, Charset.defaultCharset()); + return readString(length, Charset.forName(C.UTF8_NAME)); } /** @@ -460,7 +479,7 @@ public final class ParsableByteArray { if (lastIndex < limit && data[lastIndex] == 0) { stringLength--; } - String result = new String(data, position, stringLength); + String result = Util.fromUtf8Bytes(data, position, stringLength); position += length; return result; } @@ -471,6 +490,7 @@ public final class ParsableByteArray { * @return The string not including any terminating NUL byte, or null if the end of the data has * already been reached. */ + @Nullable public String readNullTerminatedString() { if (bytesLeft() == 0) { return null; @@ -479,7 +499,7 @@ public final class ParsableByteArray { while (stringLimit < limit && data[stringLimit] != 0) { stringLimit++; } - String string = new String(data, position, stringLimit - position); + String string = Util.fromUtf8Bytes(data, position, stringLimit - position); position = stringLimit; if (position < limit) { position++; @@ -489,14 +509,15 @@ public final class ParsableByteArray { /** * Reads a line of text. - *

- * A line is considered to be terminated by any one of a carriage return ('\r'), a line feed + * + *

A line is considered to be terminated by any one of a carriage return ('\r'), a line feed * ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). The system's default - * charset (UTF-8) is used. + * charset (UTF-8) is used. This method discards leading UTF-8 byte order marks, if present. * * @return The line not including any line-termination characters, or null if the end of the data * has already been reached. */ + @Nullable public String readLine() { if (bytesLeft() == 0) { return null; @@ -507,10 +528,10 @@ public final class ParsableByteArray { } if (lineLimit - position >= 3 && data[position] == (byte) 0xEF && data[position + 1] == (byte) 0xBB && data[position + 2] == (byte) 0xBF) { - // There's a byte order mark at the start of the line. Discard it. + // There's a UTF-8 byte order mark at the start of the line. Discard it. position += 3; } - String line = new String(data, position, lineLimit - position); + String line = Util.fromUtf8Bytes(data, position, lineLimit - position); position = lineLimit; if (position == limit) { return line; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java index 140551841b56..e73404fd910b 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java @@ -35,6 +35,7 @@ public final class ParsableNalUnitBitArray { * @param offset The byte offset in {@code data} to start reading from. * @param limit The byte offset of the end of the bitstream in {@code data}. */ + @SuppressWarnings({"initialization.fields.uninitialized", "method.invocation.invalid"}) public ParsableNalUnitBitArray(byte[] data, int offset, int limit) { reset(data, offset, limit); } @@ -54,15 +55,27 @@ public final class ParsableNalUnitBitArray { assertValidOffset(); } + /** + * Skips a single bit. + */ + public void skipBit() { + if (++bitOffset == 8) { + bitOffset = 0; + byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1; + } + assertValidOffset(); + } + /** * Skips bits and moves current reading position forward. * - * @param n The number of bits to skip. + * @param numBits The number of bits to skip. */ - public void skipBits(int n) { + public void skipBits(int numBits) { int oldByteOffset = byteOffset; - byteOffset += (n / 8); - bitOffset += (n % 8); + int numBytes = numBits / 8; + byteOffset += numBytes; + bitOffset += numBits - (numBytes * 8); if (bitOffset > 7) { byteOffset++; bitOffset -= 8; @@ -81,13 +94,14 @@ public final class ParsableNalUnitBitArray { * Returns whether it's possible to read {@code n} bits starting from the current offset. The * offset is not modified. * - * @param n The number of bits. + * @param numBits The number of bits. * @return Whether it is possible to read {@code n} bits. */ - public boolean canReadBits(int n) { + public boolean canReadBits(int numBits) { int oldByteOffset = byteOffset; - int newByteOffset = byteOffset + (n / 8); - int newBitOffset = bitOffset + (n % 8); + int numBytes = numBits / 8; + int newByteOffset = byteOffset + numBytes; + int newBitOffset = bitOffset + numBits - (numBytes * 8); if (newBitOffset > 7) { newByteOffset++; newBitOffset -= 8; @@ -108,7 +122,9 @@ public final class ParsableNalUnitBitArray { * @return Whether the bit is set. */ public boolean readBit() { - return readBits(1) == 1; + boolean returnValue = (data[byteOffset] & (0x80 >> bitOffset)) != 0; + skipBit(); + return returnValue; } /** @@ -118,50 +134,19 @@ public final class ParsableNalUnitBitArray { * @return An integer whose bottom n bits hold the read data. */ public int readBits(int numBits) { - if (numBits == 0) { - return 0; - } - int returnValue = 0; - - // Read as many whole bytes as we can. - int wholeBytes = (numBits / 8); - for (int i = 0; i < wholeBytes; i++) { - int nextByteOffset = shouldSkipByte(byteOffset + 1) ? byteOffset + 2 : byteOffset + 1; - int byteValue; - if (bitOffset != 0) { - byteValue = ((data[byteOffset] & 0xFF) << bitOffset) - | ((data[nextByteOffset] & 0xFF) >>> (8 - bitOffset)); - } else { - byteValue = data[byteOffset]; - } - numBits -= 8; - returnValue |= (byteValue & 0xFF) << numBits; - byteOffset = nextByteOffset; + bitOffset += numBits; + while (bitOffset > 8) { + bitOffset -= 8; + returnValue |= (data[byteOffset] & 0xFF) << bitOffset; + byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1; } - - // Read any remaining bits. - if (numBits > 0) { - int nextBit = bitOffset + numBits; - byte writeMask = (byte) (0xFF >> (8 - numBits)); - int nextByteOffset = shouldSkipByte(byteOffset + 1) ? byteOffset + 2 : byteOffset + 1; - - if (nextBit > 8) { - // Combine bits from current byte and next byte. - returnValue |= ((((data[byteOffset] & 0xFF) << (nextBit - 8) - | ((data[nextByteOffset] & 0xFF) >> (16 - nextBit))) & writeMask)); - byteOffset = nextByteOffset; - } else { - // Bits to be read only within current byte. - returnValue |= (((data[byteOffset] & 0xFF) >> (8 - nextBit)) & writeMask); - if (nextBit == 8) { - byteOffset = nextByteOffset; - } - } - - bitOffset = nextBit % 8; + returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset); + returnValue &= 0xFFFFFFFF >>> (32 - numBits); + if (bitOffset == 8) { + bitOffset = 0; + byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1; } - assertValidOffset(); return returnValue; } @@ -220,7 +205,6 @@ public final class ParsableNalUnitBitArray { private void assertValidOffset() { // It is fine for position to be at the end of the array, but no further. Assertions.checkState(byteOffset >= 0 - && (bitOffset >= 0 && bitOffset < 8) && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0))); } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Predicate.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Predicate.java index e938cbab3b36..d91d9f725488 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Predicate.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Predicate.java @@ -16,7 +16,7 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.util; /** - * Determines a true of false value for a given input. + * Determines a true or false value for a given input. * * @param The input type of the predicate. */ diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/PriorityTaskManager.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/PriorityTaskManager.java index d1fc27dad051..1067014b40ed 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/PriorityTaskManager.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/PriorityTaskManager.java @@ -111,7 +111,7 @@ public final class PriorityTaskManager { public void remove(int priority) { synchronized (lock) { queue.remove(priority); - highestPriority = queue.isEmpty() ? Integer.MIN_VALUE : queue.peek(); + highestPriority = queue.isEmpty() ? Integer.MIN_VALUE : Util.castNonNull(queue.peek()); lock.notifyAll(); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/RepeatModeUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/RepeatModeUtil.java new file mode 100644 index 000000000000..c4964e6848ba --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/RepeatModeUtil.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util; + +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Player; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Util class for repeat mode handling. + */ +public final class RepeatModeUtil { + + // LINT.IfChange + /** + * Set of repeat toggle modes. Can be combined using bit-wise operations. Possible flag values are + * {@link #REPEAT_TOGGLE_MODE_NONE}, {@link #REPEAT_TOGGLE_MODE_ONE} and {@link + * #REPEAT_TOGGLE_MODE_ALL}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {REPEAT_TOGGLE_MODE_NONE, REPEAT_TOGGLE_MODE_ONE, REPEAT_TOGGLE_MODE_ALL}) + public @interface RepeatToggleModes {} + /** + * All repeat mode buttons disabled. + */ + public static final int REPEAT_TOGGLE_MODE_NONE = 0; + /** + * "Repeat One" button enabled. + */ + public static final int REPEAT_TOGGLE_MODE_ONE = 1; + /** "Repeat All" button enabled. */ + public static final int REPEAT_TOGGLE_MODE_ALL = 1 << 1; // 2 + // LINT.ThenChange(../../../../../../../../../ui/src/main/res/values/attrs.xml) + + private RepeatModeUtil() { + // Prevent instantiation. + } + + /** + * Gets the next repeat mode out of {@code enabledModes} starting from {@code currentMode}. + * + * @param currentMode The current repeat mode. + * @param enabledModes Bitmask of enabled modes. + * @return The next repeat mode. + */ + public static @Player.RepeatMode int getNextRepeatMode(@Player.RepeatMode int currentMode, + int enabledModes) { + for (int offset = 1; offset <= 2; offset++) { + @Player.RepeatMode int proposedMode = (currentMode + offset) % 3; + if (isRepeatModeEnabled(proposedMode, enabledModes)) { + return proposedMode; + } + } + return currentMode; + } + + /** + * Verifies whether a given {@code repeatMode} is enabled in the bitmask {@code enabledModes}. + * + * @param repeatMode The mode to check. + * @param enabledModes The bitmask representing the enabled modes. + * @return {@code true} if enabled. + */ + public static boolean isRepeatModeEnabled(@Player.RepeatMode int repeatMode, int enabledModes) { + switch (repeatMode) { + case Player.REPEAT_MODE_OFF: + return true; + case Player.REPEAT_MODE_ONE: + return (enabledModes & REPEAT_TOGGLE_MODE_ONE) != 0; + case Player.REPEAT_MODE_ALL: + return (enabledModes & REPEAT_TOGGLE_MODE_ALL) != 0; + default: + return false; + } + } + +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SlidingPercentile.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SlidingPercentile.java index 4adb81d166f0..9048de2f3497 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SlidingPercentile.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SlidingPercentile.java @@ -35,19 +35,9 @@ import java.util.Comparator; public class SlidingPercentile { // Orderings. - private static final Comparator INDEX_COMPARATOR = new Comparator() { - @Override - public int compare(Sample a, Sample b) { - return a.index - b.index; - } - }; - - private static final Comparator VALUE_COMPARATOR = new Comparator() { - @Override - public int compare(Sample a, Sample b) { - return a.value < b.value ? -1 : b.value < a.value ? 1 : 0; - } - }; + private static final Comparator INDEX_COMPARATOR = (a, b) -> a.index - b.index; + private static final Comparator VALUE_COMPARATOR = + (a, b) -> Float.compare(a.value, b.value); private static final int SORT_ORDER_NONE = -1; private static final int SORT_ORDER_BY_VALUE = 0; @@ -75,6 +65,14 @@ public class SlidingPercentile { currentSortOrder = SORT_ORDER_NONE; } + /** Resets the sliding percentile. */ + public void reset() { + samples.clear(); + currentSortOrder = SORT_ORDER_NONE; + nextSampleIndex = 0; + totalWeight = 0; + } + /** * Adds a new weighted value. * diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/StandaloneMediaClock.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/StandaloneMediaClock.java index 5acb4fa08eb6..f72867694d49 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/StandaloneMediaClock.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/StandaloneMediaClock.java @@ -15,7 +15,6 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.util; -import android.os.SystemClock; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters; @@ -25,16 +24,21 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters; */ public final class StandaloneMediaClock implements MediaClock { + private final Clock clock; + private boolean started; private long baseUs; private long baseElapsedMs; private PlaybackParameters playbackParameters; /** - * Creates a new standalone media clock. + * Creates a new standalone media clock using the given {@link Clock} implementation. + * + * @param clock A {@link Clock}. */ - public StandaloneMediaClock() { - playbackParameters = PlaybackParameters.DEFAULT; + public StandaloneMediaClock(Clock clock) { + this.clock = clock; + this.playbackParameters = PlaybackParameters.DEFAULT; } /** @@ -42,7 +46,7 @@ public final class StandaloneMediaClock implements MediaClock { */ public void start() { if (!started) { - baseElapsedMs = SystemClock.elapsedRealtime(); + baseElapsedMs = clock.elapsedRealtime(); started = true; } } @@ -52,55 +56,44 @@ public final class StandaloneMediaClock implements MediaClock { */ public void stop() { if (started) { - setPositionUs(getPositionUs()); + resetPosition(getPositionUs()); started = false; } } /** - * Sets the clock's position. + * Resets the clock's position. * * @param positionUs The position to set in microseconds. */ - public void setPositionUs(long positionUs) { + public void resetPosition(long positionUs) { baseUs = positionUs; if (started) { - baseElapsedMs = SystemClock.elapsedRealtime(); + baseElapsedMs = clock.elapsedRealtime(); } } - /** - * Synchronizes this clock with the current state of {@code clock}. - * - * @param clock The clock with which to synchronize. - */ - public void synchronize(MediaClock clock) { - setPositionUs(clock.getPositionUs()); - playbackParameters = clock.getPlaybackParameters(); - } - @Override public long getPositionUs() { long positionUs = baseUs; if (started) { - long elapsedSinceBaseMs = SystemClock.elapsedRealtime() - baseElapsedMs; + long elapsedSinceBaseMs = clock.elapsedRealtime() - baseElapsedMs; if (playbackParameters.speed == 1f) { positionUs += C.msToUs(elapsedSinceBaseMs); } else { - positionUs += playbackParameters.getSpeedAdjustedDurationUs(elapsedSinceBaseMs); + positionUs += playbackParameters.getMediaTimeUsForPlayoutTimeMs(elapsedSinceBaseMs); } } return positionUs; } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(PlaybackParameters playbackParameters) { // Store the current position as the new base, in case the playback speed has changed. if (started) { - setPositionUs(getPositionUs()); + resetPosition(getPositionUs()); } this.playbackParameters = playbackParameters; - return playbackParameters; } @Override diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemClock.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemClock.java index 302056251d81..a2f915866daa 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemClock.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemClock.java @@ -15,14 +15,33 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.util; +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.Looper; +import androidx.annotation.Nullable; + /** * The standard implementation of {@link Clock}. */ -public final class SystemClock implements Clock { +/* package */ final class SystemClock implements Clock { @Override public long elapsedRealtime() { return android.os.SystemClock.elapsedRealtime(); } + @Override + public long uptimeMillis() { + return android.os.SystemClock.uptimeMillis(); + } + + @Override + public void sleep(long sleepTimeMs) { + android.os.SystemClock.sleep(sleepTimeMs); + } + + @Override + public HandlerWrapper createHandler(Looper looper, @Nullable Callback callback) { + return new SystemHandlerWrapper(new Handler(looper, callback)); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemHandlerWrapper.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemHandlerWrapper.java new file mode 100644 index 000000000000..e69a24cc10ed --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemHandlerWrapper.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util; + +import android.os.Looper; +import android.os.Message; +import androidx.annotation.Nullable; + +/** The standard implementation of {@link HandlerWrapper}. */ +/* package */ final class SystemHandlerWrapper implements HandlerWrapper { + + private final android.os.Handler handler; + + public SystemHandlerWrapper(android.os.Handler handler) { + this.handler = handler; + } + + @Override + public Looper getLooper() { + return handler.getLooper(); + } + + @Override + public Message obtainMessage(int what) { + return handler.obtainMessage(what); + } + + @Override + public Message obtainMessage(int what, @Nullable Object obj) { + return handler.obtainMessage(what, obj); + } + + @Override + public Message obtainMessage(int what, int arg1, int arg2) { + return handler.obtainMessage(what, arg1, arg2); + } + + @Override + public Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj) { + return handler.obtainMessage(what, arg1, arg2, obj); + } + + @Override + public boolean sendEmptyMessage(int what) { + return handler.sendEmptyMessage(what); + } + + @Override + public boolean sendEmptyMessageAtTime(int what, long uptimeMs) { + return handler.sendEmptyMessageAtTime(what, uptimeMs); + } + + @Override + public void removeMessages(int what) { + handler.removeMessages(what); + } + + @Override + public void removeCallbacksAndMessages(@Nullable Object token) { + handler.removeCallbacksAndMessages(token); + } + + @Override + public boolean post(Runnable runnable) { + return handler.post(runnable); + } + + @Override + public boolean postDelayed(Runnable runnable, long delayMs) { + return handler.postDelayed(runnable, delayMs); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimedValueQueue.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimedValueQueue.java new file mode 100644 index 000000000000..396e50dcffb6 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimedValueQueue.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util; + +import androidx.annotation.Nullable; +import java.util.Arrays; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** A utility class to keep a queue of values with timestamps. This class is thread safe. */ +public final class TimedValueQueue { + private static final int INITIAL_BUFFER_SIZE = 10; + + // Looping buffer for timestamps and values + private long[] timestamps; + private @NullableType V[] values; + private int first; + private int size; + + public TimedValueQueue() { + this(INITIAL_BUFFER_SIZE); + } + + /** Creates a TimedValueBuffer with the given initial buffer size. */ + public TimedValueQueue(int initialBufferSize) { + timestamps = new long[initialBufferSize]; + values = newArray(initialBufferSize); + } + + /** + * Associates the specified value with the specified timestamp. All new values should have a + * greater timestamp than the previously added values. Otherwise all values are removed before + * adding the new one. + */ + public synchronized void add(long timestamp, V value) { + clearBufferOnTimeDiscontinuity(timestamp); + doubleCapacityIfFull(); + addUnchecked(timestamp, value); + } + + /** Removes all of the values. */ + public synchronized void clear() { + first = 0; + size = 0; + Arrays.fill(values, null); + } + + /** Returns number of the values buffered. */ + public synchronized int size() { + return size; + } + + /** + * Returns the value with the greatest timestamp which is less than or equal to the given + * timestamp. Removes all older values and the returned one from the buffer. + * + * @param timestamp The timestamp value. + * @return The value with the greatest timestamp which is less than or equal to the given + * timestamp or null if there is no such value. + * @see #poll(long) + */ + public synchronized @Nullable V pollFloor(long timestamp) { + return poll(timestamp, /* onlyOlder= */ true); + } + + /** + * Returns the value with the closest timestamp to the given timestamp. Removes all older values + * including the returned one from the buffer. + * + * @param timestamp The timestamp value. + * @return The value with the closest timestamp or null if the buffer is empty. + * @see #pollFloor(long) + */ + public synchronized @Nullable V poll(long timestamp) { + return poll(timestamp, /* onlyOlder= */ false); + } + + /** + * Returns the value with the closest timestamp to the given timestamp. Removes all older values + * including the returned one from the buffer. + * + * @param timestamp The timestamp value. + * @param onlyOlder Whether this method can return a new value in case its timestamp value is + * closest to {@code timestamp}. + * @return The value with the closest timestamp or null if the buffer is empty or there is no + * older value and {@code onlyOlder} is true. + */ + @Nullable + private V poll(long timestamp, boolean onlyOlder) { + V value = null; + long previousTimeDiff = Long.MAX_VALUE; + while (size > 0) { + long timeDiff = timestamp - timestamps[first]; + if (timeDiff < 0 && (onlyOlder || -timeDiff >= previousTimeDiff)) { + break; + } + previousTimeDiff = timeDiff; + value = values[first]; + values[first] = null; + first = (first + 1) % values.length; + size--; + } + return value; + } + + private void clearBufferOnTimeDiscontinuity(long timestamp) { + if (size > 0) { + int last = (first + size - 1) % values.length; + if (timestamp <= timestamps[last]) { + clear(); + } + } + } + + private void doubleCapacityIfFull() { + int capacity = values.length; + if (size < capacity) { + return; + } + int newCapacity = capacity * 2; + long[] newTimestamps = new long[newCapacity]; + V[] newValues = newArray(newCapacity); + // Reset the loop starting index to 0 while coping to the new buffer. + // First copy the values from 'first' index to the end of original array. + int length = capacity - first; + System.arraycopy(timestamps, first, newTimestamps, 0, length); + System.arraycopy(values, first, newValues, 0, length); + // Then the values from index 0 to 'first' index. + if (first > 0) { + System.arraycopy(timestamps, 0, newTimestamps, length, first); + System.arraycopy(values, 0, newValues, length, first); + } + timestamps = newTimestamps; + values = newValues; + first = 0; + } + + private void addUnchecked(long timestamp, V value) { + int next = (first + size) % values.length; + timestamps[next] = timestamp; + values[next] = value; + size++; + } + + @SuppressWarnings("unchecked") + private static V[] newArray(int length) { + return (V[]) new Object[length]; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimestampAdjuster.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimestampAdjuster.java index 4cc8e6ef0a3a..e82425128254 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimestampAdjuster.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimestampAdjuster.java @@ -30,7 +30,8 @@ public final class TimestampAdjuster { public static final long DO_NOT_OFFSET = Long.MAX_VALUE; /** - * The value one greater than the largest representable (33 bit) MPEG-2 TS presentation timestamp. + * The value one greater than the largest representable (33 bit) MPEG-2 TS 90 kHz clock + * presentation timestamp. */ private static final long MAX_PTS_PLUS_ONE = 0x200000000L; @@ -38,13 +39,13 @@ public final class TimestampAdjuster { private long timestampOffsetUs; // Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp. - private volatile long lastSampleTimestamp; + private volatile long lastSampleTimestampUs; /** * @param firstSampleTimestampUs See {@link #setFirstSampleTimestampUs(long)}. */ public TimestampAdjuster(long firstSampleTimestampUs) { - lastSampleTimestamp = C.TIME_UNSET; + lastSampleTimestampUs = C.TIME_UNSET; setFirstSampleTimestampUs(firstSampleTimestampUs); } @@ -56,30 +57,24 @@ public final class TimestampAdjuster { * {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset. */ public synchronized void setFirstSampleTimestampUs(long firstSampleTimestampUs) { - Assertions.checkState(lastSampleTimestamp == C.TIME_UNSET); + Assertions.checkState(lastSampleTimestampUs == C.TIME_UNSET); this.firstSampleTimestampUs = firstSampleTimestampUs; } - /** - * Returns the first adjusted sample timestamp in microseconds. - * - * @return The first adjusted sample timestamp in microseconds. - */ + /** Returns the last value passed to {@link #setFirstSampleTimestampUs(long)}. */ public long getFirstSampleTimestampUs() { return firstSampleTimestampUs; } /** - * Returns the last adjusted timestamp. If no timestamp has been adjusted, returns - * {@code firstSampleTimestampUs} as provided to the constructor. If this value is - * {@link #DO_NOT_OFFSET}, returns {@link C#TIME_UNSET}. - * - * @return The last adjusted timestamp. If not present, {@code firstSampleTimestampUs} is - * returned unless equal to {@link #DO_NOT_OFFSET}, in which case {@link C#TIME_UNSET} is - * returned. + * Returns the last value obtained from {@link #adjustSampleTimestamp}. If {@link + * #adjustSampleTimestamp} has not been called, returns the result of calling {@link + * #getFirstSampleTimestampUs()}. If this value is {@link #DO_NOT_OFFSET}, returns {@link + * C#TIME_UNSET}. */ public long getLastAdjustedTimestampUs() { - return lastSampleTimestamp != C.TIME_UNSET ? lastSampleTimestamp + return lastSampleTimestampUs != C.TIME_UNSET + ? (lastSampleTimestampUs + timestampOffsetUs) : firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET; } @@ -93,44 +88,47 @@ public final class TimestampAdjuster { * be offset. */ public long getTimestampOffsetUs() { - return firstSampleTimestampUs == DO_NOT_OFFSET ? 0 - : lastSampleTimestamp == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs; + return firstSampleTimestampUs == DO_NOT_OFFSET + ? 0 + : lastSampleTimestampUs == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs; } /** * Resets the instance to its initial state. */ public void reset() { - lastSampleTimestamp = C.TIME_UNSET; + lastSampleTimestampUs = C.TIME_UNSET; } /** * Scales and offsets an MPEG-2 TS presentation timestamp considering wraparound. * - * @param pts The MPEG-2 TS presentation timestamp. + * @param pts90Khz A 90 kHz clock MPEG-2 TS presentation timestamp. * @return The adjusted timestamp in microseconds. */ - public long adjustTsTimestamp(long pts) { - if (pts == C.TIME_UNSET) { + public long adjustTsTimestamp(long pts90Khz) { + if (pts90Khz == C.TIME_UNSET) { return C.TIME_UNSET; } - if (lastSampleTimestamp != C.TIME_UNSET) { + if (lastSampleTimestampUs != C.TIME_UNSET) { // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1), - // and we need to snap to the one closest to lastSampleTimestamp. - long lastPts = usToPts(lastSampleTimestamp); + // and we need to snap to the one closest to lastSampleTimestampUs. + long lastPts = usToPts(lastSampleTimestampUs); long closestWrapCount = (lastPts + (MAX_PTS_PLUS_ONE / 2)) / MAX_PTS_PLUS_ONE; - long ptsWrapBelow = pts + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1)); - long ptsWrapAbove = pts + (MAX_PTS_PLUS_ONE * closestWrapCount); - pts = Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts) - ? ptsWrapBelow : ptsWrapAbove; + long ptsWrapBelow = pts90Khz + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1)); + long ptsWrapAbove = pts90Khz + (MAX_PTS_PLUS_ONE * closestWrapCount); + pts90Khz = + Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts) + ? ptsWrapBelow + : ptsWrapAbove; } - return adjustSampleTimestamp(ptsToUs(pts)); + return adjustSampleTimestamp(ptsToUs(pts90Khz)); } /** - * Offsets a sample timestamp in microseconds. + * Offsets a timestamp in microseconds. * - * @param timeUs The timestamp of a sample to adjust. + * @param timeUs The timestamp to adjust in microseconds. * @return The adjusted timestamp in microseconds. */ public long adjustSampleTimestamp(long timeUs) { @@ -138,15 +136,15 @@ public final class TimestampAdjuster { return C.TIME_UNSET; } // Record the adjusted PTS to adjust for wraparound next time. - if (lastSampleTimestamp != C.TIME_UNSET) { - lastSampleTimestamp = timeUs; + if (lastSampleTimestampUs != C.TIME_UNSET) { + lastSampleTimestampUs = timeUs; } else { if (firstSampleTimestampUs != DO_NOT_OFFSET) { // Calculate the timestamp offset. timestampOffsetUs = firstSampleTimestampUs - timeUs; } synchronized (this) { - lastSampleTimestamp = timeUs; + lastSampleTimestampUs = timeUs; // Notify threads waiting for this adjuster to be initialized. notifyAll(); } @@ -160,15 +158,15 @@ public final class TimestampAdjuster { * @throws InterruptedException If the thread was interrupted. */ public synchronized void waitUntilInitialized() throws InterruptedException { - while (lastSampleTimestamp == C.TIME_UNSET) { + while (lastSampleTimestampUs == C.TIME_UNSET) { wait(); } } /** - * Converts a value in MPEG-2 timestamp units to the corresponding value in microseconds. + * Converts a 90 kHz clock timestamp to a timestamp in microseconds. * - * @param pts A value in MPEG-2 timestamp units. + * @param pts A 90 kHz clock timestamp. * @return The corresponding value in microseconds. */ public static long ptsToUs(long pts) { @@ -176,10 +174,10 @@ public final class TimestampAdjuster { } /** - * Converts a value in microseconds to the corresponding values in MPEG-2 timestamp units. + * Converts a timestamp in microseconds to a 90 kHz clock timestamp. * * @param us A value in microseconds. - * @return The corresponding value in MPEG-2 timestamp units. + * @return The corresponding value as a 90 kHz clock timestamp. */ public static long usToPts(long us) { return (us * 90000) / C.MICROS_PER_SECOND; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/UriUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/UriUtil.java index a3a36b8c9254..03b5d26a51eb 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/UriUtil.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/UriUtil.java @@ -17,6 +17,7 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.util; import android.net.Uri; import android.text.TextUtils; +import androidx.annotation.Nullable; /** * Utility methods for manipulating URIs. @@ -69,19 +70,19 @@ public final class UriUtil { * @param baseUri The base URI. * @param referenceUri The reference URI to resolve. */ - public static Uri resolveToUri(String baseUri, String referenceUri) { + public static Uri resolveToUri(@Nullable String baseUri, @Nullable String referenceUri) { return Uri.parse(resolve(baseUri, referenceUri)); } /** * Performs relative resolution of a {@code referenceUri} with respect to a {@code baseUri}. - *

- * The resolution is performed as specified by RFC-3986. + * + *

The resolution is performed as specified by RFC-3986. * * @param baseUri The base URI. * @param referenceUri The reference URI to resolve. */ - public static String resolve(String baseUri, String referenceUri) { + public static String resolve(@Nullable String baseUri, @Nullable String referenceUri) { StringBuilder uri = new StringBuilder(); // Map null onto empty string, to make the following logic simpler. @@ -143,6 +144,26 @@ public final class UriUtil { } } + /** + * Removes query parameter from an Uri, if present. + * + * @param uri The uri. + * @param queryParameterName The name of the query parameter. + * @return The uri without the query parameter. + */ + public static Uri removeQueryParameter(Uri uri, String queryParameterName) { + Uri.Builder builder = uri.buildUpon(); + builder.clearQuery(); + for (String key : uri.getQueryParameterNames()) { + if (!key.equals(queryParameterName)) { + for (String value : uri.getQueryParameters(key)) { + builder.appendQueryParameter(key, value); + } + } + } + return builder.build(); + } + /** * Removes dot segments from the path of a URI. * diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Util.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Util.java index 7bc7523774ab..4d7d8014dd56 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Util.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Util.java @@ -15,26 +15,48 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.util; +import static android.content.Context.UI_MODE_SERVICE; + import android.Manifest.permission; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; +import android.app.UiModeManager; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Point; +import android.media.AudioFormat; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; -import android.support.annotation.NonNull; +import android.os.Handler; +import android.os.Looper; +import android.os.Parcel; +import android.security.NetworkSecurityPolicy; +import android.telephony.TelephonyManager; import android.text.TextUtils; -import android.util.Log; import android.view.Display; +import android.view.SurfaceView; import android.view.WindowManager; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Renderer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RenderersFactory; +import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioRendererEventListener; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource; -import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoRendererEventListener; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; @@ -48,14 +70,22 @@ import java.util.Calendar; import java.util.Collections; import java.util.Formatter; import java.util.GregorianCalendar; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.MissingResourceException; import java.util.TimeZone; +import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.PolyNull; /** * Miscellaneous utility methods. @@ -66,9 +96,7 @@ public final class Util { * Like {@link android.os.Build.VERSION#SDK_INT}, but in a place where it can be conveniently * overridden for local testing. */ - public static final int SDK_INT = - (Build.VERSION.SDK_INT == 25 && Build.VERSION.CODENAME.charAt(0) == 'O') ? 26 - : Build.VERSION.SDK_INT; + public static final int SDK_INT = Build.VERSION.SDK_INT; /** * Like {@link Build#DEVICE}, but in a place where it can be conveniently overridden for local @@ -94,23 +122,29 @@ public final class Util { public static final String DEVICE_DEBUG_INFO = DEVICE + ", " + MODEL + ", " + MANUFACTURER + ", " + SDK_INT; + /** An empty byte array. */ + public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + private static final String TAG = "Util"; private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile( "(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]" + "(\\d\\d):(\\d\\d):(\\d\\d)([\\.,](\\d+))?" - + "([Zz]|((\\+|\\-)(\\d\\d):?(\\d\\d)))?"); + + "([Zz]|((\\+|\\-)(\\d?\\d):?(\\d\\d)))?"); private static final Pattern XS_DURATION_PATTERN = Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?" + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})"); + // Replacement map of ISO language codes used for normalization. + @Nullable private static HashMap languageTagReplacementMap; + private Util() {} /** * Converts the entirety of an {@link InputStream} to a byte array. * * @param inputStream the {@link InputStream} to be read. The input stream is not closed by this - * method. + * method. * @return a byte array containing all of the inputStream's bytes. * @throws IOException if an error occurs reading from the stream. */ @@ -124,6 +158,23 @@ public final class Util { return outputStream.toByteArray(); } + /** + * Calls {@link Context#startForegroundService(Intent)} if {@link #SDK_INT} is 26 or higher, or + * {@link Context#startService(Intent)} otherwise. + * + * @param context The context to call. + * @param intent The intent to pass to the called method. + * @return The result of the called method. + */ + @Nullable + public static ComponentName startForegroundService(Context context, Intent intent) { + if (Util.SDK_INT >= 26) { + return context.startForegroundService(intent); + } else { + return context.startService(intent); + } + } + /** * Checks whether it's necessary to request the {@link permission#READ_EXTERNAL_STORAGE} * permission read the specified {@link Uri}s, requesting the permission if necessary. @@ -138,7 +189,7 @@ public final class Util { return false; } for (Uri uri : uris) { - if (Util.isLocalFileUri(uri)) { + if (isLocalFileUri(uri)) { if (activity.checkSelfPermission(permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { activity.requestPermissions(new String[] {permission.READ_EXTERNAL_STORAGE}, 0); @@ -150,6 +201,30 @@ public final class Util { return false; } + /** + * Returns whether it may be possible to load the given URIs based on the network security + * policy's cleartext traffic permissions. + * + * @param uris A list of URIs that will be loaded. + * @return Whether it may be possible to load the given URIs. + */ + @TargetApi(24) + public static boolean checkCleartextTrafficPermitted(Uri... uris) { + if (Util.SDK_INT < 24) { + // We assume cleartext traffic is permitted. + return true; + } + for (Uri uri : uris) { + if ("http".equals(uri.getScheme()) + && !NetworkSecurityPolicy.getInstance() + .isCleartextTrafficPermitted(Assertions.checkNotNull(uri.getHost()))) { + // The security policy prevents cleartext traffic. + return false; + } + } + return true; + } + /** * Returns true if the URI is a path to a local file or a reference to a local file. * @@ -157,7 +232,7 @@ public final class Util { */ public static boolean isLocalFileUri(Uri uri) { String scheme = uri.getScheme(); - return TextUtils.isEmpty(scheme) || scheme.equals("file"); + return TextUtils.isEmpty(scheme) || "file".equals(scheme); } /** @@ -168,29 +243,168 @@ public final class Util { * @param o2 The second object. * @return {@code o1 == null ? o2 == null : o1.equals(o2)}. */ - public static boolean areEqual(Object o1, Object o2) { + public static boolean areEqual(@Nullable Object o1, @Nullable Object o2) { return o1 == null ? o2 == null : o1.equals(o2); } /** * Tests whether an {@code items} array contains an object equal to {@code item}, according to * {@link Object#equals(Object)}. - *

- * If {@code item} is null then true is returned if and only if {@code items} contains null. + * + *

If {@code item} is null then true is returned if and only if {@code items} contains null. * * @param items The array of items to search. * @param item The item to search for. * @return True if the array contains an object equal to the item being searched for. */ - public static boolean contains(Object[] items, Object item) { + public static boolean contains(@NullableType Object[] items, @Nullable Object item) { for (Object arrayItem : items) { - if (Util.areEqual(arrayItem, item)) { + if (areEqual(arrayItem, item)) { return true; } } return false; } + /** + * Removes an indexed range from a List. + * + *

Does nothing if the provided range is valid and {@code fromIndex == toIndex}. + * + * @param list The List to remove the range from. + * @param fromIndex The first index to be removed (inclusive). + * @param toIndex The last index to be removed (exclusive). + * @throws IllegalArgumentException If {@code fromIndex} < 0, {@code toIndex} > {@code + * list.size()}, or {@code fromIndex} > {@code toIndex}. + */ + public static void removeRange(List list, int fromIndex, int toIndex) { + if (fromIndex < 0 || toIndex > list.size() || fromIndex > toIndex) { + throw new IllegalArgumentException(); + } else if (fromIndex != toIndex) { + // Checking index inequality prevents an unnecessary allocation. + list.subList(fromIndex, toIndex).clear(); + } + } + + /** + * Casts a nullable variable to a non-null variable without runtime null check. + * + *

Use {@link Assertions#checkNotNull(Object)} to throw if the value is null. + */ + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) + @EnsuresNonNull("#1") + public static T castNonNull(@Nullable T value) { + return value; + } + + /** Casts a nullable type array to a non-null type array without runtime null check. */ + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) + @EnsuresNonNull("#1") + public static T[] castNonNullTypeArray(@NullableType T[] value) { + return value; + } + + /** + * Copies and optionally truncates an array. Prevents null array elements created by {@link + * Arrays#copyOf(Object[], int)} by ensuring the new length does not exceed the current length. + * + * @param input The input array. + * @param length The output array length. Must be less or equal to the length of the input array. + * @return The copied array. + */ + @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:return.type.incompatible"}) + public static T[] nullSafeArrayCopy(T[] input, int length) { + Assertions.checkArgument(length <= input.length); + return Arrays.copyOf(input, length); + } + + /** + * Copies a subset of an array. + * + * @param input The input array. + * @param from The start the range to be copied, inclusive + * @param to The end of the range to be copied, exclusive. + * @return The copied array. + */ + @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:return.type.incompatible"}) + public static T[] nullSafeArrayCopyOfRange(T[] input, int from, int to) { + Assertions.checkArgument(0 <= from); + Assertions.checkArgument(to <= input.length); + return Arrays.copyOfRange(input, from, to); + } + + /** + * Creates a new array containing {@code original} with {@code newElement} appended. + * + * @param original The input array. + * @param newElement The element to append. + * @return The new array. + */ + public static T[] nullSafeArrayAppend(T[] original, T newElement) { + @NullableType T[] result = Arrays.copyOf(original, original.length + 1); + result[original.length] = newElement; + return castNonNullTypeArray(result); + } + + /** + * Creates a new array containing the concatenation of two non-null type arrays. + * + * @param first The first array. + * @param second The second array. + * @return The concatenated result. + */ + @SuppressWarnings({"nullness:assignment.type.incompatible"}) + public static T[] nullSafeArrayConcatenation(T[] first, T[] second) { + T[] concatenation = Arrays.copyOf(first, first.length + second.length); + System.arraycopy( + /* src= */ second, + /* srcPos= */ 0, + /* dest= */ concatenation, + /* destPos= */ first.length, + /* length= */ second.length); + return concatenation; + } + /** + * Creates a {@link Handler} with the specified {@link Handler.Callback} on the current {@link + * Looper} thread. The method accepts partially initialized objects as callback under the + * assumption that the Handler won't be used to send messages until the callback is fully + * initialized. + * + *

If the current thread doesn't have a {@link Looper}, the application's main thread {@link + * Looper} is used. + * + * @param callback A {@link Handler.Callback}. May be a partially initialized class. + * @return A {@link Handler} with the specified callback on the current {@link Looper} thread. + */ + public static Handler createHandler(Handler.@UnknownInitialization Callback callback) { + return createHandler(getLooper(), callback); + } + + /** + * Creates a {@link Handler} with the specified {@link Handler.Callback} on the specified {@link + * Looper} thread. The method accepts partially initialized objects as callback under the + * assumption that the Handler won't be used to send messages until the callback is fully + * initialized. + * + * @param looper A {@link Looper} to run the callback on. + * @param callback A {@link Handler.Callback}. May be a partially initialized class. + * @return A {@link Handler} with the specified callback on the current {@link Looper} thread. + */ + @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:return.type.incompatible"}) + public static Handler createHandler( + Looper looper, Handler.@UnknownInitialization Callback callback) { + return new Handler(looper, callback); + } + + /** + * Returns the {@link Looper} associated with the current thread, or the {@link Looper} of the + * application's main thread if the current thread doesn't have a {@link Looper}. + */ + public static Looper getLooper() { + Looper myLooper = Looper.myLooper(); + return myLooper != null ? myLooper : Looper.getMainLooper(); + } + /** * Instantiates a new single threaded executor whose thread has the specified name. * @@ -198,12 +412,7 @@ public final class Util { * @return The executor. */ public static ExecutorService newSingleThreadExecutor(final String threadName) { - return Executors.newSingleThreadExecutor(new ThreadFactory() { - @Override - public Thread newThread(@NonNull Runnable r) { - return new Thread(r, threadName); - } - }); + return Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, threadName)); } /** @@ -211,7 +420,7 @@ public final class Util { * * @param dataSource The {@link DataSource} to close. */ - public static void closeQuietly(DataSource dataSource) { + public static void closeQuietly(@Nullable DataSource dataSource) { try { if (dataSource != null) { dataSource.close(); @@ -227,7 +436,7 @@ public final class Util { * * @param closeable The {@link Closeable} to close. */ - public static void closeQuietly(Closeable closeable) { + public static void closeQuietly(@Nullable Closeable closeable) { try { if (closeable != null) { closeable.close(); @@ -238,13 +447,97 @@ public final class Util { } /** - * Returns a normalized RFC 5646 language code. + * Reads an integer from a {@link Parcel} and interprets it as a boolean, with 0 mapping to false + * and all other values mapping to true. * - * @param language A possibly non-normalized RFC 5646 language code. - * @return The normalized code, or null if the input was null. + * @param parcel The {@link Parcel} to read from. + * @return The read value. */ - public static String normalizeLanguageCode(String language) { - return language == null ? null : new Locale(language).getLanguage(); + public static boolean readBoolean(Parcel parcel) { + return parcel.readInt() != 0; + } + + /** + * Writes a boolean to a {@link Parcel}. The boolean is written as an integer with value 1 (true) + * or 0 (false). + * + * @param parcel The {@link Parcel} to write to. + * @param value The value to write. + */ + public static void writeBoolean(Parcel parcel, boolean value) { + parcel.writeInt(value ? 1 : 0); + } + + /** + * Returns the language tag for a {@link Locale}. + * + *

For API levels ≥ 21, this tag is IETF BCP 47 compliant. Use {@link + * #normalizeLanguageCode(String)} to retrieve a normalized IETF BCP 47 language tag for all API + * levels if needed. + * + * @param locale A {@link Locale}. + * @return The language tag. + */ + public static String getLocaleLanguageTag(Locale locale) { + return SDK_INT >= 21 ? getLocaleLanguageTagV21(locale) : locale.toString(); + } + + /** + * Returns a normalized IETF BCP 47 language tag for {@code language}. + * + * @param language A case-insensitive language code supported by {@link + * Locale#forLanguageTag(String)}. + * @return The all-lowercase normalized code, or null if the input was null, or {@code + * language.toLowerCase()} if the language could not be normalized. + */ + public static @PolyNull String normalizeLanguageCode(@PolyNull String language) { + if (language == null) { + return null; + } + // Locale data (especially for API < 21) may produce tags with '_' instead of the + // standard-conformant '-'. + String normalizedTag = language.replace('_', '-'); + if (normalizedTag.isEmpty() || "und".equals(normalizedTag)) { + // Tag isn't valid, keep using the original. + normalizedTag = language; + } + normalizedTag = Util.toLowerInvariant(normalizedTag); + String mainLanguage = Util.splitAtFirst(normalizedTag, "-")[0]; + if (languageTagReplacementMap == null) { + languageTagReplacementMap = createIsoLanguageReplacementMap(); + } + @Nullable String replacedLanguage = languageTagReplacementMap.get(mainLanguage); + if (replacedLanguage != null) { + normalizedTag = + replacedLanguage + normalizedTag.substring(/* beginIndex= */ mainLanguage.length()); + mainLanguage = replacedLanguage; + } + if ("no".equals(mainLanguage) || "i".equals(mainLanguage) || "zh".equals(mainLanguage)) { + normalizedTag = maybeReplaceGrandfatheredLanguageTags(normalizedTag); + } + return normalizedTag; + } + + /** + * Returns a new {@link String} constructed by decoding UTF-8 encoded bytes. + * + * @param bytes The UTF-8 encoded bytes to decode. + * @return The string. + */ + public static String fromUtf8Bytes(byte[] bytes) { + return new String(bytes, Charset.forName(C.UTF8_NAME)); + } + + /** + * Returns a new {@link String} constructed by decoding UTF-8 encoded bytes in a subarray. + * + * @param bytes The UTF-8 encoded bytes to decode. + * @param offset The index of the first byte to decode. + * @param length The number of bytes to decode. + * @return The string. + */ + public static String fromUtf8Bytes(byte[] bytes, int offset, int length) { + return new String(bytes, offset, length, Charset.forName(C.UTF8_NAME)); } /** @@ -254,7 +547,34 @@ public final class Util { * @return The code points encoding using UTF-8. */ public static byte[] getUtf8Bytes(String value) { - return value.getBytes(Charset.defaultCharset()); // UTF-8 is the default on Android. + return value.getBytes(Charset.forName(C.UTF8_NAME)); + } + + /** + * Splits a string using {@code value.split(regex, -1}). Note: this is is similar to {@link + * String#split(String)} but empty matches at the end of the string will not be omitted from the + * returned array. + * + * @param value The string to split. + * @param regex A delimiting regular expression. + * @return The array of strings resulting from splitting the string. + */ + public static String[] split(String value, String regex) { + return value.split(regex, /* limit= */ -1); + } + + /** + * Splits the string at the first occurrence of the delimiter {@code regex}. If the delimiter does + * not match, returns an array with one element which is the input string. If the delimiter does + * match, returns an array with the portion of the string before the delimiter and the rest of the + * string. + * + * @param value The string. + * @param regex A delimiting regular expression. + * @return The string split by the first occurrence of the delimiter. + */ + public static String[] splitAtFirst(String value, String regex) { + return value.split(regex, /* limit= */ 2); } /** @@ -273,8 +593,27 @@ public final class Util { * @param text The text to convert. * @return The lower case text, or null if {@code text} is null. */ - public static String toLowerInvariant(String text) { - return text == null ? null : text.toLowerCase(Locale.US); + public static @PolyNull String toLowerInvariant(@PolyNull String text) { + return text == null ? text : text.toLowerCase(Locale.US); + } + + /** + * Converts text to upper case using {@link Locale#US}. + * + * @param text The text to convert. + * @return The upper case text, or null if {@code text} is null. + */ + public static @PolyNull String toUpperInvariant(@PolyNull String text) { + return text == null ? text : text.toUpperCase(Locale.US); + } + + /** + * Formats a string using {@link Locale#US}. + * + * @see String#format(String, Object...) + */ + public static String formatInvariant(String format, Object... args) { + return String.format(Locale.US, format, args); } /** @@ -335,11 +674,63 @@ public final class Util { return Math.max(min, Math.min(value, max)); } + /** + * Returns the sum of two arguments, or a third argument if the result overflows. + * + * @param x The first value. + * @param y The second value. + * @param overflowResult The return value if {@code x + y} overflows. + * @return {@code x + y}, or {@code overflowResult} if the result overflows. + */ + public static long addWithOverflowDefault(long x, long y, long overflowResult) { + long result = x + y; + // See Hacker's Delight 2-13 (H. Warren Jr). + if (((x ^ result) & (y ^ result)) < 0) { + return overflowResult; + } + return result; + } + + /** + * Returns the difference between two arguments, or a third argument if the result overflows. + * + * @param x The first value. + * @param y The second value. + * @param overflowResult The return value if {@code x - y} overflows. + * @return {@code x - y}, or {@code overflowResult} if the result overflows. + */ + public static long subtractWithOverflowDefault(long x, long y, long overflowResult) { + long result = x - y; + // See Hacker's Delight 2-13 (H. Warren Jr). + if (((x ^ y) & (x ^ result)) < 0) { + return overflowResult; + } + return result; + } + + /** + * Returns the index of the first occurrence of {@code value} in {@code array}, or {@link + * C#INDEX_UNSET} if {@code value} is not contained in {@code array}. + * + * @param array The array to search. + * @param value The value to search for. + * @return The index of the first occurrence of value in {@code array}, or {@link C#INDEX_UNSET} + * if {@code value} is not contained in {@code array}. + */ + public static int linearSearch(int[] array, int value) { + for (int i = 0; i < array.length; i++) { + if (array[i] == value) { + return i; + } + } + return C.INDEX_UNSET; + } + /** * Returns the index of the largest element in {@code array} that is less than (or optionally * equal to) a specified {@code value}. - *

- * The search is performed using a binary search algorithm, so the array must be sorted. If the + * + *

The search is performed using a binary search algorithm, so the array must be sorted. If the * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the * index of the first one will be returned. * @@ -353,13 +744,13 @@ public final class Util { * @return The index of the largest element in {@code array} that is less than (or optionally * equal to) {@code value}. */ - public static int binarySearchFloor(int[] array, int value, boolean inclusive, - boolean stayInBounds) { + public static int binarySearchFloor( + int[] array, int value, boolean inclusive, boolean stayInBounds) { int index = Arrays.binarySearch(array, value); if (index < 0) { index = -(index + 2); } else { - while ((--index) >= 0 && array[index] == value) {} + while (--index >= 0 && array[index] == value) {} if (inclusive) { index++; } @@ -391,7 +782,43 @@ public final class Util { if (index < 0) { index = -(index + 2); } else { - while ((--index) >= 0 && array[index] == value) {} + while (--index >= 0 && array[index] == value) {} + if (inclusive) { + index++; + } + } + return stayInBounds ? Math.max(0, index) : index; + } + + /** + * Returns the index of the largest element in {@code list} that is less than (or optionally equal + * to) a specified {@code value}. + * + *

The search is performed using a binary search algorithm, so the list must be sorted. If the + * list contains multiple elements equal to {@code value} and {@code inclusive} is true, the index + * of the first one will be returned. + * + * @param The type of values being searched. + * @param list The list to search. + * @param value The value being searched for. + * @param inclusive If the value is present in the list, whether to return the corresponding + * index. If false then the returned index corresponds to the largest element strictly less + * than the value. + * @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than + * the smallest element in the list. If false then -1 will be returned. + * @return The index of the largest element in {@code list} that is less than (or optionally equal + * to) {@code value}. + */ + public static > int binarySearchFloor( + List> list, + T value, + boolean inclusive, + boolean stayInBounds) { + int index = Collections.binarySearch(list, value); + if (index < 0) { + index = -(index + 2); + } else { + while (--index >= 0 && list.get(index).compareTo(value) == 0) {} if (inclusive) { index++; } @@ -402,9 +829,9 @@ public final class Util { /** * Returns the index of the smallest element in {@code array} that is greater than (or optionally * equal to) a specified {@code value}. - *

- * The search is performed using a binary search algorithm, so the array must be sorted. If - * the array contains multiple elements equal to {@code value} and {@code inclusive} is true, the + * + *

The search is performed using a binary search algorithm, so the array must be sorted. If the + * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the * index of the last one will be returned. * * @param array The array to search. @@ -418,13 +845,13 @@ public final class Util { * @return The index of the smallest element in {@code array} that is greater than (or optionally * equal to) {@code value}. */ - public static int binarySearchCeil(long[] array, long value, boolean inclusive, - boolean stayInBounds) { + public static int binarySearchCeil( + int[] array, int value, boolean inclusive, boolean stayInBounds) { int index = Arrays.binarySearch(array, value); if (index < 0) { index = ~index; } else { - while ((++index) < array.length && array[index] == value) {} + while (++index < array.length && array[index] == value) {} if (inclusive) { index--; } @@ -433,45 +860,45 @@ public final class Util { } /** - * Returns the index of the largest element in {@code list} that is less than (or optionally equal - * to) a specified {@code value}. - *

- * The search is performed using a binary search algorithm, so the list must be sorted. If the - * list contains multiple elements equal to {@code value} and {@code inclusive} is true, the - * index of the first one will be returned. + * Returns the index of the smallest element in {@code array} that is greater than (or optionally + * equal to) a specified {@code value}. * - * @param The type of values being searched. - * @param list The list to search. + *

The search is performed using a binary search algorithm, so the array must be sorted. If the + * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the + * index of the last one will be returned. + * + * @param array The array to search. * @param value The value being searched for. - * @param inclusive If the value is present in the list, whether to return the corresponding - * index. If false then the returned index corresponds to the largest element strictly less - * than the value. - * @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than - * the smallest element in the list. If false then -1 will be returned. - * @return The index of the largest element in {@code list} that is less than (or optionally equal - * to) {@code value}. + * @param inclusive If the value is present in the array, whether to return the corresponding + * index. If false then the returned index corresponds to the smallest element strictly + * greater than the value. + * @param stayInBounds If true, then {@code (a.length - 1)} will be returned in the case that the + * value is greater than the largest element in the array. If false then {@code a.length} will + * be returned. + * @return The index of the smallest element in {@code array} that is greater than (or optionally + * equal to) {@code value}. */ - public static int binarySearchFloor(List> list, T value, - boolean inclusive, boolean stayInBounds) { - int index = Collections.binarySearch(list, value); + public static int binarySearchCeil( + long[] array, long value, boolean inclusive, boolean stayInBounds) { + int index = Arrays.binarySearch(array, value); if (index < 0) { - index = -(index + 2); + index = ~index; } else { - while ((--index) >= 0 && list.get(index).compareTo(value) == 0) {} + while (++index < array.length && array[index] == value) {} if (inclusive) { - index++; + index--; } } - return stayInBounds ? Math.max(0, index) : index; + return stayInBounds ? Math.min(array.length - 1, index) : index; } /** * Returns the index of the smallest element in {@code list} that is greater than (or optionally * equal to) a specified value. - *

- * The search is performed using a binary search algorithm, so the list must be sorted. If the - * list contains multiple elements equal to {@code value} and {@code inclusive} is true, the - * index of the last one will be returned. + * + *

The search is performed using a binary search algorithm, so the list must be sorted. If the + * list contains multiple elements equal to {@code value} and {@code inclusive} is true, the index + * of the last one will be returned. * * @param The type of values being searched. * @param list The list to search. @@ -480,19 +907,22 @@ public final class Util { * index. If false then the returned index corresponds to the smallest element strictly * greater than the value. * @param stayInBounds If true, then {@code (list.size() - 1)} will be returned in the case that - * the value is greater than the largest element in the list. If false then - * {@code list.size()} will be returned. + * the value is greater than the largest element in the list. If false then {@code + * list.size()} will be returned. * @return The index of the smallest element in {@code list} that is greater than (or optionally * equal to) {@code value}. */ - public static int binarySearchCeil(List> list, T value, - boolean inclusive, boolean stayInBounds) { + public static > int binarySearchCeil( + List> list, + T value, + boolean inclusive, + boolean stayInBounds) { int index = Collections.binarySearch(list, value); if (index < 0) { index = ~index; } else { int listSize = list.size(); - while ((++index) < listSize && list.get(index).compareTo(value) == 0) {} + while (++index < listSize && list.get(index).compareTo(value) == 0) {} if (inclusive) { index--; } @@ -500,6 +930,18 @@ public final class Util { return stayInBounds ? Math.min(list.size() - 1, index) : index; } + /** + * Compares two long values and returns the same value as {@code Long.compare(long, long)}. + * + * @param left The left operand. + * @param right The right operand. + * @return 0, if left == right, a negative value if left < right, or a positive value if left + * > right. + */ + public static int compareLong(long left, long right) { + return left < right ? -1 : left == right ? 0 : 1; + } + /** * Parses an xs:duration attribute value, returning the parsed duration in milliseconds. * @@ -554,7 +996,7 @@ public final class Util { } else { timezoneShift = ((Integer.parseInt(matcher.group(12)) * 60 + Integer.parseInt(matcher.group(13)))); - if (matcher.group(11).equals("-")) { + if ("-".equals(matcher.group(11))) { timezoneShift *= -1; } } @@ -662,13 +1104,78 @@ public final class Util { } } + /** + * Returns the duration of media that will elapse in {@code playoutDuration}. + * + * @param playoutDuration The duration to scale. + * @param speed The playback speed. + * @return The scaled duration, in the same units as {@code playoutDuration}. + */ + public static long getMediaDurationForPlayoutDuration(long playoutDuration, float speed) { + if (speed == 1f) { + return playoutDuration; + } + return Math.round((double) playoutDuration * speed); + } + + /** + * Returns the playout duration of {@code mediaDuration} of media. + * + * @param mediaDuration The duration to scale. + * @return The scaled duration, in the same units as {@code mediaDuration}. + */ + public static long getPlayoutDurationForMediaDuration(long mediaDuration, float speed) { + if (speed == 1f) { + return mediaDuration; + } + return Math.round((double) mediaDuration / speed); + } + + /** + * Resolves a seek given the requested seek position, a {@link SeekParameters} and two candidate + * sync points. + * + * @param positionUs The requested seek position, in microseocnds. + * @param seekParameters The {@link SeekParameters}. + * @param firstSyncUs The first candidate seek point, in micrseconds. + * @param secondSyncUs The second candidate seek point, in microseconds. May equal {@code + * firstSyncUs} if there's only one candidate. + * @return The resolved seek position, in microseconds. + */ + public static long resolveSeekPositionUs( + long positionUs, SeekParameters seekParameters, long firstSyncUs, long secondSyncUs) { + if (SeekParameters.EXACT.equals(seekParameters)) { + return positionUs; + } + long minPositionUs = + subtractWithOverflowDefault(positionUs, seekParameters.toleranceBeforeUs, Long.MIN_VALUE); + long maxPositionUs = + addWithOverflowDefault(positionUs, seekParameters.toleranceAfterUs, Long.MAX_VALUE); + boolean firstSyncPositionValid = minPositionUs <= firstSyncUs && firstSyncUs <= maxPositionUs; + boolean secondSyncPositionValid = + minPositionUs <= secondSyncUs && secondSyncUs <= maxPositionUs; + if (firstSyncPositionValid && secondSyncPositionValid) { + if (Math.abs(firstSyncUs - positionUs) <= Math.abs(secondSyncUs - positionUs)) { + return firstSyncUs; + } else { + return secondSyncUs; + } + } else if (firstSyncPositionValid) { + return firstSyncUs; + } else if (secondSyncPositionValid) { + return secondSyncUs; + } else { + return minPositionUs; + } + } + /** * Converts a list of integers to a primitive array. * * @param list A list of integers. * @return The list in array form, or null if the input list was null. */ - public static int[] toArray(List list) { + public static int @PolyNull [] toArray(@PolyNull List list) { if (list == null) { return null; } @@ -680,25 +1187,6 @@ public final class Util { return intArray; } - /** - * Given a {@link DataSpec} and a number of bytes already loaded, returns a {@link DataSpec} - * that represents the remainder of the data. - * - * @param dataSpec The original {@link DataSpec}. - * @param bytesLoaded The number of bytes already loaded. - * @return A {@link DataSpec} that represents the remainder of the data. - */ - public static DataSpec getRemainderDataSpec(DataSpec dataSpec, int bytesLoaded) { - if (bytesLoaded == 0) { - return dataSpec; - } else { - long remainingLength = dataSpec.length == C.LENGTH_UNSET ? C.LENGTH_UNSET - : dataSpec.length - bytesLoaded; - return new DataSpec(dataSpec.uri, dataSpec.position + bytesLoaded, remainingLength, - dataSpec.key, dataSpec.flags); - } - } - /** * Returns the integer equal to the big-endian concatenation of the characters in {@code string} * as bytes. The string must be no more than four characters long. @@ -716,6 +1204,29 @@ public final class Util { return result; } + /** + * Converts an integer to a long by unsigned conversion. + * + *

This method is equivalent to {@link Integer#toUnsignedLong(int)} for API 26+. + */ + public static long toUnsignedLong(int x) { + // x is implicitly casted to a long before the bit operation is executed but this does not + // impact the method correctness. + return x & 0xFFFFFFFFL; + } + + /** + * Return the long that is composed of the bits of the 2 specified integers. + * + * @param mostSignificantBits The 32 most significant bits of the long to return. + * @param leastSignificantBits The 32 least significant bits of the long to return. + * @return a long where its 32 most significant bits are {@code mostSignificantBits} bits and its + * 32 least significant bits are {@code leastSignificantBits}. + */ + public static long toLong(int mostSignificantBits, int leastSignificantBits) { + return (toUnsignedLong(mostSignificantBits) << 32) | toUnsignedLong(leastSignificantBits); + } + /** * Returns a byte array containing values parsed from the hex string provided. * @@ -769,6 +1280,45 @@ public final class Util { + ") " + ExoPlayerLibraryInfo.VERSION_SLASHY; } + /** + * Returns a copy of {@code codecs} without the codecs whose track type doesn't match {@code + * trackType}. + * + * @param codecs A codec sequence string, as defined in RFC 6381. + * @param trackType One of {@link C}{@code .TRACK_TYPE_*}. + * @return A copy of {@code codecs} without the codecs whose track type doesn't match {@code + * trackType}. If this ends up empty, or {@code codecs} is null, return null. + */ + public static @Nullable String getCodecsOfType(@Nullable String codecs, int trackType) { + String[] codecArray = splitCodecs(codecs); + if (codecArray.length == 0) { + return null; + } + StringBuilder builder = new StringBuilder(); + for (String codec : codecArray) { + if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) { + if (builder.length() > 0) { + builder.append(","); + } + builder.append(codec); + } + } + return builder.length() > 0 ? builder.toString() : null; + } + + /** + * Splits a codecs sequence string, as defined in RFC 6381, into individual codec strings. + * + * @param codecs A codec sequence string, as defined in RFC 6381. + * @return The split codecs, or an array of length zero if the input was empty or null. + */ + public static String[] splitCodecs(@Nullable String codecs) { + if (TextUtils.isEmpty(codecs)) { + return new String[0]; + } + return split(codecs.trim(), "(\\s*,\\s*)"); + } + /** * Converts a sample bit depth to a corresponding PCM encoding constant. * @@ -794,6 +1344,74 @@ public final class Util { } } + /** + * Returns whether {@code encoding} is one of the linear PCM encodings. + * + * @param encoding The encoding of the audio data. + * @return Whether the encoding is one of the PCM encodings. + */ + public static boolean isEncodingLinearPcm(@C.Encoding int encoding) { + return encoding == C.ENCODING_PCM_8BIT + || encoding == C.ENCODING_PCM_16BIT + || encoding == C.ENCODING_PCM_16BIT_BIG_ENDIAN + || encoding == C.ENCODING_PCM_24BIT + || encoding == C.ENCODING_PCM_32BIT + || encoding == C.ENCODING_PCM_FLOAT; + } + + /** + * Returns whether {@code encoding} is high resolution (> 16-bit) PCM. + * + * @param encoding The encoding of the audio data. + * @return Whether the encoding is high resolution PCM. + */ + public static boolean isEncodingHighResolutionPcm(@C.PcmEncoding int encoding) { + return encoding == C.ENCODING_PCM_24BIT + || encoding == C.ENCODING_PCM_32BIT + || encoding == C.ENCODING_PCM_FLOAT; + } + + /** + * Returns the audio track channel configuration for the given channel count, or {@link + * AudioFormat#CHANNEL_INVALID} if output is not poossible. + * + * @param channelCount The number of channels in the input audio. + * @return The channel configuration or {@link AudioFormat#CHANNEL_INVALID} if output is not + * possible. + */ + public static int getAudioTrackChannelConfig(int channelCount) { + switch (channelCount) { + case 1: + return AudioFormat.CHANNEL_OUT_MONO; + case 2: + return AudioFormat.CHANNEL_OUT_STEREO; + case 3: + return AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER; + case 4: + return AudioFormat.CHANNEL_OUT_QUAD; + case 5: + return AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER; + case 6: + return AudioFormat.CHANNEL_OUT_5POINT1; + case 7: + return AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER; + case 8: + if (Util.SDK_INT >= 23) { + return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; + } else if (Util.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 + | AudioFormat.CHANNEL_OUT_SIDE_RIGHT; + } else { + // 8 ch output is not supported before Android L. + return AudioFormat.CHANNEL_INVALID; + } + default: + return AudioFormat.CHANNEL_INVALID; + } + } + /** * Returns the frame size for audio with {@code channelCount} channels in the specified encoding. * @@ -806,16 +1424,138 @@ public final class Util { case C.ENCODING_PCM_8BIT: return channelCount; case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: return channelCount * 2; case C.ENCODING_PCM_24BIT: return channelCount * 3; case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_FLOAT: return channelCount * 4; + case C.ENCODING_INVALID: + case Format.NO_VALUE: default: throw new IllegalArgumentException(); } } + /** + * Returns the {@link C.AudioUsage} corresponding to the specified {@link C.StreamType}. + */ + @C.AudioUsage + public static int getAudioUsageForStreamType(@C.StreamType int streamType) { + switch (streamType) { + case C.STREAM_TYPE_ALARM: + return C.USAGE_ALARM; + case C.STREAM_TYPE_DTMF: + return C.USAGE_VOICE_COMMUNICATION_SIGNALLING; + case C.STREAM_TYPE_NOTIFICATION: + return C.USAGE_NOTIFICATION; + case C.STREAM_TYPE_RING: + return C.USAGE_NOTIFICATION_RINGTONE; + case C.STREAM_TYPE_SYSTEM: + return C.USAGE_ASSISTANCE_SONIFICATION; + case C.STREAM_TYPE_VOICE_CALL: + return C.USAGE_VOICE_COMMUNICATION; + case C.STREAM_TYPE_USE_DEFAULT: + case C.STREAM_TYPE_MUSIC: + default: + return C.USAGE_MEDIA; + } + } + + /** + * Returns the {@link C.AudioContentType} corresponding to the specified {@link C.StreamType}. + */ + @C.AudioContentType + public static int getAudioContentTypeForStreamType(@C.StreamType int streamType) { + switch (streamType) { + case C.STREAM_TYPE_ALARM: + case C.STREAM_TYPE_DTMF: + case C.STREAM_TYPE_NOTIFICATION: + case C.STREAM_TYPE_RING: + case C.STREAM_TYPE_SYSTEM: + return C.CONTENT_TYPE_SONIFICATION; + case C.STREAM_TYPE_VOICE_CALL: + return C.CONTENT_TYPE_SPEECH; + case C.STREAM_TYPE_USE_DEFAULT: + case C.STREAM_TYPE_MUSIC: + default: + return C.CONTENT_TYPE_MUSIC; + } + } + + /** + * Returns the {@link C.StreamType} corresponding to the specified {@link C.AudioUsage}. + */ + @C.StreamType + public static int getStreamTypeForAudioUsage(@C.AudioUsage int usage) { + switch (usage) { + case C.USAGE_MEDIA: + case C.USAGE_GAME: + case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: + return C.STREAM_TYPE_MUSIC; + case C.USAGE_ASSISTANCE_SONIFICATION: + return C.STREAM_TYPE_SYSTEM; + case C.USAGE_VOICE_COMMUNICATION: + return C.STREAM_TYPE_VOICE_CALL; + case C.USAGE_VOICE_COMMUNICATION_SIGNALLING: + return C.STREAM_TYPE_DTMF; + case C.USAGE_ALARM: + return C.STREAM_TYPE_ALARM; + case C.USAGE_NOTIFICATION_RINGTONE: + return C.STREAM_TYPE_RING; + case C.USAGE_NOTIFICATION: + case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST: + case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT: + case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED: + case C.USAGE_NOTIFICATION_EVENT: + return C.STREAM_TYPE_NOTIFICATION; + case C.USAGE_ASSISTANCE_ACCESSIBILITY: + case C.USAGE_ASSISTANT: + case C.USAGE_UNKNOWN: + default: + return C.STREAM_TYPE_DEFAULT; + } + } + + /** + * Derives a DRM {@link UUID} from {@code drmScheme}. + * + * @param drmScheme A UUID string, or {@code "widevine"}, {@code "playready"} or {@code + * "clearkey"}. + * @return The derived {@link UUID}, or {@code null} if one could not be derived. + */ + public static @Nullable UUID getDrmUuid(String drmScheme) { + switch (toLowerInvariant(drmScheme)) { + case "widevine": + return C.WIDEVINE_UUID; + case "playready": + return C.PLAYREADY_UUID; + case "clearkey": + return C.CLEARKEY_UUID; + default: + try { + return UUID.fromString(drmScheme); + } catch (RuntimeException e) { + return null; + } + } + } + + /** + * Makes a best guess to infer the type from a {@link Uri}. + * + * @param uri The {@link Uri}. + * @param overrideExtension If not null, used to infer the type. + * @return The content type. + */ + @C.ContentType + public static int inferContentType(Uri uri, @Nullable String overrideExtension) { + return TextUtils.isEmpty(overrideExtension) + ? inferContentType(uri) + : inferContentType("." + overrideExtension); + } + /** * Makes a best guess to infer the type from a {@link Uri}. * @@ -836,13 +1576,12 @@ public final class Util { */ @C.ContentType public static int inferContentType(String fileName) { - fileName = fileName.toLowerCase(); + fileName = toLowerInvariant(fileName); if (fileName.endsWith(".mpd")) { return C.TYPE_DASH; } else if (fileName.endsWith(".m3u8")) { return C.TYPE_HLS; - } else if (fileName.endsWith(".ism") || fileName.endsWith(".isml") - || fileName.endsWith(".ism/manifest") || fileName.endsWith(".isml/manifest")) { + } else if (fileName.matches(".*\\.ism(l)?(/manifest(\\(.+\\))?)?")) { return C.TYPE_SS; } else { return C.TYPE_OTHER; @@ -870,30 +1609,6 @@ public final class Util { : formatter.format("%02d:%02d", minutes, seconds).toString(); } - /** - * Maps a {@link C} {@code TRACK_TYPE_*} constant to the corresponding {@link C} - * {@code DEFAULT_*_BUFFER_SIZE} constant. - * - * @param trackType The track type. - * @return The corresponding default buffer size in bytes. - */ - public static int getDefaultBufferSize(int trackType) { - switch (trackType) { - case C.TRACK_TYPE_DEFAULT: - return C.DEFAULT_MUXED_BUFFER_SIZE; - case C.TRACK_TYPE_AUDIO: - return C.DEFAULT_AUDIO_BUFFER_SIZE; - case C.TRACK_TYPE_VIDEO: - return C.DEFAULT_VIDEO_BUFFER_SIZE; - case C.TRACK_TYPE_TEXT: - return C.DEFAULT_TEXT_BUFFER_SIZE; - case C.TRACK_TYPE_METADATA: - return C.DEFAULT_METADATA_BUFFER_SIZE; - default: - throw new IllegalStateException(); - } - } - /** * Escapes a string so that it's safe for use as a file or directory name on at least FAT32 * filesystems. FAT32 is the most restrictive of all filesystems still commonly used today. @@ -962,7 +1677,7 @@ public final class Util { * @return The original value of the file name before it was escaped, or null if the escaped * fileName seems invalid. */ - public static String unescapeFileName(String fileName) { + public static @Nullable String unescapeFileName(String fileName) { int length = fileName.length(); int percentCharacterCount = 0; for (int i = 0; i < length; i++) { @@ -977,15 +1692,15 @@ public final class Util { int expectedLength = length - percentCharacterCount * 2; StringBuilder builder = new StringBuilder(expectedLength); Matcher matcher = ESCAPED_CHARACTER_PATTERN.matcher(fileName); - int endOfLastMatch = 0; + int startOfNotEscaped = 0; while (percentCharacterCount > 0 && matcher.find()) { char unescapedCharacter = (char) Integer.parseInt(matcher.group(1), 16); - builder.append(fileName, endOfLastMatch, matcher.start()).append(unescapedCharacter); - endOfLastMatch = matcher.end(); + builder.append(fileName, startOfNotEscaped, matcher.start()).append(unescapedCharacter); + startOfNotEscaped = matcher.end(); percentCharacterCount--; } - if (endOfLastMatch < length) { - builder.append(fileName, endOfLastMatch, length); + if (startOfNotEscaped < length) { + builder.append(fileName, startOfNotEscaped, length); } if (builder.length() != expectedLength) { return null; @@ -998,7 +1713,7 @@ public final class Util { * and is not declared to be thrown. */ public static void sneakyThrow(Throwable t) { - Util.sneakyThrowInternal(t); + sneakyThrowInternal(t); } @SuppressWarnings("unchecked") @@ -1008,8 +1723,9 @@ public final class Util { /** Recursively deletes a directory and its content. */ public static void recursiveDelete(File fileOrDirectory) { - if (fileOrDirectory.isDirectory()) { - for (File child : fileOrDirectory.listFiles()) { + File[] directoryFiles = fileOrDirectory.listFiles(); + if (directoryFiles != null) { + for (File child : directoryFiles) { recursiveDelete(child); } } @@ -1018,15 +1734,20 @@ public final class Util { /** Creates an empty directory in the directory returned by {@link Context#getCacheDir()}. */ public static File createTempDirectory(Context context, String prefix) throws IOException { - File tempFile = File.createTempFile(prefix, null, context.getCacheDir()); + File tempFile = createTempFile(context, prefix); tempFile.delete(); // Delete the temp file. tempFile.mkdir(); // Create a directory with the same name. return tempFile; } + /** Creates a new empty file in the directory returned by {@link Context#getCacheDir()}. */ + public static File createTempFile(Context context, String prefix) throws IOException { + return File.createTempFile(prefix, null, context.getCacheDir()); + } + /** - * Returns the result of updating a CRC with the specified bytes in a "most significant bit first" - * order. + * Returns the result of updating a CRC-32 with the specified bytes in a "most significant bit + * first" order. * * @param bytes Array containing the bytes to update the crc value with. * @param start The index to the first byte in the byte range to update the crc with. @@ -1034,7 +1755,7 @@ public final class Util { * @param initialValue The initial value for the crc calculation. * @return The result of updating the initial value with the specified bytes. */ - public static int crc(byte[] bytes, int start, int end, int initialValue) { + public static int crc32(byte[] bytes, int start, int end, int initialValue) { for (int i = start; i < end; i++) { initialValue = (initialValue << 8) ^ CRC32_BYTES_MSBF[((initialValue >>> 24) ^ (bytes[i] & 0xFF)) & 0xFF]; @@ -1043,56 +1764,224 @@ public final class Util { } /** - * Gets the physical size of the default display, in pixels. + * Returns the result of updating a CRC-8 with the specified bytes in a "most significant bit + * first" order. * - * @param context Any context. - * @return The physical display size, in pixels. + * @param bytes Array containing the bytes to update the crc value with. + * @param start The index to the first byte in the byte range to update the crc with. + * @param end The index after the last byte in the byte range to update the crc with. + * @param initialValue The initial value for the crc calculation. + * @return The result of updating the initial value with the specified bytes. */ - public static Point getPhysicalDisplaySize(Context context) { - WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - return getPhysicalDisplaySize(context, windowManager.getDefaultDisplay()); + public static int crc8(byte[] bytes, int start, int end, int initialValue) { + for (int i = start; i < end; i++) { + initialValue = CRC8_BYTES_MSBF[initialValue ^ (bytes[i] & 0xFF)]; + } + return initialValue; } /** - * Gets the physical size of the specified display, in pixels. + * Returns the {@link C.NetworkType} of the current network connection. + * + * @param context A context to access the connectivity manager. + * @return The {@link C.NetworkType} of the current network connection. + */ + @C.NetworkType + public static int getNetworkType(Context context) { + if (context == null) { + // Note: This is for backward compatibility only (context used to be @Nullable). + return C.NETWORK_TYPE_UNKNOWN; + } + NetworkInfo networkInfo; + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivityManager == null) { + return C.NETWORK_TYPE_UNKNOWN; + } + try { + networkInfo = connectivityManager.getActiveNetworkInfo(); + } catch (SecurityException e) { + // Expected if permission was revoked. + return C.NETWORK_TYPE_UNKNOWN; + } + if (networkInfo == null || !networkInfo.isConnected()) { + return C.NETWORK_TYPE_OFFLINE; + } + switch (networkInfo.getType()) { + case ConnectivityManager.TYPE_WIFI: + return C.NETWORK_TYPE_WIFI; + case ConnectivityManager.TYPE_WIMAX: + return C.NETWORK_TYPE_4G; + case ConnectivityManager.TYPE_MOBILE: + case ConnectivityManager.TYPE_MOBILE_DUN: + case ConnectivityManager.TYPE_MOBILE_HIPRI: + return getMobileNetworkType(networkInfo); + case ConnectivityManager.TYPE_ETHERNET: + return C.NETWORK_TYPE_ETHERNET; + default: // VPN, Bluetooth, Dummy. + return C.NETWORK_TYPE_OTHER; + } + } + + /** + * Returns the upper-case ISO 3166-1 alpha-2 country code of the current registered operator's MCC + * (Mobile Country Code), or the country code of the default Locale if not available. + * + * @param context A context to access the telephony service. If null, only the Locale can be used. + * @return The upper-case ISO 3166-1 alpha-2 country code, or an empty String if unavailable. + */ + public static String getCountryCode(@Nullable Context context) { + if (context != null) { + TelephonyManager telephonyManager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + if (telephonyManager != null) { + String countryCode = telephonyManager.getNetworkCountryIso(); + if (!TextUtils.isEmpty(countryCode)) { + return toUpperInvariant(countryCode); + } + } + } + return toUpperInvariant(Locale.getDefault().getCountry()); + } + + /** + * Returns a non-empty array of normalized IETF BCP 47 language tags for the system languages + * ordered by preference. + */ + public static String[] getSystemLanguageCodes() { + String[] systemLocales = getSystemLocales(); + for (int i = 0; i < systemLocales.length; i++) { + systemLocales[i] = normalizeLanguageCode(systemLocales[i]); + } + return systemLocales; + } + + /** + * Uncompresses the data in {@code input}. + * + * @param input Wraps the compressed input data. + * @param output Wraps an output buffer to be used to store the uncompressed data. If {@code + * output.data} isn't big enough to hold the uncompressed data, a new array is created. If + * {@code true} is returned then the output's position will be set to 0 and its limit will be + * set to the length of the uncompressed data. + * @param inflater If not null, used to uncompressed the input. Otherwise a new {@link Inflater} + * is created. + * @return Whether the input is uncompressed successfully. + */ + public static boolean inflate( + ParsableByteArray input, ParsableByteArray output, @Nullable Inflater inflater) { + if (input.bytesLeft() <= 0) { + return false; + } + byte[] outputData = output.data; + if (outputData.length < input.bytesLeft()) { + outputData = new byte[2 * input.bytesLeft()]; + } + if (inflater == null) { + inflater = new Inflater(); + } + inflater.setInput(input.data, input.getPosition(), input.bytesLeft()); + try { + int outputSize = 0; + while (true) { + outputSize += inflater.inflate(outputData, outputSize, outputData.length - outputSize); + if (inflater.finished()) { + output.reset(outputData, outputSize); + return true; + } + if (inflater.needsDictionary() || inflater.needsInput()) { + return false; + } + if (outputSize == outputData.length) { + outputData = Arrays.copyOf(outputData, outputData.length * 2); + } + } + } catch (DataFormatException e) { + return false; + } finally { + inflater.reset(); + } + } + + /** + * Returns whether the app is running on a TV device. + * + * @param context Any context. + * @return Whether the app is running on a TV device. + */ + public static boolean isTv(Context context) { + // See https://developer.android.com/training/tv/start/hardware.html#runtime-check. + UiModeManager uiModeManager = + (UiModeManager) context.getApplicationContext().getSystemService(UI_MODE_SERVICE); + return uiModeManager != null + && uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION; + } + + /** + * Gets the size of the current mode of the default display, in pixels. + * + *

Note that due to application UI scaling, the number of pixels made available to applications + * (as reported by {@link Display#getSize(Point)} may differ from the mode's actual resolution (as + * reported by this function). For example, applications running on a display configured with a 4K + * mode may have their UI laid out and rendered in 1080p and then scaled up. Applications can take + * advantage of the full mode resolution through a {@link SurfaceView} using full size buffers. + * + * @param context Any context. + * @return The size of the current mode, in pixels. + */ + public static Point getCurrentDisplayModeSize(Context context) { + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + return getCurrentDisplayModeSize(context, windowManager.getDefaultDisplay()); + } + + /** + * Gets the size of the current mode of the specified display, in pixels. + * + *

Note that due to application UI scaling, the number of pixels made available to applications + * (as reported by {@link Display#getSize(Point)} may differ from the mode's actual resolution (as + * reported by this function). For example, applications running on a display configured with a 4K + * mode may have their UI laid out and rendered in 1080p and then scaled up. Applications can take + * advantage of the full mode resolution through a {@link SurfaceView} using full size buffers. * * @param context Any context. * @param display The display whose size is to be returned. - * @return The physical display size, in pixels. + * @return The size of the current mode, in pixels. */ - public static Point getPhysicalDisplaySize(Context context, Display display) { - if (Util.SDK_INT < 25 && display.getDisplayId() == Display.DEFAULT_DISPLAY) { - // Before API 25 the Display object does not provide a working way to identify Android TVs - // that can show 4k resolution in a SurfaceView, so check for supported devices here. - if ("Sony".equals(Util.MANUFACTURER) && Util.MODEL.startsWith("BRAVIA") + public static Point getCurrentDisplayModeSize(Context context, Display display) { + if (Util.SDK_INT <= 29 && display.getDisplayId() == Display.DEFAULT_DISPLAY && isTv(context)) { + // On Android TVs it is common for the UI to be configured for a lower resolution than + // SurfaceViews can output. Before API 26 the Display object does not provide a way to + // identify this case, and up to and including API 28 many devices still do not correctly set + // their hardware compositor output size. + + // Sony Android TVs advertise support for 4k output via a system feature. + if ("Sony".equals(Util.MANUFACTURER) + && Util.MODEL.startsWith("BRAVIA") && context.getPackageManager().hasSystemFeature("com.sony.dtv.hardware.panel.qfhd")) { return new Point(3840, 2160); - } else if ("NVIDIA".equals(Util.MANUFACTURER) && Util.MODEL.contains("SHIELD")) { - // Attempt to read sys.display-size. - String sysDisplaySize = null; + } + + // Otherwise check the system property for display size. From API 28 treble may prevent the + // system from writing sys.display-size so we check vendor.display-size instead. + String displaySize = + Util.SDK_INT < 28 + ? getSystemProperty("sys.display-size") + : getSystemProperty("vendor.display-size"); + // If we managed to read the display size, attempt to parse it. + if (!TextUtils.isEmpty(displaySize)) { try { - Class systemProperties = Class.forName("android.os.SystemProperties"); - Method getMethod = systemProperties.getMethod("get", String.class); - sysDisplaySize = (String) getMethod.invoke(systemProperties, "sys.display-size"); - } catch (Exception e) { - Log.e(TAG, "Failed to read sys.display-size", e); - } - // If we managed to read sys.display-size, attempt to parse it. - if (!TextUtils.isEmpty(sysDisplaySize)) { - try { - String[] sysDisplaySizeParts = sysDisplaySize.trim().split("x"); - if (sysDisplaySizeParts.length == 2) { - int width = Integer.parseInt(sysDisplaySizeParts[0]); - int height = Integer.parseInt(sysDisplaySizeParts[1]); - if (width > 0 && height > 0) { - return new Point(width, height); - } + String[] displaySizeParts = split(displaySize.trim(), "x"); + if (displaySizeParts.length == 2) { + int width = Integer.parseInt(displaySizeParts[0]); + int height = Integer.parseInt(displaySizeParts[1]); + if (width > 0 && height > 0) { + return new Point(width, height); } - } catch (NumberFormatException e) { - // Do nothing. } - Log.e(TAG, "Invalid sys.display-size: " + sysDisplaySize); + } catch (NumberFormatException e) { + // Do nothing. } + Log.e(TAG, "Invalid display size: " + displaySize); } } @@ -1101,14 +1990,75 @@ public final class Util { getDisplaySizeV23(display, displaySize); } else if (Util.SDK_INT >= 17) { getDisplaySizeV17(display, displaySize); - } else if (Util.SDK_INT >= 16) { - getDisplaySizeV16(display, displaySize); } else { - getDisplaySizeV9(display, displaySize); + getDisplaySizeV16(display, displaySize); } return displaySize; } + /** + * Extract renderer capabilities for the renderers created by the provided renderers factory. + * + * @param renderersFactory A {@link RenderersFactory}. + * @return The {@link RendererCapabilities} for each renderer created by the {@code + * renderersFactory}. + */ + public static RendererCapabilities[] getRendererCapabilities(RenderersFactory renderersFactory) { + Renderer[] renderers = + renderersFactory.createRenderers( + new Handler(), + new VideoRendererEventListener() {}, + new AudioRendererEventListener() {}, + (cues) -> {}, + (metadata) -> {}, + /* drmSessionManager= */ null); + RendererCapabilities[] capabilities = new RendererCapabilities[renderers.length]; + for (int i = 0; i < renderers.length; i++) { + capabilities[i] = renderers[i].getCapabilities(); + } + return capabilities; + } + + /** + * Returns a string representation of a {@code TRACK_TYPE_*} constant defined in {@link C}. + * + * @param trackType A {@code TRACK_TYPE_*} constant, + * @return A string representation of this constant. + */ + public static String getTrackTypeString(int trackType) { + switch (trackType) { + case C.TRACK_TYPE_AUDIO: + return "audio"; + case C.TRACK_TYPE_DEFAULT: + return "default"; + case C.TRACK_TYPE_METADATA: + return "metadata"; + case C.TRACK_TYPE_CAMERA_MOTION: + return "camera motion"; + case C.TRACK_TYPE_NONE: + return "none"; + case C.TRACK_TYPE_TEXT: + return "text"; + case C.TRACK_TYPE_VIDEO: + return "video"; + default: + return trackType >= C.TRACK_TYPE_CUSTOM_BASE ? "custom (" + trackType + ")" : "?"; + } + } + + @Nullable + private static String getSystemProperty(String name) { + try { + @SuppressLint("PrivateApi") + Class systemProperties = Class.forName("android.os.SystemProperties"); + Method getMethod = systemProperties.getMethod("get", String.class); + return (String) getMethod.invoke(systemProperties, name); + } catch (Exception e) { + Log.e(TAG, "Failed to read system property " + name, e); + return null; + } + } + @TargetApi(23) private static void getDisplaySizeV23(Display display, Point outSize) { Display.Mode mode = display.getMode(); @@ -1121,59 +2071,228 @@ public final class Util { display.getRealSize(outSize); } - @TargetApi(16) private static void getDisplaySizeV16(Display display, Point outSize) { display.getSize(outSize); } - @SuppressWarnings("deprecation") - private static void getDisplaySizeV9(Display display, Point outSize) { - outSize.x = display.getWidth(); - outSize.y = display.getHeight(); + private static String[] getSystemLocales() { + Configuration config = Resources.getSystem().getConfiguration(); + return SDK_INT >= 24 + ? getSystemLocalesV24(config) + : new String[] {getLocaleLanguageTag(config.locale)}; } + @TargetApi(24) + private static String[] getSystemLocalesV24(Configuration config) { + return Util.split(config.getLocales().toLanguageTags(), ","); + } + + @TargetApi(21) + private static String getLocaleLanguageTagV21(Locale locale) { + return locale.toLanguageTag(); + } + + private static @C.NetworkType int getMobileNetworkType(NetworkInfo networkInfo) { + switch (networkInfo.getSubtype()) { + case TelephonyManager.NETWORK_TYPE_EDGE: + case TelephonyManager.NETWORK_TYPE_GPRS: + return C.NETWORK_TYPE_2G; + case TelephonyManager.NETWORK_TYPE_1xRTT: + case TelephonyManager.NETWORK_TYPE_CDMA: + case TelephonyManager.NETWORK_TYPE_EVDO_0: + case TelephonyManager.NETWORK_TYPE_EVDO_A: + case TelephonyManager.NETWORK_TYPE_EVDO_B: + case TelephonyManager.NETWORK_TYPE_HSDPA: + case TelephonyManager.NETWORK_TYPE_HSPA: + case TelephonyManager.NETWORK_TYPE_HSUPA: + case TelephonyManager.NETWORK_TYPE_IDEN: + case TelephonyManager.NETWORK_TYPE_UMTS: + case TelephonyManager.NETWORK_TYPE_EHRPD: + case TelephonyManager.NETWORK_TYPE_HSPAP: + case TelephonyManager.NETWORK_TYPE_TD_SCDMA: + return C.NETWORK_TYPE_3G; + case TelephonyManager.NETWORK_TYPE_LTE: + return C.NETWORK_TYPE_4G; + case TelephonyManager.NETWORK_TYPE_NR: + return C.NETWORK_TYPE_5G; + case TelephonyManager.NETWORK_TYPE_IWLAN: + return C.NETWORK_TYPE_WIFI; + case TelephonyManager.NETWORK_TYPE_GSM: + case TelephonyManager.NETWORK_TYPE_UNKNOWN: + default: // Future mobile network types. + return C.NETWORK_TYPE_CELLULAR_UNKNOWN; + } + } + + private static HashMap createIsoLanguageReplacementMap() { + String[] iso2Languages = Locale.getISOLanguages(); + HashMap replacedLanguages = + new HashMap<>( + /* initialCapacity= */ iso2Languages.length + additionalIsoLanguageReplacements.length); + for (String iso2 : iso2Languages) { + try { + // This returns the ISO 639-2/T code for the language. + String iso3 = new Locale(iso2).getISO3Language(); + if (!TextUtils.isEmpty(iso3)) { + replacedLanguages.put(iso3, iso2); + } + } catch (MissingResourceException e) { + // Shouldn't happen for list of known languages, but we don't want to throw either. + } + } + // Add additional replacement mappings. + for (int i = 0; i < additionalIsoLanguageReplacements.length; i += 2) { + replacedLanguages.put( + additionalIsoLanguageReplacements[i], additionalIsoLanguageReplacements[i + 1]); + } + return replacedLanguages; + } + + private static String maybeReplaceGrandfatheredLanguageTags(String languageTag) { + for (int i = 0; i < isoGrandfatheredTagReplacements.length; i += 2) { + if (languageTag.startsWith(isoGrandfatheredTagReplacements[i])) { + return isoGrandfatheredTagReplacements[i + 1] + + languageTag.substring(/* beginIndex= */ isoGrandfatheredTagReplacements[i].length()); + } + } + return languageTag; + } + + // Additional mapping from ISO3 to ISO2 language codes. + private static final String[] additionalIsoLanguageReplacements = + new String[] { + // Bibliographical codes defined in ISO 639-2/B, replaced by terminological code defined in + // ISO 639-2/T. See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. + "alb", "sq", + "arm", "hy", + "baq", "eu", + "bur", "my", + "tib", "bo", + "chi", "zh", + "cze", "cs", + "dut", "nl", + "ger", "de", + "gre", "el", + "fre", "fr", + "geo", "ka", + "ice", "is", + "mac", "mk", + "mao", "mi", + "may", "ms", + "per", "fa", + "rum", "ro", + "scc", "hbs-srp", + "slo", "sk", + "wel", "cy", + // Deprecated 2-letter codes, replaced by modern equivalent (including macrolanguage) + // See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes, "ISO 639:1988" + "id", "ms-ind", + "iw", "he", + "heb", "he", + "ji", "yi", + // Individual macrolanguage codes mapped back to full macrolanguage code. + // See https://en.wikipedia.org/wiki/ISO_639_macrolanguage + "in", "ms-ind", + "ind", "ms-ind", + "nb", "no-nob", + "nob", "no-nob", + "nn", "no-nno", + "nno", "no-nno", + "tw", "ak-twi", + "twi", "ak-twi", + "bs", "hbs-bos", + "bos", "hbs-bos", + "hr", "hbs-hrv", + "hrv", "hbs-hrv", + "sr", "hbs-srp", + "srp", "hbs-srp", + "cmn", "zh-cmn", + "hak", "zh-hak", + "nan", "zh-nan", + "hsn", "zh-hsn" + }; + + // "Grandfathered tags", replaced by modern equivalents (including macrolanguage) + // See https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry. + private static final String[] isoGrandfatheredTagReplacements = + new String[] { + "i-lux", "lb", + "i-hak", "zh-hak", + "i-navajo", "nv", + "no-bok", "no-nob", + "no-nyn", "no-nno", + "zh-guoyu", "zh-cmn", + "zh-hakka", "zh-hak", + "zh-min-nan", "zh-nan", + "zh-xiang", "zh-hsn" + }; + /** - * Allows the CRC calculation to be done byte by byte instead of bit per bit being the order - * "most significant bit first". + * Allows the CRC-32 calculation to be done byte by byte instead of bit per bit in the order "most + * significant bit first". */ private static final int[] CRC32_BYTES_MSBF = { - 0X00000000, 0X04C11DB7, 0X09823B6E, 0X0D4326D9, 0X130476DC, 0X17C56B6B, 0X1A864DB2, - 0X1E475005, 0X2608EDB8, 0X22C9F00F, 0X2F8AD6D6, 0X2B4BCB61, 0X350C9B64, 0X31CD86D3, - 0X3C8EA00A, 0X384FBDBD, 0X4C11DB70, 0X48D0C6C7, 0X4593E01E, 0X4152FDA9, 0X5F15ADAC, - 0X5BD4B01B, 0X569796C2, 0X52568B75, 0X6A1936C8, 0X6ED82B7F, 0X639B0DA6, 0X675A1011, - 0X791D4014, 0X7DDC5DA3, 0X709F7B7A, 0X745E66CD, 0X9823B6E0, 0X9CE2AB57, 0X91A18D8E, - 0X95609039, 0X8B27C03C, 0X8FE6DD8B, 0X82A5FB52, 0X8664E6E5, 0XBE2B5B58, 0XBAEA46EF, - 0XB7A96036, 0XB3687D81, 0XAD2F2D84, 0XA9EE3033, 0XA4AD16EA, 0XA06C0B5D, 0XD4326D90, - 0XD0F37027, 0XDDB056FE, 0XD9714B49, 0XC7361B4C, 0XC3F706FB, 0XCEB42022, 0XCA753D95, - 0XF23A8028, 0XF6FB9D9F, 0XFBB8BB46, 0XFF79A6F1, 0XE13EF6F4, 0XE5FFEB43, 0XE8BCCD9A, - 0XEC7DD02D, 0X34867077, 0X30476DC0, 0X3D044B19, 0X39C556AE, 0X278206AB, 0X23431B1C, - 0X2E003DC5, 0X2AC12072, 0X128E9DCF, 0X164F8078, 0X1B0CA6A1, 0X1FCDBB16, 0X018AEB13, - 0X054BF6A4, 0X0808D07D, 0X0CC9CDCA, 0X7897AB07, 0X7C56B6B0, 0X71159069, 0X75D48DDE, - 0X6B93DDDB, 0X6F52C06C, 0X6211E6B5, 0X66D0FB02, 0X5E9F46BF, 0X5A5E5B08, 0X571D7DD1, - 0X53DC6066, 0X4D9B3063, 0X495A2DD4, 0X44190B0D, 0X40D816BA, 0XACA5C697, 0XA864DB20, - 0XA527FDF9, 0XA1E6E04E, 0XBFA1B04B, 0XBB60ADFC, 0XB6238B25, 0XB2E29692, 0X8AAD2B2F, - 0X8E6C3698, 0X832F1041, 0X87EE0DF6, 0X99A95DF3, 0X9D684044, 0X902B669D, 0X94EA7B2A, - 0XE0B41DE7, 0XE4750050, 0XE9362689, 0XEDF73B3E, 0XF3B06B3B, 0XF771768C, 0XFA325055, - 0XFEF34DE2, 0XC6BCF05F, 0XC27DEDE8, 0XCF3ECB31, 0XCBFFD686, 0XD5B88683, 0XD1799B34, - 0XDC3ABDED, 0XD8FBA05A, 0X690CE0EE, 0X6DCDFD59, 0X608EDB80, 0X644FC637, 0X7A089632, - 0X7EC98B85, 0X738AAD5C, 0X774BB0EB, 0X4F040D56, 0X4BC510E1, 0X46863638, 0X42472B8F, - 0X5C007B8A, 0X58C1663D, 0X558240E4, 0X51435D53, 0X251D3B9E, 0X21DC2629, 0X2C9F00F0, - 0X285E1D47, 0X36194D42, 0X32D850F5, 0X3F9B762C, 0X3B5A6B9B, 0X0315D626, 0X07D4CB91, - 0X0A97ED48, 0X0E56F0FF, 0X1011A0FA, 0X14D0BD4D, 0X19939B94, 0X1D528623, 0XF12F560E, - 0XF5EE4BB9, 0XF8AD6D60, 0XFC6C70D7, 0XE22B20D2, 0XE6EA3D65, 0XEBA91BBC, 0XEF68060B, - 0XD727BBB6, 0XD3E6A601, 0XDEA580D8, 0XDA649D6F, 0XC423CD6A, 0XC0E2D0DD, 0XCDA1F604, - 0XC960EBB3, 0XBD3E8D7E, 0XB9FF90C9, 0XB4BCB610, 0XB07DABA7, 0XAE3AFBA2, 0XAAFBE615, - 0XA7B8C0CC, 0XA379DD7B, 0X9B3660C6, 0X9FF77D71, 0X92B45BA8, 0X9675461F, 0X8832161A, - 0X8CF30BAD, 0X81B02D74, 0X857130C3, 0X5D8A9099, 0X594B8D2E, 0X5408ABF7, 0X50C9B640, - 0X4E8EE645, 0X4A4FFBF2, 0X470CDD2B, 0X43CDC09C, 0X7B827D21, 0X7F436096, 0X7200464F, - 0X76C15BF8, 0X68860BFD, 0X6C47164A, 0X61043093, 0X65C52D24, 0X119B4BE9, 0X155A565E, - 0X18197087, 0X1CD86D30, 0X029F3D35, 0X065E2082, 0X0B1D065B, 0X0FDC1BEC, 0X3793A651, - 0X3352BBE6, 0X3E119D3F, 0X3AD08088, 0X2497D08D, 0X2056CD3A, 0X2D15EBE3, 0X29D4F654, - 0XC5A92679, 0XC1683BCE, 0XCC2B1D17, 0XC8EA00A0, 0XD6AD50A5, 0XD26C4D12, 0XDF2F6BCB, - 0XDBEE767C, 0XE3A1CBC1, 0XE760D676, 0XEA23F0AF, 0XEEE2ED18, 0XF0A5BD1D, 0XF464A0AA, - 0XF9278673, 0XFDE69BC4, 0X89B8FD09, 0X8D79E0BE, 0X803AC667, 0X84FBDBD0, 0X9ABC8BD5, - 0X9E7D9662, 0X933EB0BB, 0X97FFAD0C, 0XAFB010B1, 0XAB710D06, 0XA6322BDF, 0XA2F33668, - 0XBCB4666D, 0XB8757BDA, 0XB5365D03, 0XB1F740B4 + 0X00000000, 0X04C11DB7, 0X09823B6E, 0X0D4326D9, 0X130476DC, 0X17C56B6B, 0X1A864DB2, + 0X1E475005, 0X2608EDB8, 0X22C9F00F, 0X2F8AD6D6, 0X2B4BCB61, 0X350C9B64, 0X31CD86D3, + 0X3C8EA00A, 0X384FBDBD, 0X4C11DB70, 0X48D0C6C7, 0X4593E01E, 0X4152FDA9, 0X5F15ADAC, + 0X5BD4B01B, 0X569796C2, 0X52568B75, 0X6A1936C8, 0X6ED82B7F, 0X639B0DA6, 0X675A1011, + 0X791D4014, 0X7DDC5DA3, 0X709F7B7A, 0X745E66CD, 0X9823B6E0, 0X9CE2AB57, 0X91A18D8E, + 0X95609039, 0X8B27C03C, 0X8FE6DD8B, 0X82A5FB52, 0X8664E6E5, 0XBE2B5B58, 0XBAEA46EF, + 0XB7A96036, 0XB3687D81, 0XAD2F2D84, 0XA9EE3033, 0XA4AD16EA, 0XA06C0B5D, 0XD4326D90, + 0XD0F37027, 0XDDB056FE, 0XD9714B49, 0XC7361B4C, 0XC3F706FB, 0XCEB42022, 0XCA753D95, + 0XF23A8028, 0XF6FB9D9F, 0XFBB8BB46, 0XFF79A6F1, 0XE13EF6F4, 0XE5FFEB43, 0XE8BCCD9A, + 0XEC7DD02D, 0X34867077, 0X30476DC0, 0X3D044B19, 0X39C556AE, 0X278206AB, 0X23431B1C, + 0X2E003DC5, 0X2AC12072, 0X128E9DCF, 0X164F8078, 0X1B0CA6A1, 0X1FCDBB16, 0X018AEB13, + 0X054BF6A4, 0X0808D07D, 0X0CC9CDCA, 0X7897AB07, 0X7C56B6B0, 0X71159069, 0X75D48DDE, + 0X6B93DDDB, 0X6F52C06C, 0X6211E6B5, 0X66D0FB02, 0X5E9F46BF, 0X5A5E5B08, 0X571D7DD1, + 0X53DC6066, 0X4D9B3063, 0X495A2DD4, 0X44190B0D, 0X40D816BA, 0XACA5C697, 0XA864DB20, + 0XA527FDF9, 0XA1E6E04E, 0XBFA1B04B, 0XBB60ADFC, 0XB6238B25, 0XB2E29692, 0X8AAD2B2F, + 0X8E6C3698, 0X832F1041, 0X87EE0DF6, 0X99A95DF3, 0X9D684044, 0X902B669D, 0X94EA7B2A, + 0XE0B41DE7, 0XE4750050, 0XE9362689, 0XEDF73B3E, 0XF3B06B3B, 0XF771768C, 0XFA325055, + 0XFEF34DE2, 0XC6BCF05F, 0XC27DEDE8, 0XCF3ECB31, 0XCBFFD686, 0XD5B88683, 0XD1799B34, + 0XDC3ABDED, 0XD8FBA05A, 0X690CE0EE, 0X6DCDFD59, 0X608EDB80, 0X644FC637, 0X7A089632, + 0X7EC98B85, 0X738AAD5C, 0X774BB0EB, 0X4F040D56, 0X4BC510E1, 0X46863638, 0X42472B8F, + 0X5C007B8A, 0X58C1663D, 0X558240E4, 0X51435D53, 0X251D3B9E, 0X21DC2629, 0X2C9F00F0, + 0X285E1D47, 0X36194D42, 0X32D850F5, 0X3F9B762C, 0X3B5A6B9B, 0X0315D626, 0X07D4CB91, + 0X0A97ED48, 0X0E56F0FF, 0X1011A0FA, 0X14D0BD4D, 0X19939B94, 0X1D528623, 0XF12F560E, + 0XF5EE4BB9, 0XF8AD6D60, 0XFC6C70D7, 0XE22B20D2, 0XE6EA3D65, 0XEBA91BBC, 0XEF68060B, + 0XD727BBB6, 0XD3E6A601, 0XDEA580D8, 0XDA649D6F, 0XC423CD6A, 0XC0E2D0DD, 0XCDA1F604, + 0XC960EBB3, 0XBD3E8D7E, 0XB9FF90C9, 0XB4BCB610, 0XB07DABA7, 0XAE3AFBA2, 0XAAFBE615, + 0XA7B8C0CC, 0XA379DD7B, 0X9B3660C6, 0X9FF77D71, 0X92B45BA8, 0X9675461F, 0X8832161A, + 0X8CF30BAD, 0X81B02D74, 0X857130C3, 0X5D8A9099, 0X594B8D2E, 0X5408ABF7, 0X50C9B640, + 0X4E8EE645, 0X4A4FFBF2, 0X470CDD2B, 0X43CDC09C, 0X7B827D21, 0X7F436096, 0X7200464F, + 0X76C15BF8, 0X68860BFD, 0X6C47164A, 0X61043093, 0X65C52D24, 0X119B4BE9, 0X155A565E, + 0X18197087, 0X1CD86D30, 0X029F3D35, 0X065E2082, 0X0B1D065B, 0X0FDC1BEC, 0X3793A651, + 0X3352BBE6, 0X3E119D3F, 0X3AD08088, 0X2497D08D, 0X2056CD3A, 0X2D15EBE3, 0X29D4F654, + 0XC5A92679, 0XC1683BCE, 0XCC2B1D17, 0XC8EA00A0, 0XD6AD50A5, 0XD26C4D12, 0XDF2F6BCB, + 0XDBEE767C, 0XE3A1CBC1, 0XE760D676, 0XEA23F0AF, 0XEEE2ED18, 0XF0A5BD1D, 0XF464A0AA, + 0XF9278673, 0XFDE69BC4, 0X89B8FD09, 0X8D79E0BE, 0X803AC667, 0X84FBDBD0, 0X9ABC8BD5, + 0X9E7D9662, 0X933EB0BB, 0X97FFAD0C, 0XAFB010B1, 0XAB710D06, 0XA6322BDF, 0XA2F33668, + 0XBCB4666D, 0XB8757BDA, 0XB5365D03, 0XB1F740B4 }; + /** + * Allows the CRC-8 calculation to be done byte by byte instead of bit per bit in the order "most + * significant bit first". + */ + private static final int[] CRC8_BYTES_MSBF = { + 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, + 0x2D, 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, + 0x5A, 0x5D, 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, + 0xC3, 0xCA, 0xCD, 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, + 0xB4, 0xB3, 0xBA, 0xBD, 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, + 0xF6, 0xE3, 0xE4, 0xED, 0xEA, 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, + 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, + 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A, 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, + 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, + 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, + 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4, 0x69, 0x6E, 0x67, 0x60, 0x75, + 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, 0x19, 0x1E, 0x17, 0x10, + 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, 0x4E, 0x49, 0x40, + 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63, 0x3E, 0x39, + 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, 0xAE, + 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83, + 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, + 0xF3 + }; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/XmlPullParserUtil.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/XmlPullParserUtil.java index e3c129e35e6a..7b56886dbac0 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/XmlPullParserUtil.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/XmlPullParserUtil.java @@ -15,6 +15,7 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.util; +import androidx.annotation.Nullable; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -56,8 +57,7 @@ public final class XmlPullParserUtil { * @return Whether the current event is a start tag with the specified name. * @throws XmlPullParserException If an error occurs querying the parser. */ - public static boolean isStartTag(XmlPullParser xpp, String name) - throws XmlPullParserException { + public static boolean isStartTag(XmlPullParser xpp, String name) throws XmlPullParserException { return isStartTag(xpp) && xpp.getName().equals(name); } @@ -72,22 +72,60 @@ public final class XmlPullParserUtil { return xpp.getEventType() == XmlPullParser.START_TAG; } + /** + * Returns whether the current event is a start tag with the specified name. If the current event + * has a raw name then its prefix is stripped before matching. + * + * @param xpp The {@link XmlPullParser} to query. + * @param name The specified name. + * @return Whether the current event is a start tag with the specified name. + * @throws XmlPullParserException If an error occurs querying the parser. + */ + public static boolean isStartTagIgnorePrefix(XmlPullParser xpp, String name) + throws XmlPullParserException { + return isStartTag(xpp) && stripPrefix(xpp.getName()).equals(name); + } + /** * Returns the value of an attribute of the current start tag. * * @param xpp The {@link XmlPullParser} to query. * @param attributeName The name of the attribute. * @return The value of the attribute, or null if the current event is not a start tag or if no - * no such attribute was found. + * such attribute was found. */ - public static String getAttributeValue(XmlPullParser xpp, String attributeName) { + public static @Nullable String getAttributeValue(XmlPullParser xpp, String attributeName) { int attributeCount = xpp.getAttributeCount(); for (int i = 0; i < attributeCount; i++) { - if (attributeName.equals(xpp.getAttributeName(i))) { + if (xpp.getAttributeName(i).equals(attributeName)) { return xpp.getAttributeValue(i); } } return null; } + /** + * Returns the value of an attribute of the current start tag. Any raw attribute names in the + * current start tag have their prefixes stripped before matching. + * + * @param xpp The {@link XmlPullParser} to query. + * @param attributeName The name of the attribute. + * @return The value of the attribute, or null if the current event is not a start tag or if no + * such attribute was found. + */ + public static @Nullable String getAttributeValueIgnorePrefix( + XmlPullParser xpp, String attributeName) { + int attributeCount = xpp.getAttributeCount(); + for (int i = 0; i < attributeCount; i++) { + if (stripPrefix(xpp.getAttributeName(i)).equals(attributeName)) { + return xpp.getAttributeValue(i); + } + } + return null; + } + + private static String stripPrefix(String name) { + int prefixSeparatorIndex = name.indexOf(':'); + return prefixSeparatorIndex == -1 ? name : name.substring(prefixSeparatorIndex + 1); + } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/package-info.java new file mode 100644 index 000000000000..49ee4a4d4d5e --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/package-info.java @@ -0,0 +1,17 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.util; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/ColorInfo.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/ColorInfo.java index d4fc7b28efe3..7eed4e3eafe5 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/ColorInfo.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/ColorInfo.java @@ -17,8 +17,10 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.video; import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import java.util.Arrays; /** @@ -48,10 +50,8 @@ public final class ColorInfo implements Parcelable { @C.ColorTransfer public final int colorTransfer; - /** - * HdrStaticInfo as defined in CTA-861.3. - */ - public final byte[] hdrStaticInfo; + /** HdrStaticInfo as defined in CTA-861.3, or null if none specified. */ + @Nullable public final byte[] hdrStaticInfo; // Lazily initialized hashcode. private int hashCode; @@ -62,10 +62,13 @@ public final class ColorInfo implements Parcelable { * @param colorSpace The color space of the video. * @param colorRange The color range of the video. * @param colorTransfer The color transfer characteristics of the video. - * @param hdrStaticInfo HdrStaticInfo as defined in CTA-861.3. + * @param hdrStaticInfo HdrStaticInfo as defined in CTA-861.3, or null if none specified. */ - public ColorInfo(@C.ColorSpace int colorSpace, @C.ColorRange int colorRange, - @C.ColorTransfer int colorTransfer, byte[] hdrStaticInfo) { + public ColorInfo( + @C.ColorSpace int colorSpace, + @C.ColorRange int colorRange, + @C.ColorTransfer int colorTransfer, + @Nullable byte[] hdrStaticInfo) { this.colorSpace = colorSpace; this.colorRange = colorRange; this.colorTransfer = colorTransfer; @@ -77,13 +80,13 @@ public final class ColorInfo implements Parcelable { colorSpace = in.readInt(); colorRange = in.readInt(); colorTransfer = in.readInt(); - boolean hasHdrStaticInfo = in.readInt() != 0; + boolean hasHdrStaticInfo = Util.readBoolean(in); hdrStaticInfo = hasHdrStaticInfo ? in.createByteArray() : null; } // Parcelable implementation. @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -91,12 +94,10 @@ public final class ColorInfo implements Parcelable { return false; } ColorInfo other = (ColorInfo) obj; - if (colorSpace != other.colorSpace || colorRange != other.colorRange - || colorTransfer != other.colorTransfer - || !Arrays.equals(hdrStaticInfo, other.hdrStaticInfo)) { - return false; - } - return true; + return colorSpace == other.colorSpace + && colorRange == other.colorRange + && colorTransfer == other.colorTransfer + && Arrays.equals(hdrStaticInfo, other.hdrStaticInfo); } @Override @@ -128,22 +129,22 @@ public final class ColorInfo implements Parcelable { dest.writeInt(colorSpace); dest.writeInt(colorRange); dest.writeInt(colorTransfer); - dest.writeInt(hdrStaticInfo != null ? 1 : 0); + Util.writeBoolean(dest, hdrStaticInfo != null); if (hdrStaticInfo != null) { dest.writeByteArray(hdrStaticInfo); } } - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public ColorInfo createFromParcel(Parcel in) { - return new ColorInfo(in); - } - - @Override - public ColorInfo[] newArray(int size) { - return new ColorInfo[0]; - } - }; + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public ColorInfo createFromParcel(Parcel in) { + return new ColorInfo(in); + } + @Override + public ColorInfo[] newArray(int size) { + return new ColorInfo[size]; + } + }; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/DolbyVisionConfig.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/DolbyVisionConfig.java new file mode 100644 index 000000000000..bfc1f814d25f --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/DolbyVisionConfig.java @@ -0,0 +1,64 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; + +/** Dolby Vision configuration data. */ +public final class DolbyVisionConfig { + + /** + * Parses Dolby Vision configuration data. + * + * @param data A {@link ParsableByteArray}, whose position is set to the start of the Dolby Vision + * configuration data to parse. + * @return The {@link DolbyVisionConfig} corresponding to the configuration, or {@code null} if + * the configuration isn't supported. + */ + @Nullable + public static DolbyVisionConfig parse(ParsableByteArray data) { + data.skipBytes(2); // dv_version_major, dv_version_minor + int profileData = data.readUnsignedByte(); + int dvProfile = (profileData >> 1); + int dvLevel = ((profileData & 0x1) << 5) | ((data.readUnsignedByte() >> 3) & 0x1F); + String codecsPrefix; + if (dvProfile == 4 || dvProfile == 5 || dvProfile == 7) { + codecsPrefix = "dvhe"; + } else if (dvProfile == 8) { + codecsPrefix = "hev1"; + } else if (dvProfile == 9) { + codecsPrefix = "avc3"; + } else { + return null; + } + String codecs = codecsPrefix + ".0" + dvProfile + ".0" + dvLevel; + return new DolbyVisionConfig(dvProfile, dvLevel, codecs); + } + + /** The profile number. */ + public final int profile; + /** The level number. */ + public final int level; + /** The RFC 6381 codecs string. */ + public final String codecs; + + private DolbyVisionConfig(int profile, int level, String codecs) { + this.profile = profile; + this.level = level; + this.codecs = codecs; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/DummySurface.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/DummySurface.java new file mode 100644 index 000000000000..abfb8b09528e --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/DummySurface.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video; + +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_NONE; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_PROTECTED_PBUFFER; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_SURFACELESS_CONTEXT; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.HandlerThread; +import android.os.Message; +import android.view.Surface; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.EGLSurfaceTexture; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.EGLSurfaceTexture.SecureMode; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.GlUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** A dummy {@link Surface}. */ +@TargetApi(17) +public final class DummySurface extends Surface { + + private static final String TAG = "DummySurface"; + + /** + * Whether the surface is secure. + */ + public final boolean secure; + + private static @SecureMode int secureMode; + private static boolean secureModeInitialized; + + private final DummySurfaceThread thread; + private boolean threadReleased; + + /** + * Returns whether the device supports secure dummy surfaces. + * + * @param context Any {@link Context}. + * @return Whether the device supports secure dummy surfaces. + */ + public static synchronized boolean isSecureSupported(Context context) { + if (!secureModeInitialized) { + secureMode = getSecureMode(context); + secureModeInitialized = true; + } + return secureMode != SECURE_MODE_NONE; + } + + /** + * Returns a newly created dummy surface. The surface must be released by calling {@link #release} + * when it's no longer required. + *

+ * Must only be called if {@link Util#SDK_INT} is 17 or higher. + * + * @param context Any {@link Context}. + * @param secure Whether a secure surface is required. Must only be requested if + * {@link #isSecureSupported(Context)} returns {@code true}. + * @throws IllegalStateException If a secure surface is requested on a device for which + * {@link #isSecureSupported(Context)} returns {@code false}. + */ + public static DummySurface newInstanceV17(Context context, boolean secure) { + assertApiLevel17OrHigher(); + Assertions.checkState(!secure || isSecureSupported(context)); + DummySurfaceThread thread = new DummySurfaceThread(); + return thread.init(secure ? secureMode : SECURE_MODE_NONE); + } + + private DummySurface(DummySurfaceThread thread, SurfaceTexture surfaceTexture, boolean secure) { + super(surfaceTexture); + this.thread = thread; + this.secure = secure; + } + + @Override + public void release() { + super.release(); + // The Surface may be released multiple times (explicitly and by Surface.finalize()). The + // implementation of super.release() has its own deduplication logic. Below we need to + // deduplicate ourselves. Synchronization is required as we don't control the thread on which + // Surface.finalize() is called. + synchronized (thread) { + if (!threadReleased) { + thread.release(); + threadReleased = true; + } + } + } + + private static void assertApiLevel17OrHigher() { + if (Util.SDK_INT < 17) { + throw new UnsupportedOperationException("Unsupported prior to API level 17"); + } + } + + @SecureMode + private static int getSecureMode(Context context) { + if (GlUtil.isProtectedContentExtensionSupported(context)) { + if (GlUtil.isSurfacelessContextExtensionSupported()) { + return SECURE_MODE_SURFACELESS_CONTEXT; + } else { + // If we can't use surfaceless contexts, we use a protected 1 * 1 pixel buffer surface. + // This may require support for EXT_protected_surface, but in practice it works on some + // devices that don't have that extension. See also + // https://github.com/google/ExoPlayer/issues/3558. + return SECURE_MODE_PROTECTED_PBUFFER; + } + } else { + return SECURE_MODE_NONE; + } + } + + private static class DummySurfaceThread extends HandlerThread implements Callback { + + private static final int MSG_INIT = 1; + private static final int MSG_RELEASE = 2; + + private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexture; + private @MonotonicNonNull Handler handler; + @Nullable private Error initError; + @Nullable private RuntimeException initException; + @Nullable private DummySurface surface; + + public DummySurfaceThread() { + super("dummySurface"); + } + + public DummySurface init(@SecureMode int secureMode) { + start(); + handler = new Handler(getLooper(), /* callback= */ this); + eglSurfaceTexture = new EGLSurfaceTexture(handler); + boolean wasInterrupted = false; + synchronized (this) { + handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget(); + while (surface == null && initException == null && initError == null) { + try { + wait(); + } catch (InterruptedException e) { + wasInterrupted = true; + } + } + } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } + if (initException != null) { + throw initException; + } else if (initError != null) { + throw initError; + } else { + return Assertions.checkNotNull(surface); + } + } + + public void release() { + Assertions.checkNotNull(handler); + handler.sendEmptyMessage(MSG_RELEASE); + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_INIT: + try { + initInternal(/* secureMode= */ msg.arg1); + } catch (RuntimeException e) { + Log.e(TAG, "Failed to initialize dummy surface", e); + initException = e; + } catch (Error e) { + Log.e(TAG, "Failed to initialize dummy surface", e); + initError = e; + } finally { + synchronized (this) { + notify(); + } + } + return true; + case MSG_RELEASE: + try { + releaseInternal(); + } catch (Throwable e) { + Log.e(TAG, "Failed to release dummy surface", e); + } finally { + quit(); + } + return true; + default: + return true; + } + } + + private void initInternal(@SecureMode int secureMode) { + Assertions.checkNotNull(eglSurfaceTexture); + eglSurfaceTexture.init(secureMode); + this.surface = + new DummySurface( + this, eglSurfaceTexture.getSurfaceTexture(), secureMode != SECURE_MODE_NONE); + } + + private void releaseInternal() { + Assertions.checkNotNull(eglSurfaceTexture); + eglSurfaceTexture.release(); + } + + } + +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/HevcConfig.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/HevcConfig.java index c5e00daa6641..844712146ad0 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/HevcConfig.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/HevcConfig.java @@ -15,6 +15,7 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.video; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; @@ -26,7 +27,7 @@ import java.util.List; */ public final class HevcConfig { - public final List initializationData; + @Nullable public final List initializationData; public final int nalUnitLengthFieldLength; /** @@ -82,7 +83,7 @@ public final class HevcConfig { } } - private HevcConfig(List initializationData, int nalUnitLengthFieldLength) { + private HevcConfig(@Nullable List initializationData, int nalUnitLengthFieldLength) { this.initializationData = initializationData; this.nalUnitLengthFieldLength = nalUnitLengthFieldLength; } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 9c809865c584..1627b70a28e8 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -21,16 +21,24 @@ import android.content.Context; import android.graphics.Point; import android.media.MediaCodec; import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCrypto; import android.media.MediaFormat; +import android.os.Bundle; import android.os.Handler; +import android.os.Message; import android.os.SystemClock; -import android.support.annotation.NonNull; -import android.util.Log; +import android.util.Pair; import android.view.Surface; +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.PlayerMessage.Target; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; @@ -40,16 +48,33 @@ import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCode import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaFormatUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TraceUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher; import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; /** * Decodes and renders video using {@link MediaCodec}. + * + *

This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)} + * on the playback thread: + * + *

    + *
  • Message with type {@link C#MSG_SET_SURFACE} to set the output surface. The message payload + * should be the target {@link Surface}, or null. + *
  • Message with type {@link C#MSG_SET_SCALING_MODE} to set the video scaling mode. The message + * payload should be one of the integer scaling modes in {@link C.VideoScalingMode}. Note that + * the scaling mode only applies if the {@link Surface} targeted by this renderer is owned by + * a {@link android.view.SurfaceView}. + *
*/ -@TargetApi(16) public class MediaCodecVideoRenderer extends MediaCodecRenderer { private static final String TAG = "MediaCodecVideoRenderer"; @@ -62,26 +87,67 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private static final int[] STANDARD_LONG_EDGE_VIDEO_PX = new int[] { 1920, 1600, 1440, 1280, 960, 854, 640, 540, 480}; + // Generally there is zero or one pending output stream offset. We track more offsets to allow for + // pending output streams that have fewer frames than the codec latency. + private static final int MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT = 10; + /** + * Scale factor for the initial maximum input size used to configure the codec in non-adaptive + * playbacks. See {@link #getCodecMaxValues(MediaCodecInfo, Format, Format[])}. + */ + private static final float INITIAL_FORMAT_MAX_INPUT_SIZE_SCALE_FACTOR = 1.5f; + + /** Magic frame render timestamp that indicates the EOS in tunneling mode. */ + private static final long TUNNELING_EOS_PRESENTATION_TIME_US = Long.MAX_VALUE; + + /** A {@link DecoderException} with additional surface information. */ + public static final class VideoDecoderException extends DecoderException { + + /** The {@link System#identityHashCode(Object)} of the surface when the exception occurred. */ + public final int surfaceIdentityHashCode; + + /** Whether the surface was valid when the exception occurred. */ + public final boolean isSurfaceValid; + + public VideoDecoderException( + Throwable cause, @Nullable MediaCodecInfo codecInfo, @Nullable Surface surface) { + super(cause, codecInfo); + surfaceIdentityHashCode = System.identityHashCode(surface); + isSurfaceValid = surface == null || surface.isValid(); + } + } + + private static boolean evaluatedDeviceNeedsSetOutputSurfaceWorkaround; + private static boolean deviceNeedsSetOutputSurfaceWorkaround; + + private final Context context; private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper; private final EventDispatcher eventDispatcher; private final long allowedJoiningTimeMs; private final int maxDroppedFramesToNotify; - private final boolean deviceNeedsAutoFrcWorkaround; + private final boolean deviceNeedsNoPostProcessWorkaround; + private final long[] pendingOutputStreamOffsetsUs; + private final long[] pendingOutputStreamSwitchTimesUs; - private Format[] streamFormats; private CodecMaxValues codecMaxValues; + private boolean codecNeedsSetOutputSurfaceWorkaround; + private boolean codecHandlesHdr10PlusOutOfBandMetadata; private Surface surface; + private Surface dummySurface; @C.VideoScalingMode private int scalingMode; private boolean renderedFirstFrame; + private long initialPositionUs; private long joiningDeadlineMs; private long droppedFrameAccumulationStartTimeMs; private int droppedFrames; private int consecutiveDroppedFrameCount; + private int buffersInCodecCount; + private long lastRenderTimeUs; private int pendingRotationDegrees; private float pendingPixelWidthHeightRatio; + @Nullable private MediaFormat currentMediaFormat; private int currentWidth; private int currentHeight; private int currentUnappliedRotationDegrees; @@ -93,7 +159,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private boolean tunneling; private int tunnelingAudioSessionId; - /* package */ OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener; + /* package */ @Nullable OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener; + + private long lastInputTimeUs; + private long outputStreamOffsetUs; + private int pendingOutputStreamOffsetCount; + @Nullable private VideoFrameMetadataListener frameMetadataListener; /** * @param context A context. @@ -111,7 +182,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { */ public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, long allowedJoiningTimeMs) { - this(context, mediaCodecSelector, allowedJoiningTimeMs, null, null, -1); + this( + context, + mediaCodecSelector, + allowedJoiningTimeMs, + /* eventHandler= */ null, + /* eventListener= */ null, + /* maxDroppedFramesToNotify= */ -1); } /** @@ -122,14 +199,26 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between + * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. */ - public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, - long allowedJoiningTimeMs, Handler eventHandler, VideoRendererEventListener eventListener, - int maxDroppedFrameCountToNotify) { - this(context, mediaCodecSelector, allowedJoiningTimeMs, null, false, eventHandler, - eventListener, maxDroppedFrameCountToNotify); + @SuppressWarnings("deprecation") + public MediaCodecVideoRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + long allowedJoiningTimeMs, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify) { + this( + context, + mediaCodecSelector, + allowedJoiningTimeMs, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + eventHandler, + eventListener, + maxDroppedFramesToNotify); } /** @@ -149,17 +238,120 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @param eventListener A listener of events. May be null if delivery of events is not required. * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. + * @deprecated Use {@link #MediaCodecVideoRenderer(Context, MediaCodecSelector, long, boolean, + * Handler, VideoRendererEventListener, int)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ - public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, - long allowedJoiningTimeMs, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, Handler eventHandler, - VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) { - super(C.TRACK_TYPE_VIDEO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); + @Deprecated + @SuppressWarnings("deprecation") + public MediaCodecVideoRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + long allowedJoiningTimeMs, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify) { + this( + context, + mediaCodecSelector, + allowedJoiningTimeMs, + drmSessionManager, + playClearSamplesWithoutKeys, + /* enableDecoderFallback= */ false, + eventHandler, + eventListener, + maxDroppedFramesToNotify); + } + + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is slower/less efficient than + * the primary decoder. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between + * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. + */ + @SuppressWarnings("deprecation") + public MediaCodecVideoRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + long allowedJoiningTimeMs, + boolean enableDecoderFallback, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify) { + this( + context, + mediaCodecSelector, + allowedJoiningTimeMs, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + enableDecoderFallback, + eventHandler, + eventListener, + maxDroppedFramesToNotify); + } + + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is slower/less efficient than + * the primary decoder. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between + * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. + * @deprecated Use {@link #MediaCodecVideoRenderer(Context, MediaCodecSelector, long, boolean, + * Handler, VideoRendererEventListener, int)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. + */ + @Deprecated + public MediaCodecVideoRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + long allowedJoiningTimeMs, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify) { + super( + C.TRACK_TYPE_VIDEO, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + enableDecoderFallback, + /* assumedMinimumCodecOperatingRate= */ 30); this.allowedJoiningTimeMs = allowedJoiningTimeMs; this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; - frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context); + this.context = context.getApplicationContext(); + frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(this.context); eventDispatcher = new EventDispatcher(eventHandler, eventListener); - deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround(); + deviceNeedsNoPostProcessWorkaround = deviceNeedsNoPostProcessWorkaround(); + pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; + pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; + outputStreamOffsetUs = C.TIME_UNSET; + lastInputTimeUs = C.TIME_UNSET; joiningDeadlineMs = C.TIME_UNSET; currentWidth = Format.NO_VALUE; currentHeight = Format.NO_VALUE; @@ -170,65 +362,157 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) + @Capabilities + protected int supportsFormat( + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + Format format) throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isVideo(mimeType)) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } - boolean requiresSecureDecryption = false; - DrmInitData drmInitData = format.drmInitData; - if (drmInitData != null) { - for (int i = 0; i < drmInitData.schemeDataCount; i++) { - requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption; - } + @Nullable DrmInitData drmInitData = format.drmInitData; + // Assume encrypted content requires secure decoders. + boolean requiresSecureDecryption = drmInitData != null; + List decoderInfos = + getDecoderInfos( + mediaCodecSelector, + format, + requiresSecureDecryption, + /* requiresTunnelingDecoder= */ false); + if (requiresSecureDecryption && decoderInfos.isEmpty()) { + // No secure decoders are available. Fall back to non-secure decoders. + decoderInfos = + getDecoderInfos( + mediaCodecSelector, + format, + /* requiresSecureDecoder= */ false, + /* requiresTunnelingDecoder= */ false); } - MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, - requiresSecureDecryption); - if (decoderInfo == null) { - return FORMAT_UNSUPPORTED_SUBTYPE; + if (decoderInfos.isEmpty()) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } - - boolean decoderCapable = decoderInfo.isCodecSupported(format.codecs); - if (decoderCapable && format.width > 0 && format.height > 0) { - if (Util.SDK_INT >= 21) { - decoderCapable = decoderInfo.isVideoSizeAndRateSupportedV21(format.width, format.height, - format.frameRate); - } else { - decoderCapable = format.width * format.height <= MediaCodecUtil.maxH264DecodableFrameSize(); - if (!decoderCapable) { - Log.d(TAG, "FalseCheck [legacyFrameSize, " + format.width + "x" + format.height + "] [" - + Util.DEVICE_DEBUG_INFO + "]"); + boolean supportsFormatDrm = + drmInitData == null + || FrameworkMediaCrypto.class.equals(format.exoMediaCryptoType) + || (format.exoMediaCryptoType == null + && supportsFormatDrm(drmSessionManager, drmInitData)); + if (!supportsFormatDrm) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); + } + // Check capabilities for the first decoder in the list, which takes priority. + MediaCodecInfo decoderInfo = decoderInfos.get(0); + boolean isFormatSupported = decoderInfo.isFormatSupported(format); + @AdaptiveSupport + int adaptiveSupport = + decoderInfo.isSeamlessAdaptationSupported(format) + ? ADAPTIVE_SEAMLESS + : ADAPTIVE_NOT_SEAMLESS; + @TunnelingSupport int tunnelingSupport = TUNNELING_NOT_SUPPORTED; + if (isFormatSupported) { + List tunnelingDecoderInfos = + getDecoderInfos( + mediaCodecSelector, + format, + requiresSecureDecryption, + /* requiresTunnelingDecoder= */ true); + if (!tunnelingDecoderInfos.isEmpty()) { + MediaCodecInfo tunnelingDecoderInfo = tunnelingDecoderInfos.get(0); + if (tunnelingDecoderInfo.isFormatSupported(format) + && tunnelingDecoderInfo.isSeamlessAdaptationSupported(format)) { + tunnelingSupport = TUNNELING_SUPPORTED; } } } + @FormatSupport + int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; + return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport); + } - int adaptiveSupport = decoderInfo.adaptive ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS; - int tunnelingSupport = decoderInfo.tunneling ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; - int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; - return adaptiveSupport | tunnelingSupport | formatSupport; + @Override + protected List getDecoderInfos( + MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder) + throws DecoderQueryException { + return getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder, tunneling); + } + + private static List getDecoderInfos( + MediaCodecSelector mediaCodecSelector, + Format format, + boolean requiresSecureDecoder, + boolean requiresTunnelingDecoder) + throws DecoderQueryException { + @Nullable String mimeType = format.sampleMimeType; + if (mimeType == null) { + return Collections.emptyList(); + } + List decoderInfos = + mediaCodecSelector.getDecoderInfos( + mimeType, requiresSecureDecoder, requiresTunnelingDecoder); + decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format); + if (MimeTypes.VIDEO_DOLBY_VISION.equals(mimeType)) { + // Fall back to H.264/AVC or H.265/HEVC for the relevant DV profiles. + @Nullable + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); + if (codecProfileAndLevel != null) { + int profile = codecProfileAndLevel.first; + if (profile == CodecProfileLevel.DolbyVisionProfileDvheDtr + || profile == CodecProfileLevel.DolbyVisionProfileDvheSt) { + decoderInfos.addAll( + mediaCodecSelector.getDecoderInfos( + MimeTypes.VIDEO_H265, requiresSecureDecoder, requiresTunnelingDecoder)); + } else if (profile == CodecProfileLevel.DolbyVisionProfileDvavSe) { + decoderInfos.addAll( + mediaCodecSelector.getDecoderInfos( + MimeTypes.VIDEO_H264, requiresSecureDecoder, requiresTunnelingDecoder)); + } + } + } + return Collections.unmodifiableList(decoderInfos); } @Override protected void onEnabled(boolean joining) throws ExoPlaybackException { super.onEnabled(joining); + int oldTunnelingAudioSessionId = tunnelingAudioSessionId; tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId; tunneling = tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET; + if (tunnelingAudioSessionId != oldTunnelingAudioSessionId) { + releaseCodec(); + } eventDispatcher.enabled(decoderCounters); frameReleaseTimeHelper.enable(); } @Override - protected void onStreamChanged(Format[] formats) throws ExoPlaybackException { - streamFormats = formats; - super.onStreamChanged(formats); + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + if (outputStreamOffsetUs == C.TIME_UNSET) { + outputStreamOffsetUs = offsetUs; + } else { + if (pendingOutputStreamOffsetCount == pendingOutputStreamOffsetsUs.length) { + Log.w(TAG, "Too many stream changes, so dropping offset: " + + pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1]); + } else { + pendingOutputStreamOffsetCount++; + } + pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1] = offsetUs; + pendingOutputStreamSwitchTimesUs[pendingOutputStreamOffsetCount - 1] = lastInputTimeUs; + } + super.onStreamChanged(formats, offsetUs); } @Override protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { super.onPositionReset(positionUs, joining); clearRenderedFirstFrame(); + initialPositionUs = C.TIME_UNSET; consecutiveDroppedFrameCount = 0; + lastInputTimeUs = C.TIME_UNSET; + if (pendingOutputStreamOffsetCount != 0) { + outputStreamOffsetUs = pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1]; + pendingOutputStreamOffsetCount = 0; + } if (joining) { setJoiningDeadlineMs(); } else { @@ -238,7 +522,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override public boolean isReady() { - if ((renderedFirstFrame || super.shouldInitCodec()) && super.isReady()) { + if (super.isReady() && (renderedFirstFrame || (dummySurface != null && surface == dummySurface) + || getCodec() == null || tunneling)) { // Ready. If we were joining then we've now joined, so clear the joining deadline. joiningDeadlineMs = C.TIME_UNSET; return true; @@ -260,21 +545,22 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { super.onStarted(); droppedFrames = 0; droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); - joiningDeadlineMs = C.TIME_UNSET; + lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; } @Override protected void onStopped() { + joiningDeadlineMs = C.TIME_UNSET; maybeNotifyDroppedFrames(); super.onStopped(); } @Override protected void onDisabled() { - currentWidth = Format.NO_VALUE; - currentHeight = Format.NO_VALUE; - currentPixelWidthHeightRatio = Format.NO_VALUE; - pendingPixelWidthHeightRatio = Format.NO_VALUE; + lastInputTimeUs = C.TIME_UNSET; + outputStreamOffsetUs = C.TIME_UNSET; + pendingOutputStreamOffsetCount = 0; + currentMediaFormat = null; clearReportedVideoSize(); clearRenderedFirstFrame(); frameReleaseTimeHelper.disable(); @@ -282,41 +568,69 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { try { super.onDisabled(); } finally { - decoderCounters.ensureUpdated(); eventDispatcher.disabled(decoderCounters); } } @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + protected void onReset() { + try { + super.onReset(); + } finally { + if (dummySurface != null) { + if (surface == dummySurface) { + surface = null; + } + dummySurface.release(); + dummySurface = null; + } + } + } + + @Override + public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { if (messageType == C.MSG_SET_SURFACE) { setSurface((Surface) message); } else if (messageType == C.MSG_SET_SCALING_MODE) { scalingMode = (Integer) message; MediaCodec codec = getCodec(); if (codec != null) { - setVideoScalingMode(codec, scalingMode); + codec.setVideoScalingMode(scalingMode); } + } else if (messageType == C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) { + frameMetadataListener = (VideoFrameMetadataListener) message; } else { super.handleMessage(messageType, message); } } private void setSurface(Surface surface) throws ExoPlaybackException { + if (surface == null) { + // Use a dummy surface if possible. + if (dummySurface != null) { + surface = dummySurface; + } else { + MediaCodecInfo codecInfo = getCodecInfo(); + if (codecInfo != null && shouldUseDummySurface(codecInfo)) { + dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure); + surface = dummySurface; + } + } + } // We only need to update the codec if the surface has changed. if (this.surface != surface) { this.surface = surface; - int state = getState(); - if (state == STATE_ENABLED || state == STATE_STARTED) { - MediaCodec codec = getCodec(); - if (Util.SDK_INT >= 23 && codec != null && surface != null) { + @State int state = getState(); + MediaCodec codec = getCodec(); + if (codec != null) { + if (Util.SDK_INT >= 23 && surface != null && !codecNeedsSetOutputSurfaceWorkaround) { setOutputSurfaceV23(codec, surface); } else { releaseCodec(); maybeInitCodec(); } } - if (surface != null) { + if (surface != null && surface != dummySurface) { // If we know the video size, report it again immediately. maybeRenotifyVideoSizeChanged(); // We haven't rendered to the new surface yet. @@ -329,25 +643,49 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { clearReportedVideoSize(); clearRenderedFirstFrame(); } - } else if (surface != null) { - // The surface is unchanged and non-null. If we know the video size and/or have already - // rendered to the surface, report these again immediately. + } else if (surface != null && surface != dummySurface) { + // The surface is set and unchanged. If we know the video size and/or have already rendered to + // the surface, report these again immediately. maybeRenotifyVideoSizeChanged(); maybeRenotifyRenderedFirstFrame(); } } @Override - protected boolean shouldInitCodec() { - return super.shouldInitCodec() && surface != null && surface.isValid(); + protected boolean shouldInitCodec(MediaCodecInfo codecInfo) { + return surface != null || shouldUseDummySurface(codecInfo); } @Override - protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, - MediaCrypto crypto) throws DecoderQueryException { - codecMaxValues = getCodecMaxValues(codecInfo, format, streamFormats); - MediaFormat mediaFormat = getMediaFormat(format, codecMaxValues, deviceNeedsAutoFrcWorkaround, - tunnelingAudioSessionId); + protected boolean getCodecNeedsEosPropagation() { + // Since API 23, onFrameRenderedListener allows for detection of the renderer EOS. + return tunneling && Util.SDK_INT < 23; + } + + @Override + protected void configureCodec( + MediaCodecInfo codecInfo, + MediaCodec codec, + Format format, + @Nullable MediaCrypto crypto, + float codecOperatingRate) { + String codecMimeType = codecInfo.codecMimeType; + codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); + MediaFormat mediaFormat = + getMediaFormat( + format, + codecMimeType, + codecMaxValues, + codecOperatingRate, + deviceNeedsNoPostProcessWorkaround, + tunnelingAudioSessionId); + if (surface == null) { + Assertions.checkState(shouldUseDummySurface(codecInfo)); + if (dummySurface == null) { + dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure); + } + surface = dummySurface; + } codec.configure(mediaFormat, surface, crypto, 0); if (Util.SDK_INT >= 23 && tunneling) { tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec); @@ -355,37 +693,270 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected void onCodecInitialized(String name, long initializedTimestampMs, - long initializationDurationMs) { - eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); + protected @KeepCodecResult int canKeepCodec( + MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + if (codecInfo.isSeamlessAdaptationSupported( + oldFormat, newFormat, /* isNewFormatComplete= */ true) + && newFormat.width <= codecMaxValues.width + && newFormat.height <= codecMaxValues.height + && getMaxInputSize(codecInfo, newFormat) <= codecMaxValues.inputSize) { + return oldFormat.initializationDataEquals(newFormat) + ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION + : KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION; + } + return KEEP_CODEC_RESULT_NO; } + @CallSuper @Override - protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { - super.onInputFormatChanged(newFormat); - eventDispatcher.inputFormatChanged(newFormat); - pendingPixelWidthHeightRatio = getPixelWidthHeightRatio(newFormat); - pendingRotationDegrees = getRotationDegrees(newFormat); + protected void releaseCodec() { + try { + super.releaseCodec(); + } finally { + buffersInCodecCount = 0; + } } + @CallSuper @Override - protected void onQueueInputBuffer(DecoderInputBuffer buffer) { - if (Util.SDK_INT < 23 && tunneling) { - maybeNotifyRenderedFirstFrame(); + protected boolean flushOrReleaseCodec() { + try { + return super.flushOrReleaseCodec(); + } finally { + buffersInCodecCount = 0; } } @Override - protected void onOutputFormatChanged(MediaCodec codec, android.media.MediaFormat outputFormat) { - boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT) - && outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM) - && outputFormat.containsKey(KEY_CROP_TOP); - currentWidth = hasCrop - ? outputFormat.getInteger(KEY_CROP_RIGHT) - outputFormat.getInteger(KEY_CROP_LEFT) + 1 - : outputFormat.getInteger(MediaFormat.KEY_WIDTH); - currentHeight = hasCrop - ? outputFormat.getInteger(KEY_CROP_BOTTOM) - outputFormat.getInteger(KEY_CROP_TOP) + 1 - : outputFormat.getInteger(MediaFormat.KEY_HEIGHT); + protected float getCodecOperatingRateV23( + float operatingRate, Format format, Format[] streamFormats) { + // Use the highest known stream frame-rate up front, to avoid having to reconfigure the codec + // should an adaptive switch to that stream occur. + float maxFrameRate = -1; + for (Format streamFormat : streamFormats) { + float streamFrameRate = streamFormat.frameRate; + if (streamFrameRate != Format.NO_VALUE) { + maxFrameRate = Math.max(maxFrameRate, streamFrameRate); + } + } + return maxFrameRate == -1 ? CODEC_OPERATING_RATE_UNSET : (maxFrameRate * operatingRate); + } + + @Override + protected void onCodecInitialized(String name, long initializedTimestampMs, + long initializationDurationMs) { + eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); + codecNeedsSetOutputSurfaceWorkaround = codecNeedsSetOutputSurfaceWorkaround(name); + codecHandlesHdr10PlusOutOfBandMetadata = + Assertions.checkNotNull(getCodecInfo()).isHdr10PlusOutOfBandMetadataSupported(); + } + + @Override + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + super.onInputFormatChanged(formatHolder); + Format newFormat = formatHolder.format; + eventDispatcher.inputFormatChanged(newFormat); + pendingPixelWidthHeightRatio = newFormat.pixelWidthHeightRatio; + pendingRotationDegrees = newFormat.rotationDegrees; + } + + /** + * Called immediately before an input buffer is queued into the codec. + * + * @param buffer The buffer to be queued. + */ + @CallSuper + @Override + protected void onQueueInputBuffer(DecoderInputBuffer buffer) { + // In tunneling mode the device may do frame rate conversion, so in general we can't keep track + // of the number of buffers in the codec. + if (!tunneling) { + buffersInCodecCount++; + } + lastInputTimeUs = Math.max(buffer.timeUs, lastInputTimeUs); + if (Util.SDK_INT < 23 && tunneling) { + // In tunneled mode before API 23 we don't have a way to know when the buffer is output, so + // treat it as if it were output immediately. + onProcessedTunneledBuffer(buffer.timeUs); + } + } + + @Override + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputMediaFormat) { + currentMediaFormat = outputMediaFormat; + boolean hasCrop = + outputMediaFormat.containsKey(KEY_CROP_RIGHT) + && outputMediaFormat.containsKey(KEY_CROP_LEFT) + && outputMediaFormat.containsKey(KEY_CROP_BOTTOM) + && outputMediaFormat.containsKey(KEY_CROP_TOP); + int width = + hasCrop + ? outputMediaFormat.getInteger(KEY_CROP_RIGHT) + - outputMediaFormat.getInteger(KEY_CROP_LEFT) + + 1 + : outputMediaFormat.getInteger(MediaFormat.KEY_WIDTH); + int height = + hasCrop + ? outputMediaFormat.getInteger(KEY_CROP_BOTTOM) + - outputMediaFormat.getInteger(KEY_CROP_TOP) + + 1 + : outputMediaFormat.getInteger(MediaFormat.KEY_HEIGHT); + processOutputFormat(codec, width, height); + } + + @Override + protected void handleInputBufferSupplementalData(DecoderInputBuffer buffer) + throws ExoPlaybackException { + if (!codecHandlesHdr10PlusOutOfBandMetadata) { + return; + } + ByteBuffer data = Assertions.checkNotNull(buffer.supplementalData); + if (data.remaining() >= 7) { + // Check for HDR10+ out-of-band metadata. See User_data_registered_itu_t_t35 in ST 2094-40. + byte ituTT35CountryCode = data.get(); + int ituTT35TerminalProviderCode = data.getShort(); + int ituTT35TerminalProviderOrientedCode = data.getShort(); + byte applicationIdentifier = data.get(); + byte applicationVersion = data.get(); + data.position(0); + if (ituTT35CountryCode == (byte) 0xB5 + && ituTT35TerminalProviderCode == 0x003C + && ituTT35TerminalProviderOrientedCode == 0x0001 + && applicationIdentifier == 4 + && applicationVersion == 0) { + // The metadata size may vary so allocate a new array every time. This is not too + // inefficient because the metadata is only a few tens of bytes. + byte[] hdr10PlusInfo = new byte[data.remaining()]; + data.get(hdr10PlusInfo); + data.position(0); + // If codecHandlesHdr10PlusOutOfBandMetadata is true, this is an API 29 or later build. + setHdr10PlusInfoV29(getCodec(), hdr10PlusInfo); + } + } + } + + @Override + protected boolean processOutputBuffer( + long positionUs, + long elapsedRealtimeUs, + MediaCodec codec, + ByteBuffer buffer, + int bufferIndex, + int bufferFlags, + long bufferPresentationTimeUs, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, + Format format) + throws ExoPlaybackException { + if (initialPositionUs == C.TIME_UNSET) { + initialPositionUs = positionUs; + } + + long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs; + + if (isDecodeOnlyBuffer && !isLastBuffer) { + skipOutputBuffer(codec, bufferIndex, presentationTimeUs); + return true; + } + + long earlyUs = bufferPresentationTimeUs - positionUs; + if (surface == dummySurface) { + // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. + if (isBufferLate(earlyUs)) { + skipOutputBuffer(codec, bufferIndex, presentationTimeUs); + return true; + } + return false; + } + + long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000; + long elapsedSinceLastRenderUs = elapsedRealtimeNowUs - lastRenderTimeUs; + boolean isStarted = getState() == STATE_STARTED; + // Don't force output until we joined and the position reached the current stream. + boolean forceRenderOutputBuffer = + joiningDeadlineMs == C.TIME_UNSET + && positionUs >= outputStreamOffsetUs + && (!renderedFirstFrame + || (isStarted && shouldForceRenderOutputBuffer(earlyUs, elapsedSinceLastRenderUs))); + if (forceRenderOutputBuffer) { + long releaseTimeNs = System.nanoTime(); + notifyFrameMetadataListener(presentationTimeUs, releaseTimeNs, format, currentMediaFormat); + if (Util.SDK_INT >= 21) { + renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, releaseTimeNs); + } else { + renderOutputBuffer(codec, bufferIndex, presentationTimeUs); + } + return true; + } + + if (!isStarted || positionUs == initialPositionUs) { + return false; + } + + // Fine-grained adjustment of earlyUs based on the elapsed time since the start of the current + // iteration of the rendering loop. + long elapsedSinceStartOfLoopUs = elapsedRealtimeNowUs - elapsedRealtimeUs; + earlyUs -= elapsedSinceStartOfLoopUs; + + // Compute the buffer's desired release time in nanoseconds. + long systemTimeNs = System.nanoTime(); + long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000); + + // Apply a timestamp adjustment, if there is one. + long adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime( + bufferPresentationTimeUs, unadjustedFrameReleaseTimeNs); + earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; + + boolean treatDroppedBuffersAsSkipped = joiningDeadlineMs != C.TIME_UNSET; + if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastBuffer) + && maybeDropBuffersToKeyframe( + codec, bufferIndex, presentationTimeUs, positionUs, treatDroppedBuffersAsSkipped)) { + return false; + } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs, isLastBuffer)) { + if (treatDroppedBuffersAsSkipped) { + skipOutputBuffer(codec, bufferIndex, presentationTimeUs); + } else { + dropOutputBuffer(codec, bufferIndex, presentationTimeUs); + } + return true; + } + + if (Util.SDK_INT >= 21) { + // Let the underlying framework time the release. + if (earlyUs < 50000) { + notifyFrameMetadataListener( + presentationTimeUs, adjustedReleaseTimeNs, format, currentMediaFormat); + renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, adjustedReleaseTimeNs); + return true; + } + } else { + // We need to time the release ourselves. + if (earlyUs < 30000) { + if (earlyUs > 11000) { + // We're a little too early to render the frame. Sleep until the frame can be rendered. + // Note: The 11ms threshold was chosen fairly arbitrarily. + try { + // Subtracting 10000 rather than 11000 ensures the sleep time will be at least 1ms. + Thread.sleep((earlyUs - 10000) / 1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + notifyFrameMetadataListener( + presentationTimeUs, adjustedReleaseTimeNs, format, currentMediaFormat); + renderOutputBuffer(codec, bufferIndex, presentationTimeUs); + return true; + } + } + + // We're either not playing, or it's not time to render the frame yet. + return false; + } + + private void processOutputFormat(MediaCodec codec, int width, int height) { + currentWidth = width; + currentHeight = height; currentPixelWidthHeightRatio = pendingPixelWidthHeightRatio; if (Util.SDK_INT >= 21) { // On API level 21 and above the decoder applies the rotation when rendering to the surface. @@ -401,87 +972,73 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // On API level 20 and below the decoder does not apply the rotation. currentUnappliedRotationDegrees = pendingRotationDegrees; } - // Must be applied each time the output format changes. - setVideoScalingMode(codec, scalingMode); + // Must be applied each time the output MediaFormat changes. + codec.setVideoScalingMode(scalingMode); } - @Override - protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive, - Format oldFormat, Format newFormat) { - return areAdaptationCompatible(oldFormat, newFormat) - && newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height - && newFormat.maxInputSize <= codecMaxValues.inputSize - && (codecIsAdaptive - || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height)); + private void notifyFrameMetadataListener( + long presentationTimeUs, long releaseTimeNs, Format format, MediaFormat mediaFormat) { + if (frameMetadataListener != null) { + frameMetadataListener.onVideoFrameAboutToBeRendered( + presentationTimeUs, releaseTimeNs, format, mediaFormat); + } } + /** + * Returns the offset that should be subtracted from {@code bufferPresentationTimeUs} in {@link + * #processOutputBuffer(long, long, MediaCodec, ByteBuffer, int, int, long, boolean, boolean, + * Format)} to get the playback position with respect to the media. + */ + protected long getOutputStreamOffsetUs() { + return outputStreamOffsetUs; + } + + /** Called when a buffer was processed in tunneling mode. */ + protected void onProcessedTunneledBuffer(long presentationTimeUs) { + @Nullable Format format = updateOutputFormatForTime(presentationTimeUs); + if (format != null) { + processOutputFormat(getCodec(), format.width, format.height); + } + maybeNotifyVideoSizeChanged(); + decoderCounters.renderedOutputBufferCount++; + maybeNotifyRenderedFirstFrame(); + onProcessedOutputBuffer(presentationTimeUs); + } + + /** Called when a output EOS was received in tunneling mode. */ + private void onProcessedTunneledEndOfStream() { + setPendingOutputEndOfStream(); + } + + /** + * Called when an output buffer is successfully processed. + * + * @param presentationTimeUs The timestamp associated with the output buffer. + */ + @CallSuper @Override - protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, - ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip) { - if (shouldSkip) { - skipOutputBuffer(codec, bufferIndex); - return true; + protected void onProcessedOutputBuffer(long presentationTimeUs) { + if (!tunneling) { + buffersInCodecCount--; } - - if (!renderedFirstFrame) { - if (Util.SDK_INT >= 21) { - renderOutputBufferV21(codec, bufferIndex, System.nanoTime()); - } else { - renderOutputBuffer(codec, bufferIndex); - } - return true; + while (pendingOutputStreamOffsetCount != 0 + && presentationTimeUs >= pendingOutputStreamSwitchTimesUs[0]) { + outputStreamOffsetUs = pendingOutputStreamOffsetsUs[0]; + pendingOutputStreamOffsetCount--; + System.arraycopy( + pendingOutputStreamOffsetsUs, + /* srcPos= */ 1, + pendingOutputStreamOffsetsUs, + /* destPos= */ 0, + pendingOutputStreamOffsetCount); + System.arraycopy( + pendingOutputStreamSwitchTimesUs, + /* srcPos= */ 1, + pendingOutputStreamSwitchTimesUs, + /* destPos= */ 0, + pendingOutputStreamOffsetCount); + clearRenderedFirstFrame(); } - - if (getState() != STATE_STARTED) { - return false; - } - - // Compute how many microseconds it is until the buffer's presentation time. - long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs; - long earlyUs = bufferPresentationTimeUs - positionUs - elapsedSinceStartOfLoopUs; - - // Compute the buffer's desired release time in nanoseconds. - long systemTimeNs = System.nanoTime(); - long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000); - - // Apply a timestamp adjustment, if there is one. - long adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime( - bufferPresentationTimeUs, unadjustedFrameReleaseTimeNs); - earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; - - if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { - // We're more than 30ms late rendering the frame. - dropOutputBuffer(codec, bufferIndex); - return true; - } - - if (Util.SDK_INT >= 21) { - // Let the underlying framework time the release. - if (earlyUs < 50000) { - renderOutputBufferV21(codec, bufferIndex, adjustedReleaseTimeNs); - return true; - } - } else { - // We need to time the release ourselves. - if (earlyUs < 30000) { - if (earlyUs > 11000) { - // We're a little too early to render the frame. Sleep until the frame can be rendered. - // Note: The 11ms threshold was chosen fairly arbitrarily. - try { - // Subtracting 10000 rather than 11000 ensures the sleep time will be at least 1ms. - Thread.sleep((earlyUs - 10000) / 1000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - renderOutputBuffer(codec, bufferIndex); - return true; - } - } - - // We're either not playing, or it's not time to render the frame yet. - return false; } /** @@ -491,54 +1048,173 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * indicates that the buffer is late. * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, * measured at the start of the current iteration of the rendering loop. + * @param isLastBuffer Whether the buffer is the last buffer in the current stream. */ - protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) { - // Drop the frame if we're more than 30ms late rendering the frame. - return earlyUs < -30000; + protected boolean shouldDropOutputBuffer( + long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) { + return isBufferLate(earlyUs) && !isLastBuffer; } - private void skipOutputBuffer(MediaCodec codec, int bufferIndex) { + /** + * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after + * the current playback position, if possible. + * + * @param earlyUs The time until the current buffer should be presented in microseconds. A + * negative value indicates that the buffer is late. + * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, + * measured at the start of the current iteration of the rendering loop. + * @param isLastBuffer Whether the buffer is the last buffer in the current stream. + */ + protected boolean shouldDropBuffersToKeyframe( + long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) { + return isBufferVeryLate(earlyUs) && !isLastBuffer; + } + + /** + * Returns whether to force rendering an output buffer. + * + * @param earlyUs The time until the current buffer should be presented in microseconds. A + * negative value indicates that the buffer is late. + * @param elapsedSinceLastRenderUs The elapsed time since the last output buffer was rendered, in + * microseconds. + * @return Returns whether to force rendering an output buffer. + */ + protected boolean shouldForceRenderOutputBuffer(long earlyUs, long elapsedSinceLastRenderUs) { + // Force render late buffers every 100ms to avoid frozen video effect. + return isBufferLate(earlyUs) && elapsedSinceLastRenderUs > 100000; + } + + /** + * Skips the output buffer with the specified index. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to skip. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + */ + protected void skipOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) { TraceUtil.beginSection("skipVideoBuffer"); - codec.releaseOutputBuffer(bufferIndex, false); + codec.releaseOutputBuffer(index, false); TraceUtil.endSection(); decoderCounters.skippedOutputBufferCount++; } - private void dropOutputBuffer(MediaCodec codec, int bufferIndex) { + /** + * Drops the output buffer with the specified index. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to drop. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + */ + protected void dropOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) { TraceUtil.beginSection("dropVideoBuffer"); - codec.releaseOutputBuffer(bufferIndex, false); + codec.releaseOutputBuffer(index, false); TraceUtil.endSection(); - decoderCounters.droppedOutputBufferCount++; - droppedFrames++; - consecutiveDroppedFrameCount++; - decoderCounters.maxConsecutiveDroppedOutputBufferCount = Math.max(consecutiveDroppedFrameCount, - decoderCounters.maxConsecutiveDroppedOutputBufferCount); - if (droppedFrames == maxDroppedFramesToNotify) { + updateDroppedBufferCounters(1); + } + + /** + * Drops frames from the current output buffer to the next keyframe at or before the playback + * position. If no such keyframe exists, as the playback position is inside the same group of + * pictures as the buffer being processed, returns {@code false}. Returns {@code true} otherwise. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to drop. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + * @param positionUs The current playback position, in microseconds. + * @param treatDroppedBuffersAsSkipped Whether dropped buffers should be treated as intentionally + * skipped. + * @return Whether any buffers were dropped. + * @throws ExoPlaybackException If an error occurs flushing the codec. + */ + protected boolean maybeDropBuffersToKeyframe( + MediaCodec codec, + int index, + long presentationTimeUs, + long positionUs, + boolean treatDroppedBuffersAsSkipped) + throws ExoPlaybackException { + int droppedSourceBufferCount = skipSource(positionUs); + if (droppedSourceBufferCount == 0) { + return false; + } + decoderCounters.droppedToKeyframeCount++; + // We dropped some buffers to catch up, so update the decoder counters and flush the codec, + // which releases all pending buffers buffers including the current output buffer. + int totalDroppedBufferCount = buffersInCodecCount + droppedSourceBufferCount; + if (treatDroppedBuffersAsSkipped) { + decoderCounters.skippedOutputBufferCount += totalDroppedBufferCount; + } else { + updateDroppedBufferCounters(totalDroppedBufferCount); + } + flushOrReinitializeCodec(); + return true; + } + + /** + * Updates decoder counters to reflect that {@code droppedBufferCount} additional buffers were + * dropped. + * + * @param droppedBufferCount The number of additional dropped buffers. + */ + protected void updateDroppedBufferCounters(int droppedBufferCount) { + decoderCounters.droppedBufferCount += droppedBufferCount; + droppedFrames += droppedBufferCount; + consecutiveDroppedFrameCount += droppedBufferCount; + decoderCounters.maxConsecutiveDroppedBufferCount = Math.max(consecutiveDroppedFrameCount, + decoderCounters.maxConsecutiveDroppedBufferCount); + if (maxDroppedFramesToNotify > 0 && droppedFrames >= maxDroppedFramesToNotify) { maybeNotifyDroppedFrames(); } } - private void renderOutputBuffer(MediaCodec codec, int bufferIndex) { + /** + * Renders the output buffer with the specified index. This method is only called if the platform + * API version of the device is less than 21. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to drop. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + */ + protected void renderOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) { maybeNotifyVideoSizeChanged(); TraceUtil.beginSection("releaseOutputBuffer"); - codec.releaseOutputBuffer(bufferIndex, true); + codec.releaseOutputBuffer(index, true); TraceUtil.endSection(); + lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; decoderCounters.renderedOutputBufferCount++; consecutiveDroppedFrameCount = 0; maybeNotifyRenderedFirstFrame(); } + /** + * Renders the output buffer with the specified index. This method is only called if the platform + * API version of the device is 21 or later. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to drop. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + * @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds. + */ @TargetApi(21) - private void renderOutputBufferV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) { + protected void renderOutputBufferV21( + MediaCodec codec, int index, long presentationTimeUs, long releaseTimeNs) { maybeNotifyVideoSizeChanged(); TraceUtil.beginSection("releaseOutputBuffer"); - codec.releaseOutputBuffer(bufferIndex, releaseTimeNs); + codec.releaseOutputBuffer(index, releaseTimeNs); TraceUtil.endSection(); + lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; decoderCounters.renderedOutputBufferCount++; consecutiveDroppedFrameCount = 0; maybeNotifyRenderedFirstFrame(); } + private boolean shouldUseDummySurface(MediaCodecInfo codecInfo) { + return Util.SDK_INT >= 23 + && !tunneling + && !codecNeedsSetOutputSurfaceWorkaround(codecInfo.name) + && (!codecInfo.secure || DummySurface.isSecureSupported(context)); + } + private void setJoiningDeadlineMs() { joiningDeadlineMs = allowedJoiningTimeMs > 0 ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; @@ -580,9 +1256,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } private void maybeNotifyVideoSizeChanged() { - if (reportedWidth != currentWidth || reportedHeight != currentHeight + if ((currentWidth != Format.NO_VALUE || currentHeight != Format.NO_VALUE) + && (reportedWidth != currentWidth || reportedHeight != currentHeight || reportedUnappliedRotationDegrees != currentUnappliedRotationDegrees - || reportedPixelWidthHeightRatio != currentPixelWidthHeightRatio) { + || reportedPixelWidthHeightRatio != currentPixelWidthHeightRatio)) { eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees, currentPixelWidthHeightRatio); reportedWidth = currentWidth; @@ -594,8 +1271,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private void maybeRenotifyVideoSizeChanged() { if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) { - eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees, - currentPixelWidthHeightRatio); + eventDispatcher.videoSizeChanged(reportedWidth, reportedHeight, + reportedUnappliedRotationDegrees, reportedPixelWidthHeightRatio); } } @@ -609,26 +1286,21 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } - @SuppressLint("InlinedApi") - private static MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues, - boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) { - MediaFormat frameworkMediaFormat = format.getFrameworkMediaFormatV16(); - // Set the maximum adaptive video dimensions. - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height); - // Set the maximum input size. - if (codecMaxValues.inputSize != Format.NO_VALUE) { - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize); - } - // Set FRC workaround. - if (deviceNeedsAutoFrcWorkaround) { - frameworkMediaFormat.setInteger("auto-frc", 0); - } - // Configure tunneling if enabled. - if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { - configureTunnelingV21(frameworkMediaFormat, tunnelingAudioSessionId); - } - return frameworkMediaFormat; + private static boolean isBufferLate(long earlyUs) { + // Class a buffer as late if it should have been presented more than 30 ms ago. + return earlyUs < -30000; + } + + private static boolean isBufferVeryLate(long earlyUs) { + // Class a buffer as very late if it should have been presented more than 500 ms ago. + return earlyUs < -500000; + } + + @TargetApi(29) + private static void setHdr10PlusInfoV29(MediaCodec codec, byte[] hdr10PlusInfo) { + Bundle codecParameters = new Bundle(); + codecParameters.putByteArray(MediaCodec.PARAMETER_KEY_HDR10_PLUS_INFO, hdr10PlusInfo); + codec.setParameters(codecParameters); } @TargetApi(23) @@ -642,34 +1314,110 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { mediaFormat.setInteger(MediaFormat.KEY_AUDIO_SESSION_ID, tunnelingAudioSessionId); } + /** + * Returns the framework {@link MediaFormat} that should be used to configure the decoder. + * + * @param format The {@link Format} of media. + * @param codecMimeType The MIME type handled by the codec. + * @param codecMaxValues Codec max values that should be used when configuring the decoder. + * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if + * no codec operating rate should be set. + * @param deviceNeedsNoPostProcessWorkaround Whether the device is known to do post processing by + * default that isn't compatible with ExoPlayer. + * @param tunnelingAudioSessionId The audio session id to use for tunneling, or {@link + * C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled. + * @return The framework {@link MediaFormat} that should be used to configure the decoder. + */ + @SuppressLint("InlinedApi") + protected MediaFormat getMediaFormat( + Format format, + String codecMimeType, + CodecMaxValues codecMaxValues, + float codecOperatingRate, + boolean deviceNeedsNoPostProcessWorkaround, + int tunnelingAudioSessionId) { + MediaFormat mediaFormat = new MediaFormat(); + // Set format parameters that should always be set. + mediaFormat.setString(MediaFormat.KEY_MIME, codecMimeType); + mediaFormat.setInteger(MediaFormat.KEY_WIDTH, format.width); + mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, format.height); + MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); + // Set format parameters that may be unset. + MediaFormatUtil.maybeSetFloat(mediaFormat, MediaFormat.KEY_FRAME_RATE, format.frameRate); + MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_ROTATION, format.rotationDegrees); + MediaFormatUtil.maybeSetColorInfo(mediaFormat, format.colorInfo); + if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) { + // Some phones require the profile to be set on the codec. + // See https://github.com/google/ExoPlayer/pull/5438. + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); + if (codecProfileAndLevel != null) { + MediaFormatUtil.maybeSetInteger( + mediaFormat, MediaFormat.KEY_PROFILE, codecProfileAndLevel.first); + } + } + // Set codec max values. + mediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); + mediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height); + MediaFormatUtil.maybeSetInteger( + mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize); + // Set codec configuration values. + if (Util.SDK_INT >= 23) { + mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */); + if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET) { + mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate); + } + } + if (deviceNeedsNoPostProcessWorkaround) { + mediaFormat.setInteger("no-post-process", 1); + mediaFormat.setInteger("auto-frc", 0); + } + if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { + configureTunnelingV21(mediaFormat, tunnelingAudioSessionId); + } + return mediaFormat; + } + /** * Returns {@link CodecMaxValues} suitable for configuring a codec for {@code format} in a way * that will allow possible adaptation to other compatible formats in {@code streamFormats}. * * @param codecInfo Information about the {@link MediaCodec} being configured. - * @param format The format for which the codec is being configured. + * @param format The {@link Format} for which the codec is being configured. * @param streamFormats The possible stream formats. * @return Suitable {@link CodecMaxValues}. - * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ - private static CodecMaxValues getCodecMaxValues(MediaCodecInfo codecInfo, Format format, - Format[] streamFormats) throws DecoderQueryException { + protected CodecMaxValues getCodecMaxValues( + MediaCodecInfo codecInfo, Format format, Format[] streamFormats) { int maxWidth = format.width; int maxHeight = format.height; - int maxInputSize = getMaxInputSize(format); + int maxInputSize = getMaxInputSize(codecInfo, format); if (streamFormats.length == 1) { // The single entry in streamFormats must correspond to the format for which the codec is // being configured. + if (maxInputSize != Format.NO_VALUE) { + int codecMaxInputSize = + getCodecMaxInputSize(codecInfo, format.sampleMimeType, format.width, format.height); + if (codecMaxInputSize != Format.NO_VALUE) { + // Scale up the initial video decoder maximum input size so playlist item transitions with + // small increases in maximum sample size don't require reinitialization. This only makes + // a difference if the exact maximum sample sizes are known from the container. + int scaledMaxInputSize = + (int) (maxInputSize * INITIAL_FORMAT_MAX_INPUT_SIZE_SCALE_FACTOR); + // Avoid exceeding the maximum expected for the codec. + maxInputSize = Math.min(scaledMaxInputSize, codecMaxInputSize); + } + } return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); } boolean haveUnknownDimensions = false; for (Format streamFormat : streamFormats) { - if (areAdaptationCompatible(format, streamFormat)) { - haveUnknownDimensions |= (streamFormat.width == Format.NO_VALUE - || streamFormat.height == Format.NO_VALUE); + if (codecInfo.isSeamlessAdaptationSupported( + format, streamFormat, /* isNewFormatComplete= */ false)) { + haveUnknownDimensions |= + (streamFormat.width == Format.NO_VALUE || streamFormat.height == Format.NO_VALUE); maxWidth = Math.max(maxWidth, streamFormat.width); maxHeight = Math.max(maxHeight, streamFormat.height); - maxInputSize = Math.max(maxInputSize, getMaxInputSize(streamFormat)); + maxInputSize = Math.max(maxInputSize, getMaxInputSize(codecInfo, streamFormat)); } } if (haveUnknownDimensions) { @@ -678,26 +1426,32 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (codecMaxSize != null) { maxWidth = Math.max(maxWidth, codecMaxSize.x); maxHeight = Math.max(maxHeight, codecMaxSize.y); - maxInputSize = Math.max(maxInputSize, - getMaxInputSize(format.sampleMimeType, maxWidth, maxHeight)); + maxInputSize = + Math.max( + maxInputSize, + getCodecMaxInputSize(codecInfo, format.sampleMimeType, maxWidth, maxHeight)); Log.w(TAG, "Codec max resolution adjusted to: " + maxWidth + "x" + maxHeight); } } return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); } + @Override + protected DecoderException createDecoderException( + Throwable cause, @Nullable MediaCodecInfo codecInfo) { + return new VideoDecoderException(cause, codecInfo, surface); + } + /** - * Returns a maximum video size to use when configuring a codec for {@code format} in a way - * that will allow possible adaptation to other compatible formats that are expected to have the - * same aspect ratio, but whose sizes are unknown. + * Returns a maximum video size to use when configuring a codec for {@code format} in a way that + * will allow possible adaptation to other compatible formats that are expected to have the same + * aspect ratio, but whose sizes are unknown. * * @param codecInfo Information about the {@link MediaCodec} being configured. - * @param format The format for which the codec is 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. - * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ - private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) - throws DecoderQueryException { + private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) { boolean isVerticalVideo = format.height > format.width; int formatLongEdgePx = isVerticalVideo ? format.height : format.width; int formatShortEdgePx = isVerticalVideo ? format.width : format.height; @@ -715,12 +1469,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return alignedSize; } } else { - // Conservatively assume the codec requires 16px width and height alignment. - longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16; - shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16; - if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) { - return new Point(isVerticalVideo ? shortEdgePx : longEdgePx, - isVerticalVideo ? longEdgePx : shortEdgePx); + try { + // Conservatively assume the codec requires 16px width and height alignment. + longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16; + shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16; + if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) { + return new Point( + isVerticalVideo ? shortEdgePx : longEdgePx, + isVerticalVideo ? longEdgePx : shortEdgePx); + } + } catch (DecoderQueryException e) { + // We tried our best. Give up! + return null; } } } @@ -728,30 +1488,42 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } /** - * Returns a maximum input size for a given format. + * Returns a maximum input buffer size for a given {@link MediaCodec} and {@link Format}. * + * @param codecInfo Information about the {@link MediaCodec} being configured. * @param format The format. - * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be - * determined. + * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not + * be determined. */ - private static int getMaxInputSize(Format format) { + private static int getMaxInputSize(MediaCodecInfo codecInfo, Format format) { if (format.maxInputSize != Format.NO_VALUE) { - // The format defines an explicit maximum input size. - return format.maxInputSize; + // The format defines an explicit maximum input size. Add the total size of initialization + // data buffers, as they may need to be queued in the same input buffer as the largest sample. + int totalInitializationDataSize = 0; + int initializationDataCount = format.initializationData.size(); + for (int i = 0; i < initializationDataCount; i++) { + totalInitializationDataSize += format.initializationData.get(i).length; + } + return format.maxInputSize + totalInitializationDataSize; + } else { + // Calculated maximum input sizes are overestimates, so it's not necessary to add the size of + // initialization data. + return getCodecMaxInputSize(codecInfo, format.sampleMimeType, format.width, format.height); } - return getMaxInputSize(format.sampleMimeType, format.width, format.height); } /** - * Returns a maximum input size for a given mime type, width and height. + * Returns a maximum input size for a given codec, MIME type, width and height. * + * @param codecInfo Information about the {@link MediaCodec} being configured. * @param sampleMimeType The format mime type. * @param width The width in pixels. * @param height The height in pixels. * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be * determined. */ - private static int getMaxInputSize(String sampleMimeType, int width, int height) { + private static int getCodecMaxInputSize( + MediaCodecInfo codecInfo, String sampleMimeType, int width, int height) { if (width == Format.NO_VALUE || height == Format.NO_VALUE) { // We can't infer a maximum input size without video dimensions. return Format.NO_VALUE; @@ -767,9 +1539,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { minCompressionRatio = 2; break; case MimeTypes.VIDEO_H264: - if ("BRAVIA 4K 2015".equals(Util.MODEL)) { - // The Sony BRAVIA 4k TV has input buffers that are too small for the calculated 4k video - // maximum input size, so use the default value. + if ("BRAVIA 4K 2015".equals(Util.MODEL) // Sony Bravia 4K + || ("Amazon".equals(Util.MANUFACTURER) + && ("KFSOWI".equals(Util.MODEL) // Kindle Soho + || ("AFTS".equals(Util.MODEL) && codecInfo.secure)))) { // Fire TV Gen 2 + // Use the default value for cases where platform limitations may prevent buffers of the + // calculated maximum input size from being allocated. return Format.NO_VALUE; } // Round up width/height to an integer number of macroblocks. @@ -794,51 +1569,236 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return (maxPixels * 3) / (2 * minCompressionRatio); } - private static void setVideoScalingMode(MediaCodec codec, int scalingMode) { - codec.setVideoScalingMode(scalingMode); - } - /** - * Returns whether the device is known to enable frame-rate conversion logic that negatively - * impacts ExoPlayer. - *

- * If true is returned then we explicitly disable the feature. + * Returns whether the device is known to do post processing by default that isn't compatible with + * ExoPlayer. * - * @return True if the device is known to enable frame-rate conversion logic that negatively - * impacts ExoPlayer. False otherwise. + * @return Whether the device is known to do post processing by default that isn't compatible with + * ExoPlayer. */ - private static boolean deviceNeedsAutoFrcWorkaround() { - // nVidia Shield prior to M tries to adjust the playback rate to better map the frame-rate of + private static boolean deviceNeedsNoPostProcessWorkaround() { + // Nvidia devices prior to M try to adjust the playback rate to better map the frame-rate of // content to the refresh rate of the display. For example playback of 23.976fps content is // adjusted to play at 1.001x speed when the output display is 60Hz. Unfortunately the // implementation causes ExoPlayer's reported playback position to drift out of sync. Captions - // also lose sync [Internal: b/26453592]. - return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER); + // also lose sync [Internal: b/26453592]. Even after M, the devices may apply post processing + // operations that can modify frame output timestamps, which is incompatible with ExoPlayer's + // logic for skipping decode-only frames. + return "NVIDIA".equals(Util.MANUFACTURER); } - /** - * Returns whether an adaptive codec with suitable {@link CodecMaxValues} will support adaptation - * between two {@link Format}s. + /* + * TODO: * - * @param first The first format. - * @param second The second format. - * @return Whether an adaptive codec with suitable {@link CodecMaxValues} will support adaptation - * between two {@link Format}s. + * 1. Validate that Android device certification now ensures correct behavior, and add a + * corresponding SDK_INT upper bound for applying the workaround (probably SDK_INT < 26). + * 2. Determine a complete list of affected devices. + * 3. Some of the devices in this list only fail to support setOutputSurface when switching from + * a SurfaceView provided Surface to a Surface of another type (e.g. TextureView/DummySurface), + * and vice versa. One hypothesis is that setOutputSurface fails when the surfaces have + * different pixel formats. If we can find a way to query the Surface instances to determine + * whether this case applies, then we'll be able to provide a more targeted workaround. */ - private static boolean areAdaptationCompatible(Format first, Format second) { - return first.sampleMimeType.equals(second.sampleMimeType) - && getRotationDegrees(first) == getRotationDegrees(second); + /** + * Returns whether the codec is known to implement {@link MediaCodec#setOutputSurface(Surface)} + * incorrectly. + * + *

If true is returned then we fall back to releasing and re-instantiating the codec instead. + * + * @param name The name of the codec. + * @return True if the device is known to implement {@link MediaCodec#setOutputSurface(Surface)} + * incorrectly. + */ + protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) { + if (name.startsWith("OMX.google")) { + // Google OMX decoders are not known to have this issue on any API level. + return false; + } + synchronized (MediaCodecVideoRenderer.class) { + if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) { + if ("dangal".equals(Util.DEVICE)) { + // Workaround for MiTV devices: + // https://github.com/google/ExoPlayer/issues/5169, + // https://github.com/google/ExoPlayer/issues/6899. + deviceNeedsSetOutputSurfaceWorkaround = true; + } else if (Util.SDK_INT <= 27 && "HWEML".equals(Util.DEVICE)) { + // Workaround for Huawei P20: + // https://github.com/google/ExoPlayer/issues/4468#issuecomment-459291645. + deviceNeedsSetOutputSurfaceWorkaround = true; + } else if (Util.SDK_INT >= 27) { + // In general, devices running API level 27 or later should be unaffected. Do nothing. + } else { + // Enable the workaround on a per-device basis. Works around: + // https://github.com/google/ExoPlayer/issues/3236, + // https://github.com/google/ExoPlayer/issues/3355, + // https://github.com/google/ExoPlayer/issues/3439, + // https://github.com/google/ExoPlayer/issues/3724, + // https://github.com/google/ExoPlayer/issues/3835, + // https://github.com/google/ExoPlayer/issues/4006, + // https://github.com/google/ExoPlayer/issues/4084, + // https://github.com/google/ExoPlayer/issues/4104, + // https://github.com/google/ExoPlayer/issues/4134, + // https://github.com/google/ExoPlayer/issues/4315, + // https://github.com/google/ExoPlayer/issues/4419, + // https://github.com/google/ExoPlayer/issues/4460, + // https://github.com/google/ExoPlayer/issues/4468, + // https://github.com/google/ExoPlayer/issues/5312, + // https://github.com/google/ExoPlayer/issues/6503. + switch (Util.DEVICE) { + case "1601": + case "1713": + case "1714": + case "A10-70F": + case "A10-70L": + case "A1601": + case "A2016a40": + case "A7000-a": + case "A7000plus": + case "A7010a48": + case "A7020a48": + case "AquaPowerM": + case "ASUS_X00AD_2": + case "Aura_Note_2": + case "BLACK-1X": + case "BRAVIA_ATV2": + case "BRAVIA_ATV3_4K": + case "C1": + case "ComioS1": + case "CP8676_I02": + case "CPH1609": + case "CPY83_I00": + case "cv1": + case "cv3": + case "deb": + case "E5643": + case "ELUGA_A3_Pro": + case "ELUGA_Note": + case "ELUGA_Prim": + case "ELUGA_Ray_X": + case "EverStar_S": + case "F3111": + case "F3113": + case "F3116": + case "F3211": + case "F3213": + case "F3215": + case "F3311": + case "flo": + case "fugu": + case "GiONEE_CBL7513": + case "GiONEE_GBL7319": + case "GIONEE_GBL7360": + case "GIONEE_SWW1609": + case "GIONEE_SWW1627": + case "GIONEE_SWW1631": + case "GIONEE_WBL5708": + case "GIONEE_WBL7365": + case "GIONEE_WBL7519": + case "griffin": + case "htc_e56ml_dtul": + case "hwALE-H": + case "HWBLN-H": + case "HWCAM-H": + case "HWVNS-H": + case "HWWAS-H": + case "i9031": + case "iball8735_9806": + case "Infinix-X572": + case "iris60": + case "itel_S41": + case "j2xlteins": + case "JGZ": + case "K50a40": + case "kate": + case "l5460": + case "le_x6": + case "LS-5017": + case "M5c": + case "manning": + case "marino_f": + case "MEIZU_M5": + case "mh": + case "mido": + case "MX6": + case "namath": + case "nicklaus_f": + case "NX541J": + case "NX573J": + case "OnePlus5T": + case "p212": + case "P681": + case "P85": + case "panell_d": + case "panell_dl": + case "panell_ds": + case "panell_dt": + case "PB2-670M": + case "PGN528": + case "PGN610": + case "PGN611": + case "Phantom6": + case "Pixi4-7_3G": + case "Pixi5-10_4G": + case "PLE": + case "PRO7S": + case "Q350": + case "Q4260": + case "Q427": + case "Q4310": + case "Q5": + case "QM16XE_U": + case "QX1": + case "santoni": + case "Slate_Pro": + case "SVP-DTV15": + case "s905x018": + case "taido_row": + case "TB3-730F": + case "TB3-730X": + case "TB3-850F": + case "TB3-850M": + case "tcl_eu": + case "V1": + case "V23GB": + case "V5": + case "vernee_M5": + case "watson": + case "whyred": + case "woods_f": + case "woods_fn": + case "X3_HK": + case "XE2X": + case "XT1663": + case "Z12_PRO": + case "Z80": + deviceNeedsSetOutputSurfaceWorkaround = true; + break; + default: + // Do nothing. + break; + } + switch (Util.MODEL) { + case "AFTA": + case "AFTN": + case "JSN-L21": + deviceNeedsSetOutputSurfaceWorkaround = true; + break; + default: + // Do nothing. + break; + } + } + evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true; + } + } + return deviceNeedsSetOutputSurfaceWorkaround; } - private static float getPixelWidthHeightRatio(Format format) { - return format.pixelWidthHeightRatio == Format.NO_VALUE ? 1 : format.pixelWidthHeightRatio; + protected Surface getSurface() { + return surface; } - private static int getRotationDegrees(Format format) { - return format.rotationDegrees == Format.NO_VALUE ? 0 : format.rotationDegrees; - } - - private static final class CodecMaxValues { + protected static final class CodecMaxValues { public final int width; public final int height; @@ -853,21 +1813,61 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @TargetApi(23) - private final class OnFrameRenderedListenerV23 implements MediaCodec.OnFrameRenderedListener { + private final class OnFrameRenderedListenerV23 + implements MediaCodec.OnFrameRenderedListener, Handler.Callback { - private OnFrameRenderedListenerV23(MediaCodec codec) { - codec.setOnFrameRenderedListener(this, new Handler()); + private static final int HANDLE_FRAME_RENDERED = 0; + + private final Handler handler; + + public OnFrameRenderedListenerV23(MediaCodec codec) { + handler = new Handler(this); + codec.setOnFrameRenderedListener(/* listener= */ this, handler); } @Override - public void onFrameRendered(@NonNull MediaCodec codec, long presentationTimeUs, long nanoTime) { + public void onFrameRendered(MediaCodec codec, long presentationTimeUs, long nanoTime) { + // Workaround bug in MediaCodec that causes deadlock if you call directly back into the + // MediaCodec from this listener method. + // Deadlock occurs because MediaCodec calls this listener method holding a lock, + // which may also be required by calls made back into the MediaCodec. + // This was fixed in https://android-review.googlesource.com/1156807. + // + // The workaround queues the event for subsequent processing, where the lock will not be held. + if (Util.SDK_INT < 30) { + Message message = + Message.obtain( + handler, + /* what= */ HANDLE_FRAME_RENDERED, + /* arg1= */ (int) (presentationTimeUs >> 32), + /* arg2= */ (int) presentationTimeUs); + handler.sendMessageAtFrontOfQueue(message); + } else { + handleFrameRendered(presentationTimeUs); + } + } + + @Override + public boolean handleMessage(Message message) { + switch (message.what) { + case HANDLE_FRAME_RENDERED: + handleFrameRendered(Util.toLong(message.arg1, message.arg2)); + return true; + default: + return false; + } + } + + private void handleFrameRendered(long presentationTimeUs) { if (this != tunnelingOnFrameRenderedListener) { // Stale event. return; } - maybeNotifyRenderedFirstFrame(); + if (presentationTimeUs == TUNNELING_EOS_PRESENTATION_TIME_US) { + onProcessedTunneledEndOfStream(); + } else { + onProcessedTunneledBuffer(presentationTimeUs); + } } - } - } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java new file mode 100644 index 000000000000..fbcd4d959c37 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java @@ -0,0 +1,975 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video; + +import android.os.Handler; +import android.os.SystemClock; +import android.view.Surface; +import androidx.annotation.CallSuper; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.BaseRenderer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderCounters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.SimpleDecoder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaCrypto; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimedValueQueue; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TraceUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Decodes and renders video using a {@link SimpleDecoder}. */ +public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { + + /** Decoder reinitialization states. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + REINITIALIZATION_STATE_NONE, + REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM, + REINITIALIZATION_STATE_WAIT_END_OF_STREAM + }) + private @interface ReinitializationState {} + /** The decoder does not need to be re-initialized. */ + private static final int REINITIALIZATION_STATE_NONE = 0; + /** + * The input format has changed in a way that requires the decoder to be re-initialized, but we + * haven't yet signaled an end of stream to the existing decoder. We need to do so in order to + * ensure that it outputs any remaining buffers before we release it. + */ + private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1; + /** + * The input format has changed in a way that requires the decoder to be re-initialized, and we've + * signaled an end of stream to the existing decoder. We're waiting for the decoder to output an + * end of stream signal to indicate that it has output any remaining buffers before we release it. + */ + private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2; + + private final long allowedJoiningTimeMs; + private final int maxDroppedFramesToNotify; + private final boolean playClearSamplesWithoutKeys; + private final EventDispatcher eventDispatcher; + private final TimedValueQueue formatQueue; + private final DecoderInputBuffer flagsOnlyBuffer; + private final DrmSessionManager drmSessionManager; + + private boolean drmResourcesAcquired; + private Format inputFormat; + private Format outputFormat; + private SimpleDecoder< + VideoDecoderInputBuffer, + ? extends VideoDecoderOutputBuffer, + ? extends VideoDecoderException> + decoder; + private VideoDecoderInputBuffer inputBuffer; + private VideoDecoderOutputBuffer outputBuffer; + @Nullable private Surface surface; + @Nullable private VideoDecoderOutputBufferRenderer outputBufferRenderer; + @C.VideoOutputMode private int outputMode; + + @Nullable private DrmSession decoderDrmSession; + @Nullable private DrmSession sourceDrmSession; + + @ReinitializationState private int decoderReinitializationState; + private boolean decoderReceivedBuffers; + + private boolean renderedFirstFrame; + private long initialPositionUs; + private long joiningDeadlineMs; + private boolean waitingForKeys; + private boolean waitingForFirstSampleInFormat; + + private boolean inputStreamEnded; + private boolean outputStreamEnded; + private int reportedWidth; + private int reportedHeight; + + private long droppedFrameAccumulationStartTimeMs; + private int droppedFrames; + private int consecutiveDroppedFrameCount; + private int buffersInCodecCount; + private long lastRenderTimeUs; + private long outputStreamOffsetUs; + + /** Decoder event counters used for debugging purposes. */ + protected DecoderCounters decoderCounters; + + /** + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between + * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. + * @param drmSessionManager For use with encrypted media. May be null if support for encrypted + * media is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + */ + protected SimpleDecoderVideoRenderer( + long allowedJoiningTimeMs, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys) { + super(C.TRACK_TYPE_VIDEO); + this.allowedJoiningTimeMs = allowedJoiningTimeMs; + this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; + this.drmSessionManager = drmSessionManager; + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + joiningDeadlineMs = C.TIME_UNSET; + clearReportedVideoSize(); + formatQueue = new TimedValueQueue<>(); + flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); + eventDispatcher = new EventDispatcher(eventHandler, eventListener); + decoderReinitializationState = REINITIALIZATION_STATE_NONE; + outputMode = C.VIDEO_OUTPUT_MODE_NONE; + } + + // BaseRenderer implementation. + + @Override + @Capabilities + public final int supportsFormat(Format format) { + return supportsFormatInternal(drmSessionManager, format); + } + + @Override + public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + if (outputStreamEnded) { + return; + } + + if (inputFormat == null) { + // We don't have a format yet, so try and read one. + FormatHolder formatHolder = getFormatHolder(); + flagsOnlyBuffer.clear(); + int result = readSource(formatHolder, flagsOnlyBuffer, true); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder); + } else if (result == C.RESULT_BUFFER_READ) { + // End of stream read having not read a format. + Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); + inputStreamEnded = true; + outputStreamEnded = true; + return; + } else { + // We still don't have a format and can't make progress without one. + return; + } + } + + // If we don't have a decoder yet, we need to instantiate one. + maybeInitDecoder(); + + if (decoder != null) { + try { + // Rendering loop. + TraceUtil.beginSection("drainAndFeed"); + while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} + while (feedInputBuffer()) {} + TraceUtil.endSection(); + } catch (VideoDecoderException e) { + throw createRendererException(e, inputFormat); + } + decoderCounters.ensureUpdated(); + } + } + + @Override + public boolean isEnded() { + return outputStreamEnded; + } + + @Override + public boolean isReady() { + if (waitingForKeys) { + return false; + } + if (inputFormat != null + && (isSourceReady() || outputBuffer != null) + && (renderedFirstFrame || !hasOutput())) { + // Ready. If we were joining then we've now joined, so clear the joining deadline. + joiningDeadlineMs = C.TIME_UNSET; + return true; + } else if (joiningDeadlineMs == C.TIME_UNSET) { + // Not joining. + return false; + } else if (SystemClock.elapsedRealtime() < joiningDeadlineMs) { + // Joining and still within the joining deadline. + return true; + } else { + // The joining deadline has been exceeded. Give up and clear the deadline. + joiningDeadlineMs = C.TIME_UNSET; + return false; + } + } + + // Protected methods. + + @Override + protected void onEnabled(boolean joining) throws ExoPlaybackException { + if (drmSessionManager != null && !drmResourcesAcquired) { + drmResourcesAcquired = true; + drmSessionManager.prepare(); + } + decoderCounters = new DecoderCounters(); + eventDispatcher.enabled(decoderCounters); + } + + @Override + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + inputStreamEnded = false; + outputStreamEnded = false; + clearRenderedFirstFrame(); + initialPositionUs = C.TIME_UNSET; + consecutiveDroppedFrameCount = 0; + if (decoder != null) { + flushDecoder(); + } + if (joining) { + setJoiningDeadlineMs(); + } else { + joiningDeadlineMs = C.TIME_UNSET; + } + formatQueue.clear(); + } + + @Override + protected void onStarted() { + droppedFrames = 0; + droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); + lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; + } + + @Override + protected void onStopped() { + joiningDeadlineMs = C.TIME_UNSET; + maybeNotifyDroppedFrames(); + } + + @Override + protected void onDisabled() { + inputFormat = null; + waitingForKeys = false; + clearReportedVideoSize(); + clearRenderedFirstFrame(); + try { + setSourceDrmSession(null); + releaseDecoder(); + } finally { + eventDispatcher.disabled(decoderCounters); + } + } + + @Override + protected void onReset() { + if (drmSessionManager != null && drmResourcesAcquired) { + drmResourcesAcquired = false; + drmSessionManager.release(); + } + } + + @Override + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + outputStreamOffsetUs = offsetUs; + super.onStreamChanged(formats, offsetUs); + } + + /** + * Called when a decoder has been created and configured. + * + *

The default implementation is a no-op. + * + * @param name The name of the decoder that was initialized. + * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization + * finished. + * @param initializationDurationMs The time taken to initialize the decoder, in milliseconds. + */ + @CallSuper + protected void onDecoderInitialized( + String name, long initializedTimestampMs, long initializationDurationMs) { + eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); + } + + /** + * Flushes the decoder. + * + * @throws ExoPlaybackException If an error occurs reinitializing a decoder. + */ + @CallSuper + protected void flushDecoder() throws ExoPlaybackException { + waitingForKeys = false; + buffersInCodecCount = 0; + if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { + releaseDecoder(); + maybeInitDecoder(); + } else { + inputBuffer = null; + if (outputBuffer != null) { + outputBuffer.release(); + outputBuffer = null; + } + decoder.flush(); + decoderReceivedBuffers = false; + } + } + + /** Releases the decoder. */ + @CallSuper + protected void releaseDecoder() { + inputBuffer = null; + outputBuffer = null; + decoderReinitializationState = REINITIALIZATION_STATE_NONE; + decoderReceivedBuffers = false; + buffersInCodecCount = 0; + if (decoder != null) { + decoder.release(); + decoder = null; + decoderCounters.decoderReleaseCount++; + } + setDecoderDrmSession(null); + } + + /** + * Called when a new format is read from the upstream source. + * + * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}. + * @throws ExoPlaybackException If an error occurs (re-)initializing the decoder. + */ + @CallSuper + @SuppressWarnings("unchecked") + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + waitingForFirstSampleInFormat = true; + Format newFormat = Assertions.checkNotNull(formatHolder.format); + if (formatHolder.includesDrmSession) { + setSourceDrmSession((DrmSession) formatHolder.drmSession); + } else { + sourceDrmSession = + getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession); + } + inputFormat = newFormat; + + if (sourceDrmSession != decoderDrmSession) { + if (decoderReceivedBuffers) { + // Signal end of stream and wait for any final output buffers before re-initialization. + decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; + } else { + // There aren't any final output buffers, so release the decoder immediately. + releaseDecoder(); + maybeInitDecoder(); + } + } + + eventDispatcher.inputFormatChanged(inputFormat); + } + + /** + * Called immediately before an input buffer is queued into the decoder. + * + *

The default implementation is a no-op. + * + * @param buffer The buffer that will be queued. + */ + protected void onQueueInputBuffer(VideoDecoderInputBuffer buffer) { + // Do nothing. + } + + /** + * Called when an output buffer is successfully processed. + * + * @param presentationTimeUs The timestamp associated with the output buffer. + */ + @CallSuper + protected void onProcessedOutputBuffer(long presentationTimeUs) { + buffersInCodecCount--; + } + + /** + * Returns whether the buffer being processed should be dropped. + * + * @param earlyUs The time until the buffer should be presented in microseconds. A negative value + * indicates that the buffer is late. + * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, + * measured at the start of the current iteration of the rendering loop. + */ + protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) { + return isBufferLate(earlyUs); + } + + /** + * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after + * the current playback position, if possible. + * + * @param earlyUs The time until the current buffer should be presented in microseconds. A + * negative value indicates that the buffer is late. + * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, + * measured at the start of the current iteration of the rendering loop. + */ + protected boolean shouldDropBuffersToKeyframe(long earlyUs, long elapsedRealtimeUs) { + return isBufferVeryLate(earlyUs); + } + + /** + * Returns whether to force rendering an output buffer. + * + * @param earlyUs The time until the current buffer should be presented in microseconds. A + * negative value indicates that the buffer is late. + * @param elapsedSinceLastRenderUs The elapsed time since the last output buffer was rendered, in + * microseconds. + * @return Returns whether to force rendering an output buffer. + */ + protected boolean shouldForceRenderOutputBuffer(long earlyUs, long elapsedSinceLastRenderUs) { + return isBufferLate(earlyUs) && elapsedSinceLastRenderUs > 100000; + } + + /** + * Skips the specified output buffer and releases it. + * + * @param outputBuffer The output buffer to skip. + */ + protected void skipOutputBuffer(VideoDecoderOutputBuffer outputBuffer) { + decoderCounters.skippedOutputBufferCount++; + outputBuffer.release(); + } + + /** + * Drops the specified output buffer and releases it. + * + * @param outputBuffer The output buffer to drop. + */ + protected void dropOutputBuffer(VideoDecoderOutputBuffer outputBuffer) { + updateDroppedBufferCounters(1); + outputBuffer.release(); + } + + /** + * Drops frames from the current output buffer to the next keyframe at or before the playback + * position. If no such keyframe exists, as the playback position is inside the same group of + * pictures as the buffer being processed, returns {@code false}. Returns {@code true} otherwise. + * + * @param positionUs The current playback position, in microseconds. + * @return Whether any buffers were dropped. + * @throws ExoPlaybackException If an error occurs flushing the decoder. + */ + protected boolean maybeDropBuffersToKeyframe(long positionUs) throws ExoPlaybackException { + int droppedSourceBufferCount = skipSource(positionUs); + if (droppedSourceBufferCount == 0) { + return false; + } + decoderCounters.droppedToKeyframeCount++; + // We dropped some buffers to catch up, so update the decoder counters and flush the decoder, + // which releases all pending buffers buffers including the current output buffer. + updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount); + flushDecoder(); + return true; + } + + /** + * Updates decoder counters to reflect that {@code droppedBufferCount} additional buffers were + * dropped. + * + * @param droppedBufferCount The number of additional dropped buffers. + */ + protected void updateDroppedBufferCounters(int droppedBufferCount) { + decoderCounters.droppedBufferCount += droppedBufferCount; + droppedFrames += droppedBufferCount; + consecutiveDroppedFrameCount += droppedBufferCount; + decoderCounters.maxConsecutiveDroppedBufferCount = + Math.max(consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedBufferCount); + if (maxDroppedFramesToNotify > 0 && droppedFrames >= maxDroppedFramesToNotify) { + maybeNotifyDroppedFrames(); + } + } + + /** + * Returns the {@link Capabilities} for the given {@link Format}. + * + * @param drmSessionManager The renderer's {@link DrmSessionManager}. + * @param format The format, which has a video {@link Format#sampleMimeType}. + * @return The {@link Capabilities} for this {@link Format}. + * @see RendererCapabilities#supportsFormat(Format) + */ + @Capabilities + protected abstract int supportsFormatInternal( + @Nullable DrmSessionManager drmSessionManager, Format format); + + /** + * Creates a decoder for the given format. + * + * @param format The format for which a decoder is required. + * @param mediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted content. + * May be null and can be ignored if decoder does not handle encrypted content. + * @return The decoder. + * @throws VideoDecoderException If an error occurred creating a suitable decoder. + */ + protected abstract SimpleDecoder< + VideoDecoderInputBuffer, + ? extends VideoDecoderOutputBuffer, + ? extends VideoDecoderException> + createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) + throws VideoDecoderException; + + /** + * Renders the specified output buffer. + * + *

The implementation of this method takes ownership of the output buffer and is responsible + * for calling {@link VideoDecoderOutputBuffer#release()} either immediately or in the future. + * + * @param outputBuffer {@link VideoDecoderOutputBuffer} to render. + * @param presentationTimeUs Presentation time in microseconds. + * @param outputFormat Output {@link Format}. + * @throws VideoDecoderException If an error occurs when rendering the output buffer. + */ + protected void renderOutputBuffer( + VideoDecoderOutputBuffer outputBuffer, long presentationTimeUs, Format outputFormat) + throws VideoDecoderException { + lastRenderTimeUs = C.msToUs(SystemClock.elapsedRealtime() * 1000); + int bufferMode = outputBuffer.mode; + boolean renderSurface = bufferMode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && surface != null; + boolean renderYuv = bufferMode == C.VIDEO_OUTPUT_MODE_YUV && outputBufferRenderer != null; + if (!renderYuv && !renderSurface) { + dropOutputBuffer(outputBuffer); + } else { + maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height); + if (renderYuv) { + outputBufferRenderer.setOutputBuffer(outputBuffer); + } else { + renderOutputBufferToSurface(outputBuffer, surface); + } + consecutiveDroppedFrameCount = 0; + decoderCounters.renderedOutputBufferCount++; + maybeNotifyRenderedFirstFrame(); + } + } + + /** + * Renders the specified output buffer to the passed surface. + * + *

The implementation of this method takes ownership of the output buffer and is responsible + * for calling {@link VideoDecoderOutputBuffer#release()} either immediately or in the future. + * + * @param outputBuffer {@link VideoDecoderOutputBuffer} to render. + * @param surface Output {@link Surface}. + * @throws VideoDecoderException If an error occurs when rendering the output buffer. + */ + protected abstract void renderOutputBufferToSurface( + VideoDecoderOutputBuffer outputBuffer, Surface surface) throws VideoDecoderException; + + /** + * Sets output surface. + * + * @param surface Surface. + */ + protected final void setOutputSurface(@Nullable Surface surface) { + if (this.surface != surface) { + // The output has changed. + this.surface = surface; + if (surface != null) { + outputBufferRenderer = null; + outputMode = C.VIDEO_OUTPUT_MODE_SURFACE_YUV; + if (decoder != null) { + setDecoderOutputMode(outputMode); + } + onOutputChanged(); + } else { + // The output has been removed. We leave the outputMode of the underlying decoder unchanged + // in anticipation that a subsequent output will likely be of the same type. + outputMode = C.VIDEO_OUTPUT_MODE_NONE; + onOutputRemoved(); + } + } else if (surface != null) { + // The output is unchanged and non-null. + onOutputReset(); + } + } + + /** + * Sets output buffer renderer. + * + * @param outputBufferRenderer Output buffer renderer. + */ + protected final void setOutputBufferRenderer( + @Nullable VideoDecoderOutputBufferRenderer outputBufferRenderer) { + if (this.outputBufferRenderer != outputBufferRenderer) { + // The output has changed. + this.outputBufferRenderer = outputBufferRenderer; + if (outputBufferRenderer != null) { + surface = null; + outputMode = C.VIDEO_OUTPUT_MODE_YUV; + if (decoder != null) { + setDecoderOutputMode(outputMode); + } + onOutputChanged(); + } else { + // The output has been removed. We leave the outputMode of the underlying decoder unchanged + // in anticipation that a subsequent output will likely be of the same type. + outputMode = C.VIDEO_OUTPUT_MODE_NONE; + onOutputRemoved(); + } + } else if (outputBufferRenderer != null) { + // The output is unchanged and non-null. + onOutputReset(); + } + } + + /** + * Sets output mode of the decoder. + * + * @param outputMode Output mode. + */ + protected abstract void setDecoderOutputMode(@C.VideoOutputMode int outputMode); + + // Internal methods. + + private void setSourceDrmSession(@Nullable DrmSession session) { + DrmSession.replaceSession(sourceDrmSession, session); + sourceDrmSession = session; + } + + private void setDecoderDrmSession(@Nullable DrmSession session) { + DrmSession.replaceSession(decoderDrmSession, session); + decoderDrmSession = session; + } + + private void maybeInitDecoder() throws ExoPlaybackException { + if (decoder != null) { + return; + } + + setDecoderDrmSession(sourceDrmSession); + + ExoMediaCrypto mediaCrypto = null; + if (decoderDrmSession != null) { + mediaCrypto = decoderDrmSession.getMediaCrypto(); + if (mediaCrypto == null) { + DrmSessionException drmError = decoderDrmSession.getError(); + if (drmError != null) { + // Continue for now. We may be able to avoid failure if the session recovers, or if a new + // input format causes the session to be replaced before it's used. + } else { + // The drm session isn't open yet. + return; + } + } + } + + try { + long decoderInitializingTimestamp = SystemClock.elapsedRealtime(); + decoder = createDecoder(inputFormat, mediaCrypto); + setDecoderOutputMode(outputMode); + long decoderInitializedTimestamp = SystemClock.elapsedRealtime(); + onDecoderInitialized( + decoder.getName(), + decoderInitializedTimestamp, + decoderInitializedTimestamp - decoderInitializingTimestamp); + decoderCounters.decoderInitCount++; + } catch (VideoDecoderException e) { + throw createRendererException(e, inputFormat); + } + } + + private boolean feedInputBuffer() throws VideoDecoderException, ExoPlaybackException { + if (decoder == null + || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM + || inputStreamEnded) { + // We need to reinitialize the decoder or the input stream has ended. + return false; + } + + if (inputBuffer == null) { + inputBuffer = decoder.dequeueInputBuffer(); + if (inputBuffer == null) { + return false; + } + } + + if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) { + inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + decoder.queueInputBuffer(inputBuffer); + inputBuffer = null; + decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM; + return false; + } + + int result; + FormatHolder formatHolder = getFormatHolder(); + if (waitingForKeys) { + // We've already read an encrypted sample into buffer, and are waiting for keys. + result = C.RESULT_BUFFER_READ; + } else { + result = readSource(formatHolder, inputBuffer, false); + } + + if (result == C.RESULT_NOTHING_READ) { + return false; + } + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder); + return true; + } + if (inputBuffer.isEndOfStream()) { + inputStreamEnded = true; + decoder.queueInputBuffer(inputBuffer); + inputBuffer = null; + return false; + } + boolean bufferEncrypted = inputBuffer.isEncrypted(); + waitingForKeys = shouldWaitForKeys(bufferEncrypted); + if (waitingForKeys) { + return false; + } + if (waitingForFirstSampleInFormat) { + formatQueue.add(inputBuffer.timeUs, inputFormat); + waitingForFirstSampleInFormat = false; + } + inputBuffer.flip(); + inputBuffer.colorInfo = inputFormat.colorInfo; + onQueueInputBuffer(inputBuffer); + decoder.queueInputBuffer(inputBuffer); + buffersInCodecCount++; + decoderReceivedBuffers = true; + decoderCounters.inputBufferCount++; + inputBuffer = null; + return true; + } + + /** + * Attempts to dequeue an output buffer from the decoder and, if successful, passes it to {@link + * #processOutputBuffer(long, long)}. + * + * @param positionUs The player's current position. + * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, + * measured at the start of the current iteration of the rendering loop. + * @return Whether it may be possible to drain more output data. + * @throws ExoPlaybackException If an error occurs draining the output buffer. + */ + private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException, VideoDecoderException { + if (outputBuffer == null) { + outputBuffer = decoder.dequeueOutputBuffer(); + if (outputBuffer == null) { + return false; + } + decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount; + buffersInCodecCount -= outputBuffer.skippedOutputBufferCount; + } + + if (outputBuffer.isEndOfStream()) { + if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) { + // We're waiting to re-initialize the decoder, and have now processed all final buffers. + releaseDecoder(); + maybeInitDecoder(); + } else { + outputBuffer.release(); + outputBuffer = null; + outputStreamEnded = true; + } + return false; + } + + boolean processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs); + if (processedOutputBuffer) { + onProcessedOutputBuffer(outputBuffer.timeUs); + outputBuffer = null; + } + return processedOutputBuffer; + } + + /** + * Processes {@link #outputBuffer} by rendering it, skipping it or doing nothing, and returns + * whether it may be possible to process another output buffer. + * + * @param positionUs The player's current position. + * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, + * measured at the start of the current iteration of the rendering loop. + * @return Whether it may be possible to drain another output buffer. + * @throws ExoPlaybackException If an error occurs processing the output buffer. + */ + private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException, VideoDecoderException { + if (initialPositionUs == C.TIME_UNSET) { + initialPositionUs = positionUs; + } + + long earlyUs = outputBuffer.timeUs - positionUs; + if (!hasOutput()) { + // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. + if (isBufferLate(earlyUs)) { + skipOutputBuffer(outputBuffer); + return true; + } + return false; + } + + long presentationTimeUs = outputBuffer.timeUs - outputStreamOffsetUs; + Format format = formatQueue.pollFloor(presentationTimeUs); + if (format != null) { + outputFormat = format; + } + + long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000; + boolean isStarted = getState() == STATE_STARTED; + if (!renderedFirstFrame + || (isStarted + && shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) { + renderOutputBuffer(outputBuffer, presentationTimeUs, outputFormat); + return true; + } + + if (!isStarted || positionUs == initialPositionUs) { + return false; + } + + if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs) + && maybeDropBuffersToKeyframe(positionUs)) { + return false; + } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { + dropOutputBuffer(outputBuffer); + return true; + } + + if (earlyUs < 30000) { + renderOutputBuffer(outputBuffer, presentationTimeUs, outputFormat); + return true; + } + + return false; + } + + private boolean hasOutput() { + return outputMode != C.VIDEO_OUTPUT_MODE_NONE; + } + + private void onOutputChanged() { + // If we know the video size, report it again immediately. + maybeRenotifyVideoSizeChanged(); + // We haven't rendered to the new output yet. + clearRenderedFirstFrame(); + if (getState() == STATE_STARTED) { + setJoiningDeadlineMs(); + } + } + + private void onOutputRemoved() { + clearReportedVideoSize(); + clearRenderedFirstFrame(); + } + + private void onOutputReset() { + // The output is unchanged and non-null. If we know the video size and/or have already + // rendered to the output, report these again immediately. + maybeRenotifyVideoSizeChanged(); + maybeRenotifyRenderedFirstFrame(); + } + + private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { + if (decoderDrmSession == null + || (!bufferEncrypted + && (playClearSamplesWithoutKeys || decoderDrmSession.playClearSamplesWithoutKeys()))) { + return false; + } + @DrmSession.State int drmSessionState = decoderDrmSession.getState(); + if (drmSessionState == DrmSession.STATE_ERROR) { + throw createRendererException(decoderDrmSession.getError(), inputFormat); + } + return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; + } + + private void setJoiningDeadlineMs() { + joiningDeadlineMs = + allowedJoiningTimeMs > 0 + ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) + : C.TIME_UNSET; + } + + private void clearRenderedFirstFrame() { + renderedFirstFrame = false; + } + + private void maybeNotifyRenderedFirstFrame() { + if (!renderedFirstFrame) { + renderedFirstFrame = true; + eventDispatcher.renderedFirstFrame(surface); + } + } + + private void maybeRenotifyRenderedFirstFrame() { + if (renderedFirstFrame) { + eventDispatcher.renderedFirstFrame(surface); + } + } + + private void clearReportedVideoSize() { + reportedWidth = Format.NO_VALUE; + reportedHeight = Format.NO_VALUE; + } + + private void maybeNotifyVideoSizeChanged(int width, int height) { + if (reportedWidth != width || reportedHeight != height) { + reportedWidth = width; + reportedHeight = height; + eventDispatcher.videoSizeChanged( + width, height, /* unappliedRotationDegrees= */ 0, /* pixelWidthHeightRatio= */ 1); + } + } + + private void maybeRenotifyVideoSizeChanged() { + if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) { + eventDispatcher.videoSizeChanged( + reportedWidth, + reportedHeight, + /* unappliedRotationDegrees= */ 0, + /* pixelWidthHeightRatio= */ 1); + } + } + + private void maybeNotifyDroppedFrames() { + if (droppedFrames > 0) { + long now = SystemClock.elapsedRealtime(); + long elapsedMs = now - droppedFrameAccumulationStartTimeMs; + eventDispatcher.droppedFrames(droppedFrames, elapsedMs); + droppedFrames = 0; + droppedFrameAccumulationStartTimeMs = now; + } + } + + private static boolean isBufferLate(long earlyUs) { + // Class a buffer as late if it should have been presented more than 30 ms ago. + return earlyUs < -30000; + } + + private static boolean isBufferVeryLate(long earlyUs) { + // Class a buffer as very late if it should have been presented more than 500 ms ago. + return earlyUs < -500000; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderException.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderException.java new file mode 100644 index 000000000000..dfffbe049bec --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderException.java @@ -0,0 +1,40 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video; + +/** Thrown when a video decoder error occurs. */ +public class VideoDecoderException extends Exception { + + /** + * Creates an instance with the given message. + * + * @param message The detail message for this exception. + */ + public VideoDecoderException(String message) { + super(message); + } + + /** + * Creates an instance with the given message and cause. + * + * @param message The detail message for this exception. + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). + * A null value is permitted, and indicates that the cause is nonexistent or unknown. + */ + public VideoDecoderException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java new file mode 100644 index 000000000000..69249dd42610 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video; + +import android.content.Context; +import android.opengl.GLSurfaceView; +import android.util.AttributeSet; +import androidx.annotation.Nullable; + +/** + * GLSurfaceView for rendering video output. To render video in this view, call {@link + * #getVideoDecoderOutputBufferRenderer()} to get a {@link VideoDecoderOutputBufferRenderer} that + * will render video decoder output buffers in this view. + * + *

This view is intended for use only with extension renderers. For other use cases a {@link + * android.view.SurfaceView} or {@link android.view.TextureView} should be used instead. + */ +public class VideoDecoderGLSurfaceView extends GLSurfaceView { + + private final VideoDecoderRenderer renderer; + + /** @param context A {@link Context}. */ + public VideoDecoderGLSurfaceView(Context context) { + this(context, /* attrs= */ null); + } + + /** + * @param context A {@link Context}. + * @param attrs Custom attributes. + */ + public VideoDecoderGLSurfaceView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + renderer = new VideoDecoderRenderer(this); + setPreserveEGLContextOnPause(true); + setEGLContextClientVersion(2); + setRenderer(renderer); + setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + } + + /** Returns the {@link VideoDecoderOutputBufferRenderer} that will render frames in this view. */ + public VideoDecoderOutputBufferRenderer getVideoDecoderOutputBufferRenderer() { + return renderer; + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java new file mode 100644 index 000000000000..d911ac3a5af6 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java @@ -0,0 +1,30 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; + +/** Input buffer to a video decoder. */ +public class VideoDecoderInputBuffer extends DecoderInputBuffer { + + @Nullable public ColorInfo colorInfo; + + public VideoDecoderInputBuffer() { + super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); + } + +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java new file mode 100644 index 000000000000..b09e8b759ad9 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java @@ -0,0 +1,185 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.OutputBuffer; +import java.nio.ByteBuffer; + +/** Video decoder output buffer containing video frame data. */ +public class VideoDecoderOutputBuffer extends OutputBuffer { + + /** Buffer owner. */ + public interface Owner { + + /** + * Releases the buffer. + * + * @param outputBuffer Output buffer. + */ + void releaseOutputBuffer(VideoDecoderOutputBuffer outputBuffer); + } + + // LINT.IfChange + public static final int COLORSPACE_UNKNOWN = 0; + public static final int COLORSPACE_BT601 = 1; + public static final int COLORSPACE_BT709 = 2; + public static final int COLORSPACE_BT2020 = 3; + // LINT.ThenChange( + // ../../../../../../../../../../extensions/av1/src/main/jni/gav1_jni.cc, + // ../../../../../../../../../../extensions/vp9/src/main/jni/vpx_jni.cc + // ) + + /** Decoder private data. */ + public int decoderPrivate; + + /** Output mode. */ + @C.VideoOutputMode public int mode; + /** RGB buffer for RGB mode. */ + @Nullable public ByteBuffer data; + + public int width; + public int height; + @Nullable public ColorInfo colorInfo; + + /** YUV planes for YUV mode. */ + @Nullable public ByteBuffer[] yuvPlanes; + + @Nullable public int[] yuvStrides; + public int colorspace; + + /** + * Supplemental data related to the output frame, if {@link #hasSupplementalData()} returns true. + * If present, the buffer is populated with supplemental data from position 0 to its limit. + */ + @Nullable public ByteBuffer supplementalData; + + private final Owner owner; + + /** + * Creates VideoDecoderOutputBuffer. + * + * @param owner Buffer owner. + */ + public VideoDecoderOutputBuffer(Owner owner) { + this.owner = owner; + } + + @Override + public void release() { + owner.releaseOutputBuffer(this); + } + + /** + * Initializes the buffer. + * + * @param timeUs The presentation timestamp for the buffer, in microseconds. + * @param mode The output mode. One of {@link C#VIDEO_OUTPUT_MODE_NONE}, {@link + * C#VIDEO_OUTPUT_MODE_YUV} and {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV}. + * @param supplementalData Supplemental data associated with the frame, or {@code null} if not + * present. It is safe to reuse the provided buffer after this method returns. + */ + public void init( + long timeUs, @C.VideoOutputMode int mode, @Nullable ByteBuffer supplementalData) { + this.timeUs = timeUs; + this.mode = mode; + if (supplementalData != null && supplementalData.hasRemaining()) { + addFlag(C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA); + int size = supplementalData.limit(); + if (this.supplementalData == null || this.supplementalData.capacity() < size) { + this.supplementalData = ByteBuffer.allocate(size); + } else { + this.supplementalData.clear(); + } + this.supplementalData.put(supplementalData); + this.supplementalData.flip(); + supplementalData.position(0); + } else { + this.supplementalData = null; + } + } + + /** + * Resizes the buffer based on the given stride. Called via JNI after decoding completes. + * + * @return Whether the buffer was resized successfully. + */ + public boolean initForYuvFrame(int width, int height, int yStride, int uvStride, int colorspace) { + this.width = width; + this.height = height; + this.colorspace = colorspace; + int uvHeight = (int) (((long) height + 1) / 2); + if (!isSafeToMultiply(yStride, height) || !isSafeToMultiply(uvStride, uvHeight)) { + return false; + } + int yLength = yStride * height; + int uvLength = uvStride * uvHeight; + int minimumYuvSize = yLength + (uvLength * 2); + if (!isSafeToMultiply(uvLength, 2) || minimumYuvSize < yLength) { + return false; + } + + // Initialize data. + if (data == null || data.capacity() < minimumYuvSize) { + data = ByteBuffer.allocateDirect(minimumYuvSize); + } else { + data.position(0); + data.limit(minimumYuvSize); + } + + if (yuvPlanes == null) { + yuvPlanes = new ByteBuffer[3]; + } + + ByteBuffer data = this.data; + ByteBuffer[] yuvPlanes = this.yuvPlanes; + + // Rewrapping has to be done on every frame since the stride might have changed. + yuvPlanes[0] = data.slice(); + yuvPlanes[0].limit(yLength); + data.position(yLength); + yuvPlanes[1] = data.slice(); + yuvPlanes[1].limit(uvLength); + data.position(yLength + uvLength); + yuvPlanes[2] = data.slice(); + yuvPlanes[2].limit(uvLength); + if (yuvStrides == null) { + yuvStrides = new int[3]; + } + yuvStrides[0] = yStride; + yuvStrides[1] = uvStride; + yuvStrides[2] = uvStride; + return true; + } + + /** + * Configures the buffer for the given frame dimensions when passing actual frame data via {@link + * #decoderPrivate}. Called via JNI after decoding completes. + */ + public void initForPrivateFrame(int width, int height) { + this.width = width; + this.height = height; + } + + /** + * Ensures that the result of multiplying individual numbers can fit into the size limit of an + * integer. + */ + private static boolean isSafeToMultiply(int a, int b) { + return a >= 0 && b >= 0 && !(b > 0 && a >= Integer.MAX_VALUE / b); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderOutputBufferRenderer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderOutputBufferRenderer.java new file mode 100644 index 000000000000..f4058ea40f85 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderOutputBufferRenderer.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video; + +/** Renders the {@link VideoDecoderOutputBuffer}. */ +public interface VideoDecoderOutputBufferRenderer { + + /** + * Sets the output buffer to be rendered. The renderer is responsible for releasing the buffer. + * + * @param outputBuffer The output buffer to be rendered. + */ + void setOutputBuffer(VideoDecoderOutputBuffer outputBuffer); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderRenderer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderRenderer.java new file mode 100644 index 000000000000..1e302e4aaae7 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderRenderer.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video; + +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.GlUtil; +import java.nio.FloatBuffer; +import java.util.concurrent.atomic.AtomicReference; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * GLSurfaceView.Renderer implementation that can render YUV Frames returned by a video decoder + * after decoding. It does the YUV to RGB color conversion in the Fragment Shader. + */ +/* package */ class VideoDecoderRenderer + implements GLSurfaceView.Renderer, VideoDecoderOutputBufferRenderer { + + private static final float[] kColorConversion601 = { + 1.164f, 1.164f, 1.164f, + 0.0f, -0.392f, 2.017f, + 1.596f, -0.813f, 0.0f, + }; + + private static final float[] kColorConversion709 = { + 1.164f, 1.164f, 1.164f, + 0.0f, -0.213f, 2.112f, + 1.793f, -0.533f, 0.0f, + }; + + private static final float[] kColorConversion2020 = { + 1.168f, 1.168f, 1.168f, + 0.0f, -0.188f, 2.148f, + 1.683f, -0.652f, 0.0f, + }; + + private static final String VERTEX_SHADER = + "varying vec2 interp_tc_y;\n" + + "varying vec2 interp_tc_u;\n" + + "varying vec2 interp_tc_v;\n" + + "attribute vec4 in_pos;\n" + + "attribute vec2 in_tc_y;\n" + + "attribute vec2 in_tc_u;\n" + + "attribute vec2 in_tc_v;\n" + + "void main() {\n" + + " gl_Position = in_pos;\n" + + " interp_tc_y = in_tc_y;\n" + + " interp_tc_u = in_tc_u;\n" + + " interp_tc_v = in_tc_v;\n" + + "}\n"; + private static final String[] TEXTURE_UNIFORMS = {"y_tex", "u_tex", "v_tex"}; + private static final String FRAGMENT_SHADER = + "precision mediump float;\n" + + "varying vec2 interp_tc_y;\n" + + "varying vec2 interp_tc_u;\n" + + "varying vec2 interp_tc_v;\n" + + "uniform sampler2D y_tex;\n" + + "uniform sampler2D u_tex;\n" + + "uniform sampler2D v_tex;\n" + + "uniform mat3 mColorConversion;\n" + + "void main() {\n" + + " vec3 yuv;\n" + + " yuv.x = texture2D(y_tex, interp_tc_y).r - 0.0625;\n" + + " yuv.y = texture2D(u_tex, interp_tc_u).r - 0.5;\n" + + " yuv.z = texture2D(v_tex, interp_tc_v).r - 0.5;\n" + + " gl_FragColor = vec4(mColorConversion * yuv, 1.0);\n" + + "}\n"; + + private static final FloatBuffer TEXTURE_VERTICES = + GlUtil.createBuffer(new float[] {-1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, -1.0f}); + private final GLSurfaceView surfaceView; + private final int[] yuvTextures = new int[3]; + private final AtomicReference pendingOutputBufferReference; + + // Kept in field rather than a local variable in order not to get garbage collected before + // glDrawArrays uses it. + private FloatBuffer[] textureCoords; + + private int program; + private int[] texLocations; + private int colorMatrixLocation; + private int[] previousWidths; + private int[] previousStrides; + + @Nullable + private VideoDecoderOutputBuffer renderedOutputBuffer; // Accessed only from the GL thread. + + public VideoDecoderRenderer(GLSurfaceView surfaceView) { + this.surfaceView = surfaceView; + pendingOutputBufferReference = new AtomicReference<>(); + textureCoords = new FloatBuffer[3]; + texLocations = new int[3]; + previousWidths = new int[3]; + previousStrides = new int[3]; + for (int i = 0; i < 3; i++) { + previousWidths[i] = previousStrides[i] = -1; + } + } + + @Override + public void onSurfaceCreated(GL10 unused, EGLConfig config) { + program = GlUtil.compileProgram(VERTEX_SHADER, FRAGMENT_SHADER); + GLES20.glUseProgram(program); + int posLocation = GLES20.glGetAttribLocation(program, "in_pos"); + GLES20.glEnableVertexAttribArray(posLocation); + GLES20.glVertexAttribPointer(posLocation, 2, GLES20.GL_FLOAT, false, 0, TEXTURE_VERTICES); + texLocations[0] = GLES20.glGetAttribLocation(program, "in_tc_y"); + GLES20.glEnableVertexAttribArray(texLocations[0]); + texLocations[1] = GLES20.glGetAttribLocation(program, "in_tc_u"); + GLES20.glEnableVertexAttribArray(texLocations[1]); + texLocations[2] = GLES20.glGetAttribLocation(program, "in_tc_v"); + GLES20.glEnableVertexAttribArray(texLocations[2]); + GlUtil.checkGlError(); + colorMatrixLocation = GLES20.glGetUniformLocation(program, "mColorConversion"); + GlUtil.checkGlError(); + setupTextures(); + GlUtil.checkGlError(); + } + + @Override + public void onSurfaceChanged(GL10 unused, int width, int height) { + GLES20.glViewport(0, 0, width, height); + } + + @Override + public void onDrawFrame(GL10 unused) { + VideoDecoderOutputBuffer pendingOutputBuffer = pendingOutputBufferReference.getAndSet(null); + if (pendingOutputBuffer == null && renderedOutputBuffer == null) { + // There is no output buffer to render at the moment. + return; + } + if (pendingOutputBuffer != null) { + if (renderedOutputBuffer != null) { + renderedOutputBuffer.release(); + } + renderedOutputBuffer = pendingOutputBuffer; + } + VideoDecoderOutputBuffer outputBuffer = renderedOutputBuffer; + // Set color matrix. Assume BT709 if the color space is unknown. + float[] colorConversion = kColorConversion709; + switch (outputBuffer.colorspace) { + case VideoDecoderOutputBuffer.COLORSPACE_BT601: + colorConversion = kColorConversion601; + break; + case VideoDecoderOutputBuffer.COLORSPACE_BT2020: + colorConversion = kColorConversion2020; + break; + case VideoDecoderOutputBuffer.COLORSPACE_BT709: + default: + break; // Do nothing + } + GLES20.glUniformMatrix3fv(colorMatrixLocation, 1, false, colorConversion, 0); + + for (int i = 0; i < 3; i++) { + int h = (i == 0) ? outputBuffer.height : (outputBuffer.height + 1) / 2; + GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]); + GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1); + GLES20.glTexImage2D( + GLES20.GL_TEXTURE_2D, + 0, + GLES20.GL_LUMINANCE, + outputBuffer.yuvStrides[i], + h, + 0, + GLES20.GL_LUMINANCE, + GLES20.GL_UNSIGNED_BYTE, + outputBuffer.yuvPlanes[i]); + } + + int[] widths = new int[3]; + widths[0] = outputBuffer.width; + // TODO: Handle streams where chroma channels are not stored at half width and height + // compared to luma channel. See [Internal: b/142097774]. + // U and V planes are being stored at half width compared to Y. + widths[1] = widths[2] = (widths[0] + 1) / 2; + for (int i = 0; i < 3; i++) { + // Set cropping of stride if either width or stride has changed. + if (previousWidths[i] != widths[i] || previousStrides[i] != outputBuffer.yuvStrides[i]) { + Assertions.checkState(outputBuffer.yuvStrides[i] != 0); + float widthRatio = (float) widths[i] / outputBuffer.yuvStrides[i]; + // These buffers are consumed during each call to glDrawArrays. They need to be member + // variables rather than local variables in order not to get garbage collected. + textureCoords[i] = + GlUtil.createBuffer( + new float[] {0.0f, 0.0f, 0.0f, 1.0f, widthRatio, 0.0f, widthRatio, 1.0f}); + GLES20.glVertexAttribPointer( + texLocations[i], 2, GLES20.GL_FLOAT, false, 0, textureCoords[i]); + previousWidths[i] = widths[i]; + previousStrides[i] = outputBuffer.yuvStrides[i]; + } + } + + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + GlUtil.checkGlError(); + } + + @Override + public void setOutputBuffer(VideoDecoderOutputBuffer outputBuffer) { + VideoDecoderOutputBuffer oldPendingOutputBuffer = + pendingOutputBufferReference.getAndSet(outputBuffer); + if (oldPendingOutputBuffer != null) { + // The old pending output buffer will never be used for rendering, so release it now. + oldPendingOutputBuffer.release(); + } + surfaceView.requestRender(); + } + + private void setupTextures() { + GLES20.glGenTextures(3, yuvTextures, 0); + for (int i = 0; i < 3; i++) { + GLES20.glUniform1i(GLES20.glGetUniformLocation(program, TEXTURE_UNIFORMS[i]), i); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + } + GlUtil.checkGlError(); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java new file mode 100644 index 000000000000..46e05def5c50 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video; + +import android.media.MediaFormat; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; + +/** A listener for metadata corresponding to video frame being rendered. */ +public interface VideoFrameMetadataListener { + /** + * Called when the video frame about to be rendered. This method is called on the playback thread. + * + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + * @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds. + * If the platform API version of the device is less than 21, then this is the best effort. + * @param format The format associated with the frame. + * @param mediaFormat The framework media format associated with the frame, or {@code null} if not + * known or not applicable (e.g., because the frame was not output by a {@link + * android.media.MediaCodec MediaCodec}). + */ + void onVideoFrameAboutToBeRendered( + long presentationTimeUs, + long releaseTimeNs, + Format format, + @Nullable MediaFormat mediaFormat); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java index 0f5e731a96cf..c13cd4b1cb91 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java @@ -17,18 +17,21 @@ package org.mozilla.thirdparty.com.google.android.exoplayer2.video; import android.annotation.TargetApi; import android.content.Context; +import android.hardware.display.DisplayManager; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.view.Choreographer; import android.view.Choreographer.FrameCallback; +import android.view.Display; import android.view.WindowManager; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; /** * Makes a best effort to adjust frame release timestamps for a smoother visual result. */ -@TargetApi(16) public final class VideoFrameReleaseTimeHelper { private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500; @@ -37,10 +40,12 @@ public final class VideoFrameReleaseTimeHelper { private static final long VSYNC_OFFSET_PERCENTAGE = 80; private static final int MIN_FRAMES_FOR_ADJUSTMENT = 6; + private final WindowManager windowManager; private final VSyncSampler vsyncSampler; - private final boolean useDefaultDisplayVsync; - private final long vsyncDurationNs; - private final long vsyncOffsetNs; + private final DefaultDisplayListener displayListener; + + private long vsyncDurationNs; + private long vsyncOffsetNs; private long lastFramePresentationTimeUs; private long adjustedLastFrameTimeNs; @@ -52,58 +57,65 @@ public final class VideoFrameReleaseTimeHelper { private long frameCount; /** - * Constructs an instance that smoothes frame release timestamps but does not align them with + * Constructs an instance that smooths frame release timestamps but does not align them with * the default display's vsync signal. */ public VideoFrameReleaseTimeHelper() { - this(-1 /* Value unused */, false); + this(null); } /** - * Constructs an instance that smoothes frame release timestamps and aligns them with the default + * Constructs an instance that smooths frame release timestamps and aligns them with the default * display's vsync signal. * * @param context A context from which information about the default display can be retrieved. */ - public VideoFrameReleaseTimeHelper(Context context) { - this(getDefaultDisplayRefreshRate(context), true); - } - - private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate, - boolean useDefaultDisplayVsync) { - this.useDefaultDisplayVsync = useDefaultDisplayVsync; - if (useDefaultDisplayVsync) { - vsyncSampler = VSyncSampler.getInstance(); - vsyncDurationNs = (long) (C.NANOS_PER_SECOND / defaultDisplayRefreshRate); - vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100; + public VideoFrameReleaseTimeHelper(@Nullable Context context) { + if (context != null) { + context = context.getApplicationContext(); + windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); } else { - vsyncSampler = null; - vsyncDurationNs = -1; // Value unused. - vsyncOffsetNs = -1; // Value unused. + windowManager = null; } + if (windowManager != null) { + displayListener = Util.SDK_INT >= 17 ? maybeBuildDefaultDisplayListenerV17(context) : null; + vsyncSampler = VSyncSampler.getInstance(); + } else { + displayListener = null; + vsyncSampler = null; + } + vsyncDurationNs = C.TIME_UNSET; + vsyncOffsetNs = C.TIME_UNSET; } /** - * Enables the helper. + * Enables the helper. Must be called from the playback thread. */ public void enable() { haveSync = false; - if (useDefaultDisplayVsync) { + if (windowManager != null) { vsyncSampler.addObserver(); + if (displayListener != null) { + displayListener.register(); + } + updateDefaultDisplayRefreshRateParams(); } } /** - * Disables the helper. + * Disables the helper. Must be called from the playback thread. */ public void disable() { - if (useDefaultDisplayVsync) { + if (windowManager != null) { + if (displayListener != null) { + displayListener.unregister(); + } vsyncSampler.removeObserver(); } } /** - * Adjusts a frame release timestamp. + * Adjusts a frame release timestamp. Must be called from the playback thread. * * @param framePresentationTimeUs The frame's presentation time, in microseconds. * @param unadjustedReleaseTimeNs The frame's unadjusted release time, in nanoseconds and in @@ -156,25 +168,39 @@ public final class VideoFrameReleaseTimeHelper { syncUnadjustedReleaseTimeNs = unadjustedReleaseTimeNs; frameCount = 0; haveSync = true; - onSynced(); } lastFramePresentationTimeUs = framePresentationTimeUs; pendingAdjustedFrameTimeNs = adjustedFrameTimeNs; - if (vsyncSampler == null || vsyncSampler.sampledVsyncTimeNs == 0) { + if (vsyncSampler == null || vsyncDurationNs == C.TIME_UNSET) { + return adjustedReleaseTimeNs; + } + long sampledVsyncTimeNs = vsyncSampler.sampledVsyncTimeNs; + if (sampledVsyncTimeNs == C.TIME_UNSET) { return adjustedReleaseTimeNs; } // Find the timestamp of the closest vsync. This is the vsync that we're targeting. - long snappedTimeNs = closestVsync(adjustedReleaseTimeNs, - vsyncSampler.sampledVsyncTimeNs, vsyncDurationNs); + long snappedTimeNs = closestVsync(adjustedReleaseTimeNs, sampledVsyncTimeNs, vsyncDurationNs); // Apply an offset so that we release before the target vsync, but after the previous one. return snappedTimeNs - vsyncOffsetNs; } - protected void onSynced() { - // Do nothing. + @TargetApi(17) + private DefaultDisplayListener maybeBuildDefaultDisplayListenerV17(Context context) { + DisplayManager manager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + return manager == null ? null : new DefaultDisplayListener(manager); + } + + private void updateDefaultDisplayRefreshRateParams() { + // Note: If we fail to update the parameters, we leave them set to their previous values. + Display defaultDisplay = windowManager.getDefaultDisplay(); + if (defaultDisplay != null) { + double defaultDisplayRefreshRate = defaultDisplay.getRefreshRate(); + vsyncDurationNs = (long) (C.NANOS_PER_SECOND / defaultDisplayRefreshRate); + vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100; + } } private boolean isDriftTooLarge(long frameTimeNs, long releaseTimeNs) { @@ -200,9 +226,40 @@ public final class VideoFrameReleaseTimeHelper { return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs; } - private static float getDefaultDisplayRefreshRate(Context context) { - WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - return manager.getDefaultDisplay().getRefreshRate(); + @TargetApi(17) + private final class DefaultDisplayListener implements DisplayManager.DisplayListener { + + private final DisplayManager displayManager; + + public DefaultDisplayListener(DisplayManager displayManager) { + this.displayManager = displayManager; + } + + public void register() { + displayManager.registerDisplayListener(this, null); + } + + public void unregister() { + displayManager.unregisterDisplayListener(this); + } + + @Override + public void onDisplayAdded(int displayId) { + // Do nothing. + } + + @Override + public void onDisplayRemoved(int displayId) { + // Do nothing. + } + + @Override + public void onDisplayChanged(int displayId) { + if (displayId == Display.DEFAULT_DISPLAY) { + updateDefaultDisplayRefreshRateParams(); + } + } + } /** @@ -230,9 +287,10 @@ public final class VideoFrameReleaseTimeHelper { } private VSyncSampler() { + sampledVsyncTimeNs = C.TIME_UNSET; choreographerOwnerThread = new HandlerThread("ChoreographerOwner:Handler"); choreographerOwnerThread.start(); - handler = new Handler(choreographerOwnerThread.getLooper(), this); + handler = Util.createHandler(choreographerOwnerThread.getLooper(), /* callback= */ this); handler.sendEmptyMessage(CREATE_CHOREOGRAPHER); } @@ -294,7 +352,7 @@ public final class VideoFrameReleaseTimeHelper { observerCount--; if (observerCount == 0) { choreographer.removeFrameCallback(this); - sampledVsyncTimeNs = 0; + sampledVsyncTimeNs = C.TIME_UNSET; } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoListener.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoListener.java new file mode 100644 index 000000000000..a469366b7800 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoListener.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video; + +/** A listener for metadata corresponding to video being rendered. */ +public interface VideoListener { + + /** + * Called each time there's a change in the size of the video being rendered. + * + * @param width The video width in pixels. + * @param height The video height in pixels. + * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise + * rotation in degrees that the application should apply for the video for it to be rendered + * in the correct orientation. This value will always be zero on API levels 21 and above, + * since the renderer will apply all necessary rotations internally. On earlier API levels + * this is not possible. Applications that use {@link android.view.TextureView} can apply the + * rotation by calling {@link android.view.TextureView#setTransform}. Applications that do not + * expect to encounter rotated videos can safely ignore this parameter. + * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case of + * square pixels this will be equal to 1.0. Different values are indicative of anamorphic + * content. + */ + default void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {} + + /** + * Called each time there's a change in the size of the surface onto which the video is being + * rendered. + * + * @param width The surface width in pixels. May be {@link + * com.google.android.exoplayer2.C#LENGTH_UNSET} if unknown, or 0 if the video is not rendered + * onto a surface. + * @param height The surface height in pixels. May be {@link + * com.google.android.exoplayer2.C#LENGTH_UNSET} if unknown, or 0 if the video is not rendered + * onto a surface. + */ + default void onSurfaceSizeChanged(int width, int height) {} + + /** + * Called when a frame is rendered for the first time since setting the surface, and when a frame + * is rendered for the first time since a video track was selected. + */ + default void onRenderedFirstFrame() {} +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoRendererEventListener.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoRendererEventListener.java index 9ef1b8b253e4..6509a353b206 100644 --- a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoRendererEventListener.java +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoRendererEventListener.java @@ -15,17 +15,21 @@ */ package org.mozilla.thirdparty.com.google.android.exoplayer2.video; +import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Handler; import android.os.SystemClock; import android.view.Surface; import android.view.TextureView; +import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.Renderer; import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderCounters; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; /** - * Listener of video {@link Renderer} events. + * Listener of video {@link Renderer} events. All methods have no-op default implementations to + * allow selective overrides. */ public interface VideoRendererEventListener { @@ -35,7 +39,7 @@ public interface VideoRendererEventListener { * @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it * remains enabled. */ - void onVideoEnabled(DecoderCounters counters); + default void onVideoEnabled(DecoderCounters counters) {} /** * Called when a decoder is created. @@ -45,15 +49,15 @@ public interface VideoRendererEventListener { * finished. * @param initializationDurationMs The time taken to initialize the decoder in milliseconds. */ - void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, - long initializationDurationMs); + default void onVideoDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) {} /** * Called when the format of the media being consumed by the renderer changes. * * @param format The new format. */ - void onVideoInputFormatChanged(Format format); + default void onVideoInputFormatChanged(Format format) {} /** * Called to report the number of frames dropped by the renderer. Dropped frames are reported @@ -61,12 +65,11 @@ public interface VideoRendererEventListener { * reaches a specified threshold whilst the renderer is started. * * @param count The number of dropped frames. - * @param elapsedMs The duration in milliseconds over which the frames were dropped. This - * duration is timed from when the renderer was started or from when dropped frames were - * last reported (whichever was more recent), and not from when the first of the reported - * drops occurred. + * @param elapsedMs The duration in milliseconds over which the frames were dropped. This duration + * is timed from when the renderer was started or from when dropped frames were last reported + * (whichever was more recent), and not from when the first of the reported drops occurred. */ - void onDroppedFrames(int count, long elapsedMs); + default void onDroppedFrames(int count, long elapsedMs) {} /** * Called before a frame is rendered for the first time since setting the surface, and each time @@ -81,12 +84,12 @@ public interface VideoRendererEventListener { * this is not possible. Applications that use {@link TextureView} can apply the rotation by * calling {@link TextureView#setTransform}. Applications that do not expect to encounter * rotated videos can safely ignore this parameter. - * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case - * of square pixels this will be equal to 1.0. Different values are indicative of anamorphic + * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case of + * square pixels this will be equal to 1.0. Different values are indicative of anamorphic * content. */ - void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio); + default void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {} /** * Called when a frame is rendered for the first time since setting the surface, and when a frame @@ -95,133 +98,98 @@ public interface VideoRendererEventListener { * @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if * the renderer renders to something that isn't a {@link Surface}. */ - void onRenderedFirstFrame(Surface surface); + default void onRenderedFirstFrame(@Nullable Surface surface) {} /** * Called when the renderer is disabled. * * @param counters {@link DecoderCounters} that were updated by the renderer. */ - void onVideoDisabled(DecoderCounters counters); + default void onVideoDisabled(DecoderCounters counters) {} /** * Dispatches events to a {@link VideoRendererEventListener}. */ final class EventDispatcher { - private final Handler handler; - private final VideoRendererEventListener listener; + @Nullable private final Handler handler; + @Nullable private final VideoRendererEventListener listener; /** * @param handler A handler for dispatching events, or null if creating a dummy instance. * @param listener The listener to which events should be dispatched, or null if creating a * dummy instance. */ - public EventDispatcher(Handler handler, VideoRendererEventListener listener) { + public EventDispatcher(@Nullable Handler handler, + @Nullable VideoRendererEventListener listener) { this.handler = listener != null ? Assertions.checkNotNull(handler) : null; this.listener = listener; } - /** - * Invokes {@link VideoRendererEventListener#onVideoEnabled(DecoderCounters)}. - */ - public void enabled(final DecoderCounters decoderCounters) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onVideoEnabled(decoderCounters); - } - }); + /** Invokes {@link VideoRendererEventListener#onVideoEnabled(DecoderCounters)}. */ + public void enabled(DecoderCounters decoderCounters) { + if (handler != null) { + handler.post(() -> castNonNull(listener).onVideoEnabled(decoderCounters)); } } - /** - * Invokes {@link VideoRendererEventListener#onVideoDecoderInitialized(String, long, long)}. - */ - public void decoderInitialized(final String decoderName, - final long initializedTimestampMs, final long initializationDurationMs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onVideoDecoderInitialized(decoderName, initializedTimestampMs, - initializationDurationMs); - } - }); + /** Invokes {@link VideoRendererEventListener#onVideoDecoderInitialized(String, long, long)}. */ + public void decoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) { + if (handler != null) { + handler.post( + () -> + castNonNull(listener) + .onVideoDecoderInitialized( + decoderName, initializedTimestampMs, initializationDurationMs)); } } - /** - * Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format)}. - */ - public void inputFormatChanged(final Format format) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onVideoInputFormatChanged(format); - } - }); + /** Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format)}. */ + public void inputFormatChanged(Format format) { + if (handler != null) { + handler.post(() -> castNonNull(listener).onVideoInputFormatChanged(format)); } } - /** - * Invokes {@link VideoRendererEventListener#onDroppedFrames(int, long)}. - */ - public void droppedFrames(final int droppedFrameCount, final long elapsedMs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onDroppedFrames(droppedFrameCount, elapsedMs); - } - }); + /** Invokes {@link VideoRendererEventListener#onDroppedFrames(int, long)}. */ + public void droppedFrames(int droppedFrameCount, long elapsedMs) { + if (handler != null) { + handler.post(() -> castNonNull(listener).onDroppedFrames(droppedFrameCount, elapsedMs)); } } - /** - * Invokes {@link VideoRendererEventListener#onVideoSizeChanged(int, int, int, float)}. - */ - public void videoSizeChanged(final int width, final int height, - final int unappliedRotationDegrees, final float pixelWidthHeightRatio) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, - pixelWidthHeightRatio); - } - }); + /** Invokes {@link VideoRendererEventListener#onVideoSizeChanged(int, int, int, float)}. */ + public void videoSizeChanged( + int width, + int height, + final int unappliedRotationDegrees, + final float pixelWidthHeightRatio) { + if (handler != null) { + handler.post( + () -> + castNonNull(listener) + .onVideoSizeChanged( + width, height, unappliedRotationDegrees, pixelWidthHeightRatio)); } } - /** - * Invokes {@link VideoRendererEventListener#onRenderedFirstFrame(Surface)}. - */ - public void renderedFirstFrame(final Surface surface) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onRenderedFirstFrame(surface); - } - }); + /** Invokes {@link VideoRendererEventListener#onRenderedFirstFrame(Surface)}. */ + public void renderedFirstFrame(@Nullable Surface surface) { + if (handler != null) { + handler.post(() -> castNonNull(listener).onRenderedFirstFrame(surface)); } } - /** - * Invokes {@link VideoRendererEventListener#onVideoDisabled(DecoderCounters)}. - */ - public void disabled(final DecoderCounters counters) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - counters.ensureUpdated(); - listener.onVideoDisabled(counters); - } - }); + /** Invokes {@link VideoRendererEventListener#onVideoDisabled(DecoderCounters)}. */ + public void disabled(DecoderCounters counters) { + counters.ensureUpdated(); + if (handler != null) { + handler.post( + () -> { + counters.ensureUpdated(); + castNonNull(listener).onVideoDisabled(counters); + }); } } diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/package-info.java new file mode 100644 index 000000000000..7053c14d16b2 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.video; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/CameraMotionListener.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/CameraMotionListener.java new file mode 100644 index 000000000000..87bd94c5bcd2 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/CameraMotionListener.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video.spherical; + +/** Listens camera motion. */ +public interface CameraMotionListener { + + /** + * Called when a new camera motion is read. This method is called on the playback thread. + * + * @param timeUs The presentation time of the data. + * @param rotation Angle axis orientation in radians representing the rotation from camera + * coordinate system to world coordinate system. + */ + void onCameraMotion(long timeUs, float[] rotation); + + /** Called when the camera motion track position is reset or the track is disabled. */ + void onCameraMotionReset(); +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java new file mode 100644 index 000000000000..378363aca081 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video.spherical; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.BaseRenderer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Renderer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.nio.ByteBuffer; + +/** A {@link Renderer} that parses the camera motion track. */ +public class CameraMotionRenderer extends BaseRenderer { + + // The amount of time to read samples ahead of the current time. + private static final int SAMPLE_WINDOW_DURATION_US = 100000; + + private final DecoderInputBuffer buffer; + private final ParsableByteArray scratch; + + private long offsetUs; + @Nullable private CameraMotionListener listener; + private long lastTimestampUs; + + public CameraMotionRenderer() { + super(C.TRACK_TYPE_CAMERA_MOTION); + buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + scratch = new ParsableByteArray(); + } + + @Override + @Capabilities + public int supportsFormat(Format format) { + return MimeTypes.APPLICATION_CAMERA_MOTION.equals(format.sampleMimeType) + ? RendererCapabilities.create(FORMAT_HANDLED) + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); + } + + @Override + public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { + if (messageType == C.MSG_SET_CAMERA_MOTION_LISTENER) { + listener = (CameraMotionListener) message; + } else { + super.handleMessage(messageType, message); + } + } + + @Override + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + this.offsetUs = offsetUs; + } + + @Override + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + resetListener(); + } + + @Override + protected void onDisabled() { + resetListener(); + } + + @Override + public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + // Keep reading available samples as long as the sample time is not too far into the future. + while (!hasReadStreamToEnd() && lastTimestampUs < positionUs + SAMPLE_WINDOW_DURATION_US) { + buffer.clear(); + FormatHolder formatHolder = getFormatHolder(); + int result = readSource(formatHolder, buffer, /* formatRequired= */ false); + if (result != C.RESULT_BUFFER_READ || buffer.isEndOfStream()) { + return; + } + + buffer.flip(); + lastTimestampUs = buffer.timeUs; + if (listener != null) { + float[] rotation = parseMetadata(Util.castNonNull(buffer.data)); + if (rotation != null) { + Util.castNonNull(listener).onCameraMotion(lastTimestampUs - offsetUs, rotation); + } + } + } + } + + @Override + public boolean isEnded() { + return hasReadStreamToEnd(); + } + + @Override + public boolean isReady() { + return true; + } + + private @Nullable float[] parseMetadata(ByteBuffer data) { + if (data.remaining() != 16) { + return null; + } + scratch.reset(data.array(), data.limit()); + scratch.setPosition(data.arrayOffset() + 4); // skip reserved bytes too. + float[] result = new float[3]; + for (int i = 0; i < 3; i++) { + result[i] = Float.intBitsToFloat(scratch.readLittleEndianInt()); + } + return result; + } + + private void resetListener() { + lastTimestampUs = 0; + if (listener != null) { + listener.onCameraMotionReset(); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/FrameRotationQueue.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/FrameRotationQueue.java new file mode 100644 index 000000000000..450058fb6acf --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/FrameRotationQueue.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video.spherical; + +import android.opengl.Matrix; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimedValueQueue; + +/** + * This class serves multiple purposes: + * + *

    + *
  • Queues the rotation metadata extracted from camera motion track. + *
  • Converts the metadata to rotation matrices in OpenGl coordinate system. + *
  • Recenters the rotations to componsate the yaw of the initial rotation. + *
+ */ +public final class FrameRotationQueue { + private final float[] recenterMatrix; + private final float[] rotationMatrix; + private final TimedValueQueue rotations; + private boolean recenterMatrixComputed; + + public FrameRotationQueue() { + recenterMatrix = new float[16]; + rotationMatrix = new float[16]; + rotations = new TimedValueQueue<>(); + } + + /** + * Sets a rotation for a given timestamp. + * + * @param timestampUs Timestamp of the rotation. + * @param angleAxis Angle axis orientation in radians representing the rotation from camera + * coordinate system to world coordinate system. + */ + public void setRotation(long timestampUs, float[] angleAxis) { + rotations.add(timestampUs, angleAxis); + } + + /** Removes all of the rotations and forces rotations to be recentered. */ + public void reset() { + rotations.clear(); + recenterMatrixComputed = false; + } + + /** + * Copies the rotation matrix with the greatest timestamp which is less than or equal to the given + * timestamp to {@code matrix}. Removes all older rotations and the returned one from the queue. + * Does nothing if there is no such rotation. + * + * @param matrix The rotation matrix. + * @param timestampUs The time in microseconds to query the rotation. + * @return Whether a rotation matrix is copied to {@code matrix}. + */ + public boolean pollRotationMatrix(float[] matrix, long timestampUs) { + float[] rotation = rotations.pollFloor(timestampUs); + if (rotation == null) { + return false; + } + // TODO [Internal: b/113315546]: Slerp between the floor and ceil rotation. + getRotationMatrixFromAngleAxis(rotationMatrix, rotation); + if (!recenterMatrixComputed) { + computeRecenterMatrix(recenterMatrix, rotationMatrix); + recenterMatrixComputed = true; + } + Matrix.multiplyMM(matrix, 0, recenterMatrix, 0, rotationMatrix, 0); + return true; + } + + /** + * Computes a recentering matrix from the given angle-axis rotation only accounting for yaw. Roll + * and tilt will not be compensated. + * + * @param recenterMatrix The recenter matrix. + * @param rotationMatrix The rotation matrix. + */ + public static void computeRecenterMatrix(float[] recenterMatrix, float[] rotationMatrix) { + // The re-centering matrix is computed as follows: + // recenter.row(2) = temp.col(2).transpose(); + // recenter.row(0) = recenter.row(1).cross(recenter.row(2)).normalized(); + // recenter.row(2) = recenter.row(0).cross(recenter.row(1)).normalized(); + // | temp[10] 0 -temp[8] 0| + // | 0 1 0 0| + // recenter = | temp[8] 0 temp[10] 0| + // | 0 0 0 1| + Matrix.setIdentityM(recenterMatrix, 0); + float normRowSqr = + rotationMatrix[10] * rotationMatrix[10] + rotationMatrix[8] * rotationMatrix[8]; + float normRow = (float) Math.sqrt(normRowSqr); + recenterMatrix[0] = rotationMatrix[10] / normRow; + recenterMatrix[2] = rotationMatrix[8] / normRow; + recenterMatrix[8] = -rotationMatrix[8] / normRow; + recenterMatrix[10] = rotationMatrix[10] / normRow; + } + + private static void getRotationMatrixFromAngleAxis(float[] matrix, float[] angleAxis) { + // Convert coordinates to OpenGL coordinates. + // CAMM motion metadata: +x right, +y down, and +z forward. + // OpenGL: +x right, +y up, -z forwards + float x = angleAxis[0]; + float y = -angleAxis[1]; + float z = -angleAxis[2]; + float angleRad = Matrix.length(x, y, z); + if (angleRad != 0) { + float angleDeg = (float) Math.toDegrees(angleRad); + Matrix.setRotateM(matrix, 0, angleDeg, x / angleRad, y / angleRad, z / angleRad); + } else { + Matrix.setIdentityM(matrix, 0); + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/Projection.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/Projection.java new file mode 100644 index 000000000000..e3d614cab3e4 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/Projection.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video.spherical; + +import androidx.annotation.IntDef; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C.StereoMode; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** The projection mesh used with 360/VR videos. */ +public final class Projection { + + /** Enforces allowed (sub) mesh draw modes. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({DRAW_MODE_TRIANGLES, DRAW_MODE_TRIANGLES_STRIP, DRAW_MODE_TRIANGLES_FAN}) + public @interface DrawMode {} + /** Triangle draw mode. */ + public static final int DRAW_MODE_TRIANGLES = 0; + /** Triangle strip draw mode. */ + public static final int DRAW_MODE_TRIANGLES_STRIP = 1; + /** Triangle fan draw mode. */ + public static final int DRAW_MODE_TRIANGLES_FAN = 2; + + /** Number of position coordinates per vertex. */ + public static final int TEXTURE_COORDS_PER_VERTEX = 2; + /** Number of texture coordinates per vertex. */ + public static final int POSITION_COORDS_PER_VERTEX = 3; + + /** + * Generates a complete sphere equirectangular projection. + * + * @param stereoMode A {@link C.StereoMode} value. + */ + public static Projection createEquirectangular(@C.StereoMode int stereoMode) { + return createEquirectangular( + /* radius= */ 50, // Should be large enough that there are no stereo artifacts. + /* latitudes= */ 36, // Should be large enough to prevent videos looking wavy. + /* longitudes= */ 72, // Should be large enough to prevent videos looking wavy. + /* verticalFovDegrees= */ 180, + /* horizontalFovDegrees= */ 360, + stereoMode); + } + + /** + * Generates an equirectangular projection. + * + * @param radius Size of the sphere. Must be > 0. + * @param latitudes Number of rows that make up the sphere. Must be >= 1. + * @param longitudes Number of columns that make up the sphere. Must be >= 1. + * @param verticalFovDegrees Total latitudinal degrees that are covered by the sphere. Must be in + * (0, 180]. + * @param horizontalFovDegrees Total longitudinal degrees that are covered by the sphere.Must be + * in (0, 360]. + * @param stereoMode A {@link C.StereoMode} value. + * @return an equirectangular projection. + */ + public static Projection createEquirectangular( + float radius, + int latitudes, + int longitudes, + float verticalFovDegrees, + float horizontalFovDegrees, + @C.StereoMode int stereoMode) { + Assertions.checkArgument(radius > 0); + Assertions.checkArgument(latitudes >= 1); + Assertions.checkArgument(longitudes >= 1); + Assertions.checkArgument(verticalFovDegrees > 0 && verticalFovDegrees <= 180); + Assertions.checkArgument(horizontalFovDegrees > 0 && horizontalFovDegrees <= 360); + + // Compute angular size in radians of each UV quad. + float verticalFovRads = (float) Math.toRadians(verticalFovDegrees); + float horizontalFovRads = (float) Math.toRadians(horizontalFovDegrees); + float quadHeightRads = verticalFovRads / latitudes; + float quadWidthRads = horizontalFovRads / longitudes; + + // Each latitude strip has 2 * (longitudes quads + extra edge) vertices + 2 degenerate vertices. + int vertexCount = (2 * (longitudes + 1) + 2) * latitudes; + // Buffer to return. + float[] vertexData = new float[vertexCount * POSITION_COORDS_PER_VERTEX]; + float[] textureData = new float[vertexCount * TEXTURE_COORDS_PER_VERTEX]; + + // Generate the data for the sphere which is a set of triangle strips representing each + // latitude band. + int vOffset = 0; // Offset into the vertexData array. + int tOffset = 0; // Offset into the textureData array. + // (i, j) represents a quad in the equirectangular sphere. + for (int j = 0; j < latitudes; ++j) { // For each horizontal triangle strip. + // Each latitude band lies between the two phi values. Each vertical edge on a band lies on + // a theta value. + float phiLow = quadHeightRads * j - verticalFovRads / 2; + float phiHigh = quadHeightRads * (j + 1) - verticalFovRads / 2; + + for (int i = 0; i < longitudes + 1; ++i) { // For each vertical edge in the band. + for (int k = 0; k < 2; ++k) { // For low and high points on an edge. + // For each point, determine it's position in polar coordinates. + float phi = k == 0 ? phiLow : phiHigh; + float theta = quadWidthRads * i + (float) Math.PI - horizontalFovRads / 2; + + // Set vertex position data as Cartesian coordinates. + vertexData[vOffset++] = -(float) (radius * Math.sin(theta) * Math.cos(phi)); + vertexData[vOffset++] = (float) (radius * Math.sin(phi)); + vertexData[vOffset++] = (float) (radius * Math.cos(theta) * Math.cos(phi)); + + textureData[tOffset++] = i * quadWidthRads / horizontalFovRads; + textureData[tOffset++] = (j + k) * quadHeightRads / verticalFovRads; + + // Break up the triangle strip with degenerate vertices by copying first and last points. + if ((i == 0 && k == 0) || (i == longitudes && k == 1)) { + System.arraycopy( + vertexData, + vOffset - POSITION_COORDS_PER_VERTEX, + vertexData, + vOffset, + POSITION_COORDS_PER_VERTEX); + vOffset += POSITION_COORDS_PER_VERTEX; + System.arraycopy( + textureData, + tOffset - TEXTURE_COORDS_PER_VERTEX, + textureData, + tOffset, + TEXTURE_COORDS_PER_VERTEX); + tOffset += TEXTURE_COORDS_PER_VERTEX; + } + } + // Move on to the next vertical edge in the triangle strip. + } + // Move on to the next triangle strip. + } + SubMesh subMesh = + new SubMesh(SubMesh.VIDEO_TEXTURE_ID, vertexData, textureData, DRAW_MODE_TRIANGLES_STRIP); + return new Projection(new Mesh(subMesh), stereoMode); + } + + /** The Mesh corresponding to the left eye. */ + public final Mesh leftMesh; + /** + * The Mesh corresponding to the right eye. If {@code singleMesh} is true then this mesh is + * identical to {@link #leftMesh}. + */ + public final Mesh rightMesh; + /** The stereo mode. */ + public final @StereoMode int stereoMode; + /** Whether the left and right mesh are identical. */ + public final boolean singleMesh; + + /** + * Creates a Projection with single mesh. + * + * @param mesh the Mesh for both eyes. + * @param stereoMode A {@link StereoMode} value. + */ + public Projection(Mesh mesh, int stereoMode) { + this(mesh, mesh, stereoMode); + } + + /** + * Creates a Projection with dual mesh. Use {@link #Projection(Mesh, int)} if there is single mesh + * for both eyes. + * + * @param leftMesh the Mesh corresponding to the left eye. + * @param rightMesh the Mesh corresponding to the right eye. + * @param stereoMode A {@link C.StereoMode} value. + */ + public Projection(Mesh leftMesh, Mesh rightMesh, int stereoMode) { + this.leftMesh = leftMesh; + this.rightMesh = rightMesh; + this.stereoMode = stereoMode; + this.singleMesh = leftMesh == rightMesh; + } + + /** The sub mesh associated with the {@link Mesh}. */ + public static final class SubMesh { + /** Texture ID for video frames. */ + public static final int VIDEO_TEXTURE_ID = 0; + + /** Texture ID. */ + public final int textureId; + /** The drawing mode. One of {@link DrawMode}. */ + public final @DrawMode int mode; + /** The SubMesh vertices. */ + public final float[] vertices; + /** The SubMesh texture coordinates. */ + public final float[] textureCoords; + + public SubMesh(int textureId, float[] vertices, float[] textureCoords, @DrawMode int mode) { + this.textureId = textureId; + Assertions.checkArgument( + vertices.length * (long) TEXTURE_COORDS_PER_VERTEX + == textureCoords.length * (long) POSITION_COORDS_PER_VERTEX); + this.vertices = vertices; + this.textureCoords = textureCoords; + this.mode = mode; + } + + /** Returns the SubMesh vertex count. */ + public int getVertexCount() { + return vertices.length / POSITION_COORDS_PER_VERTEX; + } + } + + /** A Mesh associated with the projection scene. */ + public static final class Mesh { + private final SubMesh[] subMeshes; + + public Mesh(SubMesh... subMeshes) { + this.subMeshes = subMeshes; + } + + /** Returns the number of sub meshes. */ + public int getSubMeshCount() { + return subMeshes.length; + } + + /** Returns the SubMesh for the given index. */ + public SubMesh getSubMesh(int index) { + return subMeshes[index]; + } + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java new file mode 100644 index 000000000000..cff4b2845d36 --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video.spherical; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.spherical.Projection.Mesh; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.spherical.Projection.SubMesh; +import java.util.ArrayList; +import java.util.zip.Inflater; + +/** + * A decoder for the projection mesh. + * + *

The mesh boxes parsed are described at + * Spherical Video V2 RFC. + * + *

The decoder does not perform CRC checks at the moment. + */ +public final class ProjectionDecoder { + + private static final int TYPE_YTMP = 0x79746d70; + private static final int TYPE_MSHP = 0x6d736870; + private static final int TYPE_RAW = 0x72617720; + private static final int TYPE_DFL8 = 0x64666c38; + private static final int TYPE_MESH = 0x6d657368; + private static final int TYPE_PROJ = 0x70726f6a; + + // Sanity limits to prevent a bad file from creating an OOM situation. We don't expect a mesh to + // exceed these limits. + private static final int MAX_COORDINATE_COUNT = 10000; + private static final int MAX_VERTEX_COUNT = 32 * 1000; + private static final int MAX_TRIANGLE_INDICES = 128 * 1000; + + private ProjectionDecoder() {} + + /* + * Decodes the projection data. + * + * @param projectionData The projection data. + * @param stereoMode A {@link C.StereoMode} value. + * @return The projection or null if the data can't be decoded. + */ + public static @Nullable Projection decode(byte[] projectionData, @C.StereoMode int stereoMode) { + ParsableByteArray input = new ParsableByteArray(projectionData); + // MP4 containers include the proj box but webm containers do not. + // Both containers use mshp. + ArrayList meshes = null; + try { + meshes = isProj(input) ? parseProj(input) : parseMshp(input); + } catch (ArrayIndexOutOfBoundsException ignored) { + // Do nothing. + } + if (meshes == null) { + return null; + } else { + switch (meshes.size()) { + case 1: + return new Projection(meshes.get(0), stereoMode); + case 2: + return new Projection(meshes.get(0), meshes.get(1), stereoMode); + case 0: + default: + return null; + } + } + } + + /** Returns true if the input contains a proj box. Indicates MP4 container. */ + private static boolean isProj(ParsableByteArray input) { + input.skipBytes(4); // size + int type = input.readInt(); + input.setPosition(0); + return type == TYPE_PROJ; + } + + private static @Nullable ArrayList parseProj(ParsableByteArray input) { + input.skipBytes(8); // size and type. + int position = input.getPosition(); + int limit = input.limit(); + while (position < limit) { + int childEnd = position + input.readInt(); + if (childEnd <= position || childEnd > limit) { + return null; + } + int childAtomType = input.readInt(); + // Some early files named the atom ytmp rather than mshp. + if (childAtomType == TYPE_YTMP || childAtomType == TYPE_MSHP) { + input.setLimit(childEnd); + return parseMshp(input); + } + position = childEnd; + input.setPosition(position); + } + return null; + } + + private static @Nullable ArrayList parseMshp(ParsableByteArray input) { + int version = input.readUnsignedByte(); + if (version != 0) { + return null; + } + input.skipBytes(7); // flags + crc. + int encoding = input.readInt(); + if (encoding == TYPE_DFL8) { + ParsableByteArray output = new ParsableByteArray(); + Inflater inflater = new Inflater(true); + try { + if (!Util.inflate(input, output, inflater)) { + return null; + } + } finally { + inflater.end(); + } + input = output; + } else if (encoding != TYPE_RAW) { + return null; + } + return parseRawMshpData(input); + } + + /** Parses MSHP data after the encoding_four_cc field. */ + private static @Nullable ArrayList parseRawMshpData(ParsableByteArray input) { + ArrayList meshes = new ArrayList<>(); + int position = input.getPosition(); + int limit = input.limit(); + while (position < limit) { + int childEnd = position + input.readInt(); + if (childEnd <= position || childEnd > limit) { + return null; + } + int childAtomType = input.readInt(); + if (childAtomType == TYPE_MESH) { + Mesh mesh = parseMesh(input); + if (mesh == null) { + return null; + } + meshes.add(mesh); + } + position = childEnd; + input.setPosition(position); + } + return meshes; + } + + private static @Nullable Mesh parseMesh(ParsableByteArray input) { + // Read the coordinates. + int coordinateCount = input.readInt(); + if (coordinateCount > MAX_COORDINATE_COUNT) { + return null; + } + float[] coordinates = new float[coordinateCount]; + for (int coordinate = 0; coordinate < coordinateCount; coordinate++) { + coordinates[coordinate] = input.readFloat(); + } + // Read the vertices. + int vertexCount = input.readInt(); + if (vertexCount > MAX_VERTEX_COUNT) { + return null; + } + + final double log2 = Math.log(2.0); + int coordinateCountSizeBits = (int) Math.ceil(Math.log(2.0 * coordinateCount) / log2); + + ParsableBitArray bitInput = new ParsableBitArray(input.data); + bitInput.setPosition(input.getPosition() * 8); + float[] vertices = new float[vertexCount * 5]; + int[] coordinateIndices = new int[5]; + int vertexIndex = 0; + for (int vertex = 0; vertex < vertexCount; vertex++) { + for (int i = 0; i < 5; i++) { + int coordinateIndex = + coordinateIndices[i] + decodeZigZag(bitInput.readBits(coordinateCountSizeBits)); + if (coordinateIndex >= coordinateCount || coordinateIndex < 0) { + return null; + } + vertices[vertexIndex++] = coordinates[coordinateIndex]; + coordinateIndices[i] = coordinateIndex; + } + } + + // Pad to next byte boundary + bitInput.setPosition(((bitInput.getPosition() + 7) & ~7)); + + int subMeshCount = bitInput.readBits(32); + SubMesh[] subMeshes = new SubMesh[subMeshCount]; + for (int i = 0; i < subMeshCount; i++) { + int textureId = bitInput.readBits(8); + int drawMode = bitInput.readBits(8); + int triangleIndexCount = bitInput.readBits(32); + if (triangleIndexCount > MAX_TRIANGLE_INDICES) { + return null; + } + int vertexCountSizeBits = (int) Math.ceil(Math.log(2.0 * vertexCount) / log2); + int index = 0; + float[] triangleVertices = new float[triangleIndexCount * 3]; + float[] textureCoords = new float[triangleIndexCount * 2]; + for (int counter = 0; counter < triangleIndexCount; counter++) { + index += decodeZigZag(bitInput.readBits(vertexCountSizeBits)); + if (index < 0 || index >= vertexCount) { + return null; + } + triangleVertices[counter * 3] = vertices[index * 5]; + triangleVertices[counter * 3 + 1] = vertices[index * 5 + 1]; + triangleVertices[counter * 3 + 2] = vertices[index * 5 + 2]; + textureCoords[counter * 2] = vertices[index * 5 + 3]; + textureCoords[counter * 2 + 1] = vertices[index * 5 + 4]; + } + subMeshes[i] = new SubMesh(textureId, triangleVertices, textureCoords, drawMode); + } + return new Mesh(subMeshes); + } + + /** + * Decodes Zigzag encoding as described in + * https://developers.google.com/protocol-buffers/docs/encoding#signed-integers + */ + private static int decodeZigZag(int n) { + return (n >> 1) ^ -(n & 1); + } +} diff --git a/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/package-info.java b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/package-info.java new file mode 100644 index 000000000000..7ab7fced0bdf --- /dev/null +++ b/mobile/android/geckoview/src/thirdparty/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.video.spherical; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;