summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /mobile/android/exoplayer2
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/exoplayer2')
-rw-r--r--mobile/android/exoplayer2/build.gradle110
-rw-r--r--mobile/android/exoplayer2/src/main/AndroidManifest.xml6
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioBecomingNoisyManager.java81
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioFocusManager.java397
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/BasePlayer.java209
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/BaseRenderer.java436
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/C.java1160
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ControlDispatcher.java75
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultControlDispatcher.java55
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultLoadControl.java473
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultMediaClock.java197
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultRenderersFactory.java580
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlaybackException.java233
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayer.java404
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerFactory.java350
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImpl.java848
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java2045
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java86
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Format.java1750
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/FormatHolder.java43
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/IllegalSeekPositionException.java48
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/LoadControl.java113
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodHolder.java432
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodInfo.java141
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodQueue.java743
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/NoSampleRenderer.java306
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ParserException.java51
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackInfo.java358
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackParameters.java113
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackPreparer.java23
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Player.java1040
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlayerMessage.java301
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Renderer.java306
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RendererCapabilities.java293
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RendererConfiguration.java62
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RenderersFactory.java50
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SeekParameters.java91
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SimpleExoPlayer.java1845
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Timeline.java837
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/WakeLockManager.java101
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/WifiLockManager.java94
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsCollector.java881
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsListener.java514
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java23
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java355
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java120
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStats.java980
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java1059
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Ac3Util.java584
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Ac4Util.java250
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioAttributes.java162
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilities.java161
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java166
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioDecoderException.java35
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioListener.java41
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioProcessor.java148
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioRendererEventListener.java174
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioSink.java329
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTimestampPoller.java309
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java545
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AuxEffectInfo.java85
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/BaseAudioProcessor.java143
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java99
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DefaultAudioSink.java1474
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DtsUtil.java217
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java109
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ForwardingAudioSink.java151
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java1036
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java134
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java352
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java758
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Sonic.java506
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SonicAudioProcessor.java277
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/TeeAudioProcessor.java235
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java178
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/WavUtil.java91
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DatabaseIOException.java31
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DatabaseProvider.java56
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DefaultDatabaseProvider.java42
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/ExoDatabaseProvider.java95
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/VersionTable.java170
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/Buffer.java100
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/CryptoInfo.java146
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/Decoder.java73
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/DecoderCounters.java105
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java209
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/OutputBuffer.java38
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/SimpleDecoder.java314
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java65
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ClearKeyUtil.java97
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DecryptionException.java37
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSession.java607
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java51
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java691
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmInitData.java425
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSession.java144
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSessionManager.java121
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java146
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java74
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaCrypto.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaDrm.java342
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java59
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java440
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java195
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/KeysExpiredException.java22
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java51
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/MediaDrmCallback.java46
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java266
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/UnsupportedDrmException.java67
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/WidevineUtil.java66
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java538
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.java121
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java123
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java308
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java269
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java35
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java62
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Extractor.java125
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.java280
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.java48
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorUtil.java52
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.java23
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacFrameReader.java336
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java312
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java84
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java131
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Id3Peeker.java87
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.java275
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/PositionHolder.java28
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.java141
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekPoint.java64
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java147
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisBitArray.java129
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisUtil.java522
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java383
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java131
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java411
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java130
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java308
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java227
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java87
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java141
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java260
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java150
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java57
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java2331
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.java114
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/VarintReader.java155
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java46
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java125
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java482
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Seeker.java60
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java136
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java188
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java558
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java1607
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java32
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java114
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java1660
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java115
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java588
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java824
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java208
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java201
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java148
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java103
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java197
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java108
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java313
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java143
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java114
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java155
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java135
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java57
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java132
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java268
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java198
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java170
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java149
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java209
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java156
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java216
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java332
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java532
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java283
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java181
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java130
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java63
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java333
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java567
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java494
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.java116
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/LatmReader.java310
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java223
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java109
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.java241
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java209
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java259
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java397
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java49
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java134
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.java73
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java62
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java140
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java197
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java698
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java232
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsUtil.java96
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/UserDataReader.java81
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.java562
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.java55
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java191
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java74
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java617
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java2014
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java71
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java1232
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java109
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/Metadata.java171
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoder.java33
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java94
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java36
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataOutput.java30
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataRenderer.java236
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessage.java202
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java47
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java73
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/PictureFrame.java144
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/VorbisComment.java99
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java101
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java243
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyInfo.java107
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.java108
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java83
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java145
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java127
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.java101
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.java112
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java842
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java44
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/InternalFrame.java97
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/MlltFrame.java114
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.java94
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java96
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java96
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java85
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java37
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java102
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java254
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java47
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java270
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java93
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ActionFile.java164
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java120
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java452
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java119
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/Download.java164
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadCursor.java129
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadException.java33
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadHelper.java1174
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadIndex.java49
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadManager.java1346
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadProgress.java28
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadRequest.java212
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadService.java1049
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/Downloader.java60
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java170
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloaderFactory.java28
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/FilterableManifest.java36
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/FilteringManifestParser.java49
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ProgressiveDownloader.java120
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/SegmentDownloader.java279
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/StreamKey.java132
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/WritableDownloadIndex.java87
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/PlatformScheduler.java150
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Requirements.java223
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java197
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Scheduler.java48
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java327
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java24
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BaseMediaSource.java191
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BehindLiveWindowException.java29
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaPeriod.java345
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaSource.java375
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeMediaSource.java354
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java95
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java31
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java1017
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java29
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java23
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/EmptySampleStream.java50
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaSource.java394
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ForwardingTimeline.java83
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/IcyDataSource.java149
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/LoopingMediaSource.java214
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaPeriod.java236
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaSource.java353
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaPeriod.java251
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSource.java325
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceEventListener.java740
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceFactory.java62
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaPeriod.java256
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaSource.java184
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java1162
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaSource.java327
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleDataQueue.java472
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleQueue.java926
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleStream.java79
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SequenceableLoader.java77
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ShuffleOrder.java283
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SilenceMediaSource.java253
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SinglePeriodTimeline.java227
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java423
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaSource.java371
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroup.java142
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroupArray.java141
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java40
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdPlaybackState.java486
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsLoader.java150
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsMediaSource.java439
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java66
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java100
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java75
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java80
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.java137
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java220
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkHolder.java41
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java791
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.java111
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java157
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java119
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java112
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.java68
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java104
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java61
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java120
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/Aes128DataSource.java129
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java39
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java338
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/FullSegmentEncryptionKeyCache.java85
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java668
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java35
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java92
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsManifest.java44
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java519
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java858
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaSource.java528
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStream.java97
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java1535
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java245
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java30
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java57
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/WebvttExtractor.java195
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java148
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java33
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java678
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java55
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java330
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java375
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java50
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java1007
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParserFactory.java38
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java226
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/CaptionStyleCompat.java184
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/Cue.java435
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java100
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java38
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/Subtitle.java59
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoder.java35
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderException.java43
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java126
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleInputBuffer.java34
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java77
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/TextOutput.java31
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/TextRenderer.java350
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java1014
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Cue.java62
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Decoder.java1255
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java54
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaDecoder.java204
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaSubtitle.java60
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaUtil.java138
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbDecoder.java49
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbParser.java1059
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbSubtitle.java54
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/PgsDecoder.java259
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java51
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaDecoder.java446
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java83
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaStyle.java301
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java71
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/SubripDecoder.java259
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java72
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java756
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlNode.java399
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRegion.java69
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java151
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlStyle.java268
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java81
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java241
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java63
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/CssParser.java347
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java101
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java56
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java329
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCue.java319
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java550
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java125
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java119
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java115
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/package-info.java20
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java761
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java217
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java494
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java2827
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java117
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java541
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java143
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelection.java269
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java77
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java336
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java100
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelector.java157
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java105
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocation.java46
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocator.java63
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/AssetDataSource.java150
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BandwidthMeter.java71
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BaseDataSource.java102
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java63
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java91
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ContentDataSource.java173
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java110
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSink.java67
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSource.java111
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceException.java41
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceInputStream.java107
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSpec.java478
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultAllocator.java179
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java731
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSource.java289
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java85
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java798
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java119
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java110
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DummyDataSource.java59
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSource.java171
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java38
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/HttpDataSource.java379
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java87
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Loader.java521
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java63
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ParsingLoadable.java177
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSource.java89
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java49
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/RawResourceDataSource.java199
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ResolvingDataSource.java132
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/StatsDataSource.java113
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TeeDataSource.java105
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TransferListener.java77
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/UdpDataSource.java176
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/Cache.java286
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java210
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java45
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java580
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java112
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java47
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java28
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java252
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java29
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheSpan.java106
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheUtil.java434
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContent.java208
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java956
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java204
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java87
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java145
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java173
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java89
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java57
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCache.java812
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java217
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java99
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java89
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java123
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java46
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Assertions.java217
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/AtomicFile.java201
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Clock.java49
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java384
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ColorParser.java277
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ConditionVariable.java83
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EGLSurfaceTexture.java312
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ErrorMessageProvider.java31
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventDispatcher.java100
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventLogger.java651
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacConstants.java42
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacStreamMetadata.java384
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/GlUtil.java404
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/HandlerWrapper.java61
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LibraryLoader.java68
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Log.java177
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LongArray.java84
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MediaClock.java43
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MimeTypes.java465
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NalUnitUtil.java519
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NonNullApi.java34
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NotificationUtil.java134
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableBitArray.java323
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableByteArray.java586
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java211
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Predicate.java33
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/PriorityTaskManager.java119
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/RepeatModeUtil.java95
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java73
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SlidingPercentile.java158
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/StandaloneMediaClock.java104
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemClock.java47
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemHandlerWrapper.java85
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimedValueQueue.java161
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimestampAdjuster.java186
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TraceUtil.java62
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/UriUtil.java279
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Util.java2298
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/XmlPullParserUtil.java131
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/package-info.java17
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/AvcConfig.java97
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/ColorInfo.java150
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/DolbyVisionConfig.java64
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/DummySurface.java228
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/HevcConfig.java91
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java1873
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java975
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderException.java40
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java57
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java30
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java185
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderOutputBufferRenderer.java27
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderRenderer.java241
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java40
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java361
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoListener.java58
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoRendererEventListener.java198
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/CameraMotionListener.java32
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java134
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/FrameRotationQueue.java124
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/Projection.java236
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java238
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/package-info.java19
577 files changed, 144422 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/build.gradle b/mobile/android/exoplayer2/build.gradle
new file mode 100644
index 0000000000..d67995650f
--- /dev/null
+++ b/mobile/android/exoplayer2/build.gradle
@@ -0,0 +1,110 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/exoplayer2"
+
+apply plugin: 'com.android.library'
+
+dependencies {
+ // For exoplayer.
+ 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.7.10"
+
+ androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+
+ implementation "androidx.annotation:annotation:1.1.0"
+}
+
+android {
+ buildToolsVersion project.ext.buildToolsVersion
+ compileSdkVersion project.ext.compileSdkVersion
+
+ defaultConfig {
+ targetSdkVersion project.ext.targetSdkVersion
+ minSdkVersion project.ext.minSdkVersion
+
+ versionCode project.ext.versionCode
+ versionName project.ext.versionName
+ }
+
+ sourceSets {
+ main {
+ java {
+ srcDir "${topsrcdir}/mobile/android/exoplayer2/src/main/java"
+ }
+ }
+ }
+
+ namespace 'org.mozilla.geckoview.thirdparty'
+}
+
+apply plugin: 'maven-publish'
+
+version = getVersionNumber()
+group = 'org.mozilla.geckoview'
+
+android.libraryVariants.all { variant ->
+ def javadoc = task "javadoc${name.capitalize()}"(type: Javadoc) {
+ }
+ task("javadocJar${name.capitalize()}", type: Jar, dependsOn: javadoc) {
+ archiveClassifier = 'javadoc'
+ destinationDirectory = javadoc.destinationDir
+ }
+ task("sourcesJar${name.capitalize()}", type: Jar) {
+ archiveClassifier = 'sources'
+ description = "Generate Javadoc for build variant $name"
+ destinationDirectory =
+ file("${topobjdir}/mobile/android/geckoview-exoplayer2/sources/${variant.baseName}")
+ from files(variant.sourceSets.collect({ it.java.srcDirs }).flatten())
+ }
+}
+
+publishing {
+ publications {
+ android.libraryVariants.all { variant ->
+ "${variant.name}"(MavenPublication) {
+ from components.findByName(variant.name)
+
+ pom {
+ afterEvaluate {
+ artifactId = "geckoview-exoplayer2" + project.ext.artifactSuffix
+ }
+
+ url = 'https://geckoview.dev'
+
+ licenses {
+ license {
+ name = 'The Mozilla Public License, v. 2.0'
+ url = 'http://mozilla.org/MPL/2.0/'
+ distribution = 'repo'
+ }
+ }
+
+ scm {
+ if (mozconfig.substs.MOZ_INCLUDE_SOURCE_INFO) {
+ // URL is like "https://hg.mozilla.org/mozilla-central/rev/1e64b8a0c546a49459d404aaf930d5b1f621246a".
+ connection = "scm::hg::${mozconfig.substs.MOZ_SOURCE_REPO}"
+ url = mozconfig.substs.MOZ_SOURCE_URL
+ tag = mozconfig.substs.MOZ_SOURCE_CHANGESET
+ } else {
+ // Default to mozilla-central.
+ connection = 'scm::hg::https://hg.mozilla.org/mozilla-central/'
+ url = 'https://hg.mozilla.org/mozilla-central/'
+ }
+ }
+ }
+
+ // Javadoc and sources for developer ergononomics.
+ artifact tasks["javadocJar${variant.name.capitalize()}"]
+ artifact tasks["sourcesJar${variant.name.capitalize()}"]
+ }
+ }
+ }
+ repositories {
+ maven {
+ url = "${topobjdir}/gradle/maven"
+ }
+ }
+}
+
+sourceCompatibility = JavaVersion.VERSION_1_8
+targetCompatibility = JavaVersion.VERSION_1_8
diff --git a/mobile/android/exoplayer2/src/main/AndroidManifest.xml b/mobile/android/exoplayer2/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..2593e9fcbe
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<manifest />
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioBecomingNoisyManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioBecomingNoisyManager.java
new file mode 100644
index 0000000000..c833c448e4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioFocusManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioFocusManager.java
new file mode 100644
index 0000000000..5806f57a08
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/BasePlayer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/BasePlayer.java
new file mode 100644
index 0000000000..c06361e69b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/BaseRenderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/BaseRenderer.java
new file mode 100644
index 0000000000..9c2c244053
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/BaseRenderer.java
@@ -0,0 +1,436 @@
+/*
+ * 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 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;
+
+/**
+ * An abstract base class suitable for most {@link Renderer} implementations.
+ */
+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 long readingPositionUs;
+ private boolean streamIsFinal;
+ private boolean throwRendererExceptionIsExecuting;
+
+ /**
+ * @param trackType The track type that the renderer handles. One of the {@link C}
+ * {@code TRACK_TYPE_*} constants.
+ */
+ public BaseRenderer(int trackType) {
+ this.trackType = trackType;
+ formatHolder = new FormatHolder();
+ readingPositionUs = C.TIME_END_OF_SOURCE;
+ }
+
+ @Override
+ public final int getTrackType() {
+ return trackType;
+ }
+
+ @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;
+ }
+
+ @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();
+ }
+
+ @Override
+ public final void replaceStream(Format[] formats, SampleStream stream, long offsetUs)
+ throws ExoPlaybackException {
+ Assertions.checkState(!streamIsFinal);
+ this.stream = stream;
+ readingPositionUs = offsetUs;
+ streamFormats = formats;
+ streamOffsetUs = offsetUs;
+ onStreamChanged(formats, offsetUs);
+ }
+
+ @Override
+ @Nullable
+ public final SampleStream getStream() {
+ return stream;
+ }
+
+ @Override
+ public final boolean hasReadStreamToEnd() {
+ return readingPositionUs == C.TIME_END_OF_SOURCE;
+ }
+
+ @Override
+ public final long getReadingPositionUs() {
+ return readingPositionUs;
+ }
+
+ @Override
+ public final void setCurrentStreamFinal() {
+ streamIsFinal = true;
+ }
+
+ @Override
+ public final boolean isCurrentStreamFinal() {
+ return streamIsFinal;
+ }
+
+ @Override
+ public final void maybeThrowStreamError() throws IOException {
+ stream.maybeThrowError();
+ }
+
+ @Override
+ public final void resetPosition(long positionUs) throws ExoPlaybackException {
+ streamIsFinal = false;
+ readingPositionUs = positionUs;
+ 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);
+ formatHolder.clear();
+ state = STATE_DISABLED;
+ 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;
+ }
+
+ // 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.
+ * <p>
+ * 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 stream has changed. This occurs when the renderer is enabled after
+ * {@link #onEnabled(boolean)} has been called, and also when the stream has been replaced whilst
+ * the renderer is enabled or started.
+ * <p>
+ * 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, long offsetUs) throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ /**
+ * Called when the position is reset. This occurs when the renderer is enabled after
+ * {@link #onStreamChanged(Format[], long)} has been called, and also when a position
+ * discontinuity is encountered.
+ * <p>
+ * After a position reset, the renderer's {@link SampleStream} is guaranteed to provide samples
+ * starting from a key frame.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * The default implementation is a no-op.
+ */
+ protected void onDisabled() {
+ // Do nothing.
+ }
+
+ /**
+ * Called when the renderer is reset.
+ *
+ * <p>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.
+ */
+ protected final RendererConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ /** Returns a {@link DrmSession} ready for assignment, handling resource management. */
+ @Nullable
+ protected final <T extends ExoMediaCrypto> DrmSession<T> getUpdatedSourceDrmSession(
+ @Nullable Format oldFormat,
+ Format newFormat,
+ @Nullable DrmSessionManager<T> drmSessionManager,
+ @Nullable DrmSession<T> existingSourceSession)
+ throws ExoPlaybackException {
+ boolean drmInitDataChanged =
+ !Util.areEqual(newFormat.drmInitData, oldFormat == null ? null : oldFormat.drmInitData);
+ if (!drmInitDataChanged) {
+ return existingSourceSession;
+ }
+ @Nullable DrmSession<T> 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.
+ */
+ protected final int getIndex() {
+ 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
+ * called. {@link C#RESULT_NOTHING_READ} is returned otherwise.
+ *
+ * @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.
+ * @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) {
+ int result = stream.readData(formatHolder, buffer, formatRequired);
+ if (result == C.RESULT_BUFFER_READ) {
+ if (buffer.isEndOfStream()) {
+ 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) {
+ format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + streamOffsetUs);
+ formatHolder.format = format;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Attempts to skip to the keyframe before the specified position, or to the end of the stream if
+ * {@code positionUs} is beyond it.
+ *
+ * @param positionUs The position in microseconds.
+ * @return The number of samples that were skipped.
+ */
+ protected int skipSource(long positionUs) {
+ return stream.skipData(positionUs - streamOffsetUs);
+ }
+
+ /**
+ * Returns whether the upstream source is ready.
+ */
+ protected final boolean isSourceReady() {
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/C.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/C.java
new file mode 100644
index 0000000000..673c3d90a8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/C.java
@@ -0,0 +1,1160 @@
+/*
+ * 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 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.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;
+
+/**
+ * Defines constants used by the library.
+ */
+@SuppressWarnings("InlinedApi")
+public final class C {
+
+ private C() {}
+
+ /**
+ * Special constant representing a time corresponding to the end of a source. Suitable for use in
+ * any time base.
+ */
+ public static final long TIME_END_OF_SOURCE = Long.MIN_VALUE;
+
+ /**
+ * Special constant representing an unset or unknown time or duration. Suitable for use in any
+ * time base.
+ */
+ public static final long TIME_UNSET = Long.MIN_VALUE + 1;
+
+ /**
+ * Represents an unset or unknown index.
+ */
+ public static final int INDEX_UNSET = -1;
+
+ /**
+ * Represents an unset or unknown position.
+ */
+ public static final int POSITION_UNSET = -1;
+
+ /**
+ * Represents an unset or unknown length.
+ */
+ public static final int LENGTH_UNSET = -1;
+
+ /** 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;
+
+ /**
+ * The number of nanoseconds in one second.
+ */
+ 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 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.
+ */
+ public static final String SERIF_NAME = "serif";
+
+ /**
+ * The name of the sans-serif font family.
+ */
+ public static final String SANS_SERIF_NAME = "sans-serif";
+
+ /**
+ * 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
+ */
+ public static final int CRYPTO_MODE_UNENCRYPTED = MediaCodec.CRYPTO_MODE_UNENCRYPTED;
+ /**
+ * @see MediaCodec#CRYPTO_MODE_AES_CTR
+ */
+ public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR;
+ /**
+ * @see MediaCodec#CRYPTO_MODE_AES_CBC
+ */
+ 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}.
+ */
+ public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE;
+
+ /**
+ * 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_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. 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_16BIT_BIG_ENDIAN,
+ ENCODING_PCM_24BIT,
+ ENCODING_PCM_32BIT,
+ ENCODING_PCM_FLOAT
+ })
+ public @interface PcmEncoding {}
+ /** @see AudioFormat#ENCODING_INVALID */
+ public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID;
+ /** @see AudioFormat#ENCODING_PCM_8BIT */
+ public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT;
+ /** @see AudioFormat#ENCODING_PCM_16BIT */
+ public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT;
+ /** 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 */
+ public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
+ /** @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 */
+ 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;
+
+ /**
+ * 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_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
+ */
+ public static final int STREAM_TYPE_MUSIC = AudioManager.STREAM_MUSIC;
+ /**
+ * @see AudioManager#STREAM_NOTIFICATION
+ */
+ public static final int STREAM_TYPE_NOTIFICATION = AudioManager.STREAM_NOTIFICATION;
+ /**
+ * @see AudioManager#STREAM_RING
+ */
+ public static final int STREAM_TYPE_RING = AudioManager.STREAM_RING;
+ /**
+ * @see AudioManager#STREAM_SYSTEM
+ */
+ public static final int STREAM_TYPE_SYSTEM = AudioManager.STREAM_SYSTEM;
+ /**
+ * @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;
+
+ /**
+ * 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({
+ 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}.
+ *
+ * <p>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.
+ */
+ 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.
+ */
+ 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
+ /**
+ * Video decoder output modes. Possible modes are {@link #VIDEO_OUTPUT_MODE_NONE}, {@link
+ * #VIDEO_OUTPUT_MODE_YUV} and {@link #VIDEO_OUTPUT_MODE_SURFACE_YUV}.
+ */
+ @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. 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
+ */
+ 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
+ */
+ public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING =
+ MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING;
+ /**
+ * A default video scaling mode for {@link MediaCodec}-based {@link Renderer}s.
+ */
+ public static final int VIDEO_SCALING_MODE_DEFAULT = VIDEO_SCALING_MODE_SCALE_TO_FIT;
+
+ /**
+ * 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})
+ 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 = 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 = 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. 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 {}
+ /**
+ * Value returned by {@link Util#inferContentType(String)} for DASH manifests.
+ */
+ public static final int TYPE_DASH = 0;
+ /**
+ * Value returned by {@link Util#inferContentType(String)} for Smooth Streaming manifests.
+ */
+ public static final int TYPE_SS = 1;
+ /**
+ * Value returned by {@link Util#inferContentType(String)} for HLS manifests.
+ */
+ public static final int TYPE_HLS = 2;
+ /**
+ * Value returned by {@link Util#inferContentType(String)} for files other than DASH, HLS or
+ * Smooth Streaming manifests.
+ */
+ public static final int TYPE_OTHER = 3;
+
+ /**
+ * A return value for methods where the end of an input was encountered.
+ */
+ public static final int RESULT_END_OF_INPUT = -1;
+ /**
+ * A return value for methods where the length of parsed data exceeds the maximum length allowed.
+ */
+ public static final int RESULT_MAX_LENGTH_EXCEEDED = -2;
+ /**
+ * A return value for methods where nothing was read.
+ */
+ public static final int RESULT_NOTHING_READ = -3;
+ /**
+ * A return value for methods where a buffer was read.
+ */
+ public static final int RESULT_BUFFER_READ = -4;
+ /**
+ * A return value for methods where a format was read.
+ */
+ public static final int RESULT_FORMAT_READ = -5;
+
+ /** 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. */
+ public static final int DATA_TYPE_MEDIA = 1;
+ /** 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. */
+ public static final int DATA_TYPE_DRM = 3;
+ /** A data type constant for a manifest file. */
+ public static final int DATA_TYPE_MANIFEST = 4;
+ /** 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. */
+ public static final int TRACK_TYPE_UNKNOWN = -1;
+ /** 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. */
+ public static final int TRACK_TYPE_AUDIO = 1;
+ /** A type constant for video tracks. */
+ public static final int TRACK_TYPE_VIDEO = 2;
+ /** A type constant for text tracks. */
+ public static final int TRACK_TYPE_TEXT = 3;
+ /** 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.
+ */
+ public static final int TRACK_TYPE_CUSTOM_BASE = 10000;
+
+ /**
+ * A selection reason constant for selections whose reasons are unknown or unspecified.
+ */
+ public static final int SELECTION_REASON_UNKNOWN = 0;
+ /**
+ * A selection reason constant for an initial track selection.
+ */
+ public static final int SELECTION_REASON_INITIAL = 1;
+ /**
+ * A selection reason constant for an manual (i.e. user initiated) track selection.
+ */
+ public static final int SELECTION_REASON_MANUAL = 2;
+ /**
+ * A selection reason constant for an adaptive track selection.
+ */
+ public static final int SELECTION_REASON_ADAPTIVE = 3;
+ /**
+ * A selection reason constant for a trick play track selection.
+ */
+ public static final int SELECTION_REASON_TRICK_PLAY = 4;
+ /**
+ * Applications or extensions may define custom {@code SELECTION_REASON_*} constants greater than
+ * or equal to this value.
+ */
+ 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. */
+ public static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024;
+
+ /** "cenc" scheme type name as defined in ISO/IEC 23001-7:2016. */
+ @SuppressWarnings("ConstantField")
+ public static final String CENC_TYPE_cenc = "cenc";
+
+ /** "cbc1" scheme type name as defined in ISO/IEC 23001-7:2016. */
+ @SuppressWarnings("ConstantField")
+ public static final String CENC_TYPE_cbc1 = "cbc1";
+
+ /** "cens" scheme type name as defined in ISO/IEC 23001-7:2016. */
+ @SuppressWarnings("ConstantField")
+ public static final String CENC_TYPE_cens = "cens";
+
+ /** "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
+ * <a href="https://tools.ietf.org/html/rfc4122#section-4.1.7">RFC4122</a>.
+ */
+ public static final UUID UUID_NIL = new UUID(0L, 0L);
+
+ /**
+ * UUID for the W3C
+ * <a href="https://w3c.github.io/encrypted-media/format-registry/initdata/cenc.html">Common PSSH
+ * box</a>.
+ */
+ public static final UUID COMMON_PSSH_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL);
+
+ /**
+ * UUID for the ClearKey DRM scheme.
+ * <p>
+ * ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up.
+ */
+ public static final UUID CLEARKEY_UUID = new UUID(0xE2719D58A985B3C9L, 0x781AB030AF78D30EL);
+
+ /**
+ * UUID for the Widevine DRM scheme.
+ * <p>
+ * Widevine is supported on Android devices running Android 4.3 (API Level 18) and up.
+ */
+ public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
+
+ /**
+ * UUID for the PlayReady DRM scheme.
+ * <p>
+ * PlayReady is supported on all AndroidTV devices. Note that most other Android devices do not
+ * provide PlayReady support.
+ */
+ 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#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#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#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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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)}.
+ *
+ * <p>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_AUDIO_ATTRIBUTES = 3;
+
+ /**
+ * The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer}
+ * via {@link ExoPlayer#createMessage(Target)}. The message payload should be one of the integer
+ * scaling modes in {@link C.VideoScalingMode}.
+ *
+ * <p>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;
+
+ /**
+ * 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.
+ *
+ * <p>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. 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
+ })
+ public @interface StereoMode {}
+ /**
+ * Indicates Monoscopic stereo layout, used with 360/3D/VR videos.
+ */
+ public static final int STEREO_MODE_MONO = 0;
+ /**
+ * Indicates Top-Bottom stereo layout, used with 360/3D/VR videos.
+ */
+ public static final int STEREO_MODE_TOP_BOTTOM = 1;
+ /**
+ * Indicates Left-Right stereo layout, used with 360/3D/VR videos.
+ */
+ public static final int STEREO_MODE_LEFT_RIGHT = 2;
+ /**
+ * Indicates a stereo layout where the left and right eyes have separate meshes,
+ * used with 360/3D/VR videos.
+ */
+ public static final int STEREO_MODE_STEREO_MESH = 3;
+
+ /**
+ * 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
+ */
+ public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709;
+ /**
+ * @see MediaFormat#COLOR_STANDARD_BT601_PAL
+ */
+ public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL;
+ /**
+ * @see MediaFormat#COLOR_STANDARD_BT2020
+ */
+ public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020;
+
+ /**
+ * 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
+ */
+ public static final int COLOR_TRANSFER_SDR = MediaFormat.COLOR_TRANSFER_SDR_VIDEO;
+ /**
+ * @see MediaFormat#COLOR_TRANSFER_ST2084
+ */
+ public static final int COLOR_TRANSFER_ST2084 = MediaFormat.COLOR_TRANSFER_ST2084;
+ /**
+ * @see MediaFormat#COLOR_TRANSFER_HLG
+ */
+ public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG;
+
+ /**
+ * 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
+ */
+ public static final int COLOR_RANGE_LIMITED = MediaFormat.COLOR_RANGE_LIMITED;
+ /**
+ * @see MediaFormat#COLOR_RANGE_FULL
+ */
+ 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.
+ *
+ * <p>Larger values indicate higher priorities.
+ */
+ public static final int PRIORITY_PLAYBACK = 0;
+
+ /**
+ * Priority for media downloading.
+ *
+ * <p>Larger values indicate higher priorities.
+ */
+ 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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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} 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 || timeUs == TIME_END_OF_SOURCE) ? timeUs : (timeUs / 1000);
+ }
+
+ /**
+ * Converts a time in milliseconds to the corresponding time in microseconds, preserving
+ * {@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 || timeMs == TIME_END_OF_SOURCE) ? timeMs : (timeMs * 1000);
+ }
+
+ /**
+ * 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) {
+ return ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE))
+ .generateAudioSessionId();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ControlDispatcher.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ControlDispatcher.java
new file mode 100644
index 0000000000..a23b44e685
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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}.
+ * <p>
+ * 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultControlDispatcher.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultControlDispatcher.java
new file mode 100644
index 0000000000..32fa0edf6e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultLoadControl.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultLoadControl.java
new file mode 100644
index 0000000000..ad5350a722
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultLoadControl.java
@@ -0,0 +1,473 @@
+/*
+ * 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 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.Allocator;
+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.Util;
+
+/**
+ * The default {@link LoadControl} implementation.
+ */
+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. 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 = 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 = 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.
+ */
+ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;
+
+ /**
+ * 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 minBufferAudioUs;
+ private final long minBufferVideoUs;
+ private final long maxBufferUs;
+ private final long bufferForPlaybackUs;
+ private final long bufferForPlaybackAfterRebufferUs;
+ 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. */
+ @SuppressWarnings("deprecation")
+ public DefaultLoadControl() {
+ this(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE));
+ }
+
+ /** @deprecated Use {@link Builder} instead. */
+ @Deprecated
+ public DefaultLoadControl(DefaultAllocator allocator) {
+ 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);
+ }
+
+ /** @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);
+ }
+
+ 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;
+ 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
+ public void onPrepared() {
+ reset(false);
+ }
+
+ @Override
+ public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
+ TrackSelectionArray trackSelections) {
+ hasVideo = hasVideo(renderers, trackSelections);
+ targetBufferSize =
+ targetBufferBytesOverwrite == C.LENGTH_UNSET
+ ? calculateTargetBufferSize(renderers, trackSelections)
+ : targetBufferBytesOverwrite;
+ allocator.setTargetBufferSize(targetBufferSize);
+ }
+
+ @Override
+ public void onStopped() {
+ reset(true);
+ }
+
+ @Override
+ public void onReleased() {
+ reset(true);
+ }
+
+ @Override
+ public Allocator getAllocator() {
+ return allocator;
+ }
+
+ @Override
+ public long getBackBufferDurationUs() {
+ return backBufferDurationUs;
+ }
+
+ @Override
+ public boolean retainBackBufferFromKeyframe() {
+ return retainBackBufferFromKeyframe;
+ }
+
+ @Override
+ public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) {
+ boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
+ 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;
+ }
+
+ @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;
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultMediaClock.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultMediaClock.java
new file mode 100644
index 0000000000..9967bfeb9e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultRenderersFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultRenderersFactory.java
new file mode 100644
index 0000000000..95fe509ee9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/DefaultRenderersFactory.java
@@ -0,0 +1,580 @@
+/*
+ * 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.content.Context;
+import android.media.MediaCodec;
+import android.os.Handler;
+import android.os.Looper;
+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;
+import java.util.ArrayList;
+
+/**
+ * Default {@link RenderersFactory} implementation.
+ */
+public class DefaultRenderersFactory implements RenderersFactory {
+
+ /**
+ * The default maximum duration for which a video renderer can attempt to seamlessly join an
+ * ongoing playback.
+ */
+ public static final long DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS = 5000;
+
+ /**
+ * 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})
+ public @interface ExtensionRendererMode {}
+ /**
+ * Do not allow use of extension renderers.
+ */
+ public static final int EXTENSION_RENDERER_MODE_OFF = 0;
+ /**
+ * Allow use of extension renderers. Extension renderers are indexed after core renderers of the
+ * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore
+ * prefer to use a core renderer to an extension renderer in the case that both are able to play
+ * a given track.
+ */
+ public static final int EXTENSION_RENDERER_MODE_ON = 1;
+ /**
+ * Allow use of extension renderers. Extension renderers are indexed before core renderers of the
+ * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore
+ * prefer to use an extension renderer to a core renderer in the case that both are able to play
+ * a given track.
+ */
+ public static final int EXTENSION_RENDERER_MODE_PREFER = 2;
+
+ private static final String TAG = "DefaultRenderersFactory";
+
+ protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50;
+
+ private final Context context;
+ @Nullable private DrmSessionManager<FrameworkMediaCrypto> drmSessionManager;
+ @ExtensionRendererMode private int extensionRendererMode;
+ private long allowedVideoJoiningTimeMs;
+ private boolean playClearSamplesWithoutKeys;
+ private boolean enableDecoderFallback;
+ private MediaCodecSelector mediaCodecSelector;
+
+ /** @param context A {@link Context}. */
+ public DefaultRenderersFactory(Context context) {
+ this.context = context;
+ extensionRendererMode = EXTENSION_RENDERER_MODE_OFF;
+ allowedVideoJoiningTimeMs = DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS;
+ mediaCodecSelector = MediaCodecSelector.DEFAULT;
+ }
+
+ /**
+ * @deprecated Use {@link #DefaultRenderersFactory(Context)} and pass {@link DrmSessionManager}
+ * directly to {@link SimpleExoPlayer.Builder}.
+ */
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public DefaultRenderersFactory(
+ Context context, @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
+ this(context, drmSessionManager, EXTENSION_RENDERER_MODE_OFF);
+ }
+
+ /**
+ * @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link
+ * #setExtensionRendererMode(int)}.
+ */
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public DefaultRenderersFactory(
+ Context context, @ExtensionRendererMode int extensionRendererMode) {
+ this(context, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
+ }
+
+ /**
+ * @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link
+ * #setExtensionRendererMode(int)}, and pass {@link DrmSessionManager} directly to {@link
+ * SimpleExoPlayer.Builder}.
+ */
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public DefaultRenderersFactory(
+ Context context,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> 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<FrameworkMediaCrypto> drmSessionManager,
+ @ExtensionRendererMode int extensionRendererMode,
+ long allowedVideoJoiningTimeMs) {
+ this.context = context;
+ 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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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,
+ VideoRendererEventListener videoRendererEventListener,
+ AudioRendererEventListener audioRendererEventListener,
+ TextOutput textRendererOutput,
+ MetadataOutput metadataRendererOutput,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
+ if (drmSessionManager == null) {
+ drmSessionManager = this.drmSessionManager;
+ }
+ ArrayList<Renderer> renderersList = new ArrayList<>();
+ 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[0]);
+ }
+
+ /**
+ * Builds video renderers for use by the player.
+ *
+ * @param context The {@link Context} associated with the player.
+ * @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 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,
+ @ExtensionRendererMode int extensionRendererMode,
+ MediaCodecSelector mediaCodecSelector,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ boolean playClearSamplesWithoutKeys,
+ boolean enableDecoderFallback,
+ Handler eventHandler,
+ VideoRendererEventListener eventListener,
+ long allowedVideoJoiningTimeMs,
+ ArrayList<Renderer> 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;
+ }
+ int extensionRendererIndex = out.size();
+ if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {
+ extensionRendererIndex--;
+ }
+
+ 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.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) {
+ // 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);
+ }
+ }
+
+ /**
+ * Builds audio renderers for use by the player.
+ *
+ * @param context The {@link Context} associated with the player.
+ * @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 out An array to which the built renderers should be appended.
+ */
+ protected void buildAudioRenderers(
+ Context context,
+ @ExtensionRendererMode int extensionRendererMode,
+ MediaCodecSelector mediaCodecSelector,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ boolean playClearSamplesWithoutKeys,
+ boolean enableDecoderFallback,
+ AudioProcessor[] audioProcessors,
+ Handler eventHandler,
+ AudioRendererEventListener eventListener,
+ ArrayList<Renderer> out) {
+ out.add(
+ new MediaCodecAudioRenderer(
+ context,
+ mediaCodecSelector,
+ drmSessionManager,
+ playClearSamplesWithoutKeys,
+ enableDecoderFallback,
+ eventHandler,
+ eventListener,
+ new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors)));
+
+ if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
+ return;
+ }
+ int extensionRendererIndex = out.size();
+ if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {
+ extensionRendererIndex--;
+ }
+
+ 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.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) {
+ // The extension is present, but instantiation failed.
+ throw new RuntimeException("Error instantiating Opus 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.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) {
+ // 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(
+ 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) {
+ // The extension is present, but instantiation failed.
+ throw new RuntimeException("Error instantiating FFmpeg extension", e);
+ }
+ }
+
+ /**
+ * Builds text renderers for use by the player.
+ *
+ * @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 extensionRendererMode The extension renderer mode.
+ * @param out An array to which the built renderers should be appended.
+ */
+ protected void buildTextRenderers(
+ Context context,
+ TextOutput output,
+ Looper outputLooper,
+ @ExtensionRendererMode int extensionRendererMode,
+ ArrayList<Renderer> out) {
+ out.add(new TextRenderer(output, outputLooper));
+ }
+
+ /**
+ * Builds metadata renderers for use by the player.
+ *
+ * @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 extensionRendererMode The extension renderer mode.
+ * @param out An array to which the built renderers should be appended.
+ */
+ protected void buildMetadataRenderers(
+ Context context,
+ MetadataOutput output,
+ Looper outputLooper,
+ @ExtensionRendererMode int extensionRendererMode,
+ ArrayList<Renderer> 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<Renderer> out) {
+ out.add(new CameraMotionRenderer());
+ }
+
+ /**
+ * Builds any miscellaneous renderers used by the player.
+ *
+ * @param context The {@link Context} associated with the player.
+ * @param eventHandler A handler to use when invoking event listeners and outputs.
+ * @param extensionRendererMode The extension renderer mode.
+ * @param out An array to which the built renderers should be appended.
+ */
+ protected void buildMiscellaneousRenderers(Context context, Handler eventHandler,
+ @ExtensionRendererMode int extensionRendererMode, ArrayList<Renderer> out) {
+ // Do nothing.
+ }
+
+ /**
+ * Builds an array of {@link AudioProcessor}s that will process PCM audio before output.
+ */
+ protected AudioProcessor[] buildAudioProcessors() {
+ return new AudioProcessor[0];
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlaybackException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlaybackException.java
new file mode 100644
index 0000000000..bad5cc7693
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlaybackException.java
@@ -0,0 +1,233 @@
+/*
+ * 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 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;
+
+/**
+ * Thrown when a non-recoverable playback failure occurs.
+ */
+public final class ExoPlaybackException extends Exception {
+
+ /**
+ * 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, TYPE_REMOTE, TYPE_OUT_OF_MEMORY})
+ public @interface Type {}
+ /**
+ * The error occurred loading data from a {@link MediaSource}.
+ * <p>
+ * Call {@link #getSourceException()} to retrieve the underlying cause.
+ */
+ public static final int TYPE_SOURCE = 0;
+ /**
+ * The error occurred in a {@link Renderer}.
+ * <p>
+ * Call {@link #getRendererException()} to retrieve the underlying cause.
+ */
+ public static final int TYPE_RENDERER = 1;
+ /**
+ * The error was an unexpected {@link RuntimeException}.
+ * <p>
+ * Call {@link #getUnexpectedException()} to retrieve the underlying cause.
+ */
+ public static final int TYPE_UNEXPECTED = 2;
+ /**
+ * The error occurred in a remote component.
+ *
+ * <p>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;
+
+ /**
+ * If {@link #type} is {@link #TYPE_RENDERER}, this is the index of the renderer.
+ */
+ public final int rendererIndex;
+
+ /**
+ * 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}.
+ */
+ @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}.
+ *
+ * @param cause The cause of the failure.
+ * @return The created instance.
+ */
+ public static ExoPlaybackException createForSource(IOException cause) {
+ 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);
+ }
+
+ /**
+ * Creates an instance of type {@link #TYPE_UNEXPECTED}.
+ *
+ * @param cause The cause of the failure.
+ * @return The created instance.
+ */
+ public static ExoPlaybackException createForUnexpected(RuntimeException cause) {
+ return new ExoPlaybackException(TYPE_UNEXPECTED, 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();
+ }
+
+ /**
+ * Retrieves the underlying error when {@link #type} is {@link #TYPE_SOURCE}.
+ *
+ * @throws IllegalStateException If {@link #type} is not {@link #TYPE_SOURCE}.
+ */
+ public IOException getSourceException() {
+ Assertions.checkState(type == TYPE_SOURCE);
+ return (IOException) Assertions.checkNotNull(cause);
+ }
+
+ /**
+ * Retrieves the underlying error when {@link #type} is {@link #TYPE_RENDERER}.
+ *
+ * @throws IllegalStateException If {@link #type} is not {@link #TYPE_RENDERER}.
+ */
+ public Exception getRendererException() {
+ Assertions.checkState(type == TYPE_RENDERER);
+ return (Exception) Assertions.checkNotNull(cause);
+ }
+
+ /**
+ * Retrieves the underlying error when {@link #type} is {@link #TYPE_UNEXPECTED}.
+ *
+ * @throws IllegalStateException If {@link #type} is not {@link #TYPE_UNEXPECTED}.
+ */
+ public RuntimeException getUnexpectedException() {
+ Assertions.checkState(type == TYPE_UNEXPECTED);
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayer.java
new file mode 100644
index 0000000000..048c1776c9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayer.java
@@ -0,0 +1,404 @@
+/*
+ * 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 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.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.text.TextRenderer;
+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.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 that plays {@link MediaSource}s. Instances can be obtained from {@link
+ * SimpleExoPlayer.Builder} or {@link ExoPlayer.Builder}.
+ *
+ * <h3>Player components</h3>
+ *
+ * <p>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:
+ *
+ * <ul>
+ * <li>A <b>{@link MediaSource}</b> that defines the media to be played, loads the media, and from
+ * which the loaded media can be read. A MediaSource is injected via {@link
+ * #prepare(MediaSource)} at the start of playback. The library modules provide default
+ * implementations for progressive media files ({@link ProgressiveMediaSource}), DASH
+ * (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an
+ * implementation for loading single media samples ({@link SingleSampleMediaSource}) that's
+ * most often used for side-loaded subtitle files, and implementations for building more
+ * complex MediaSources from simpler ones ({@link MergingMediaSource}, {@link
+ * ConcatenatingMediaSource}, {@link LoopingMediaSource} and {@link ClippingMediaSource}).
+ * <li><b>{@link Renderer}</b>s that render individual components of the media. The library
+ * provides default implementations for common media types ({@link MediaCodecVideoRenderer},
+ * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A
+ * Renderer consumes media from the MediaSource being played. Renderers are injected when the
+ * player is created.
+ * <li>A <b>{@link TrackSelector}</b> that selects tracks provided by the MediaSource to be
+ * consumed by each of the available Renderers. The library provides a default implementation
+ * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected
+ * when the player is created.
+ * <li>A <b>{@link LoadControl}</b> that controls when the MediaSource buffers more media, and how
+ * much media is buffered. The library provides a default implementation ({@link
+ * DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the player
+ * is created.
+ * </ul>
+ *
+ * <p>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 add support for a video codec not supported natively by Android.
+ *
+ * <p>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.
+ *
+ * <h3>Threading model</h3>
+ *
+ * <p>The figure below shows ExoPlayer's threading model.
+ *
+ * <p style="align:center"><img src="doc-files/exoplayer-threading-model.svg" alt="ExoPlayer's
+ * threading model">
+ *
+ * <ul>
+ * <li>ExoPlayer instances must be accessed from a single application thread. For the vast
+ * majority of cases this should be the application's main thread. Using the application's
+ * main thread is also a requirement when using ExoPlayer's UI components or the IMA
+ * extension. The thread on which an ExoPlayer instance must be accessed can be explicitly
+ * specified by passing a `Looper` when creating the player. If no `Looper` is specified, then
+ * the `Looper` of the thread that the player is created on is used, or if that thread does
+ * not have a `Looper`, the `Looper` of the application's main thread is used. In all cases
+ * the `Looper` of the thread from which the player must be accessed can be queried using
+ * {@link #getApplicationLooper()}.
+ * <li>Registered listeners are called on the thread associated with {@link
+ * #getApplicationLooper()}. Note that this means registered listeners are called on the same
+ * thread which must be used to access the player.
+ * <li>An internal playback thread is responsible for playback. Injected player components such as
+ * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this
+ * thread.
+ * <li>When the application performs an operation on the player, for example a seek, a message is
+ * delivered to the internal playback thread via a message queue. The internal playback thread
+ * consumes messages from the queue and performs the corresponding operations. Similarly, when
+ * a playback event occurs on the internal playback thread, a message is delivered to the
+ * application thread via a second message queue. The application thread consumes messages
+ * from the queue, updating the application visible state and calling corresponding listener
+ * methods.
+ * <li>Injected player components may use additional background threads. For example a MediaSource
+ * may use background threads to load data. These are implementation specific.
+ * </ul>
+ */
+public interface ExoPlayer extends Player {
+
+ /**
+ * A builder for {@link ExoPlayer} instances.
+ *
+ * <p>See {@link #Builder(Context, Renderer...)} for the list of default values.
+ */
+ 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;
+
+ /**
+ * Creates a builder with a list of {@link Renderer Renderers}.
+ *
+ * <p>The builder uses the following default values:
+ *
+ * <ul>
+ * <li>{@link TrackSelector}: {@link DefaultTrackSelector}
+ * <li>{@link LoadControl}: {@link DefaultLoadControl}
+ * <li>{@link BandwidthMeter}: {@link DefaultBandwidthMeter#getSingletonInstance(Context)}
+ * <li>{@link Looper}: 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}
+ * <li>{@link AnalyticsCollector}: {@link AnalyticsCollector} with {@link Clock#DEFAULT}
+ * <li>{@code useLazyPreparation}: {@code true}
+ * <li>{@link Clock}: {@link Clock#DEFAULT}
+ * </ul>
+ *
+ * @param context A {@link Context}.
+ * @param renderers The {@link Renderer Renderers} to be used by the player.
+ */
+ 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.
+ *
+ * <p>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.
+ *
+ * <p>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);
+ }
+ }
+
+ /** Returns the {@link Looper} associated with the playback thread. */
+ Looper getPlaybackLooper();
+
+ /**
+ * Retries a failed or stopped playback. Does nothing if the player has been reset, or if playback
+ * has not failed or been stopped.
+ */
+ void retry();
+
+ /**
+ * Prepares the player to play the provided {@link MediaSource}. Equivalent to {@code
+ * prepare(mediaSource, true, true)}.
+ */
+ void prepare(MediaSource mediaSource);
+
+ /**
+ * Prepares the player to play the provided {@link MediaSource}, optionally resetting the playback
+ * position the default position in the first {@link Timeline.Window}.
+ *
+ * @param mediaSource The {@link MediaSource} to play.
+ * @param resetPosition Whether the playback position should be reset to the default position in
+ * the first {@link Timeline.Window}. If false, playback will start from the position defined
+ * by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}.
+ * @param resetState Whether the timeline, manifest, tracks and track selections should be reset.
+ * Should be true unless the player is being prepared to play the same media as it was playing
+ * previously (e.g. if playback failed and is being retried).
+ */
+ void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState);
+
+ /**
+ * 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 seekParameters The seek parameters, or {@code null} to use the defaults.
+ */
+ void setSeekParameters(@Nullable SeekParameters seekParameters);
+
+ /** Returns the currently active {@link SeekParameters} of the player. */
+ SeekParameters getSeekParameters();
+
+ /**
+ * 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.
+ *
+ * <p>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 <em>both</em>
+ * of the following conditions are true:
+ *
+ * <ul>
+ * <li>The application that owns the player is in the foreground.
+ * <li>The player is used in a way that may benefit from foreground mode. For this to be true,
+ * the same player instance must be used to play multiple pieces of content, and there must
+ * be gaps between the playbacks (i.e. {@link #stop} is called to halt one playback, and
+ * {@link #prepare} is called some time later to start a new one).
+ * </ul>
+ *
+ * <p>Note that foreground mode is <em>not</em> 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.
+ *
+ * <p>If foreground mode is enabled, it's the application's responsibility to disable it when the
+ * conditions described above no longer hold.
+ *
+ * @param foregroundMode Whether the player is allowed to keep limited resources even when in the
+ * idle state.
+ */
+ void setForegroundMode(boolean foregroundMode);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerFactory.java
new file mode 100644
index 0000000000..a2e89fc3cc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerFactory.java
@@ -0,0 +1,350 @@
+/*
+ * 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 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;
+
+/** @deprecated Use {@link SimpleExoPlayer.Builder} or {@link ExoPlayer.Builder} instead. */
+@Deprecated
+public final class ExoPlayerFactory {
+
+ private ExoPlayerFactory() {}
+
+ /**
+ * @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,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) {
+ RenderersFactory renderersFactory =
+ new DefaultRenderersFactory(context).setExtensionRendererMode(extensionRendererMode);
+ return newSimpleInstance(
+ context, renderersFactory, trackSelector, loadControl, drmSessionManager);
+ }
+
+ /**
+ * @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,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode,
+ long allowedVideoJoiningTimeMs) {
+ RenderersFactory renderersFactory =
+ new DefaultRenderersFactory(context)
+ .setExtensionRendererMode(extensionRendererMode)
+ .setAllowedVideoJoiningTimeMs(allowedVideoJoiningTimeMs);
+ return newSimpleInstance(
+ context, renderersFactory, trackSelector, loadControl, drmSessionManager);
+ }
+
+ /** @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(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);
+ }
+
+ /**
+ * @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,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
+ RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
+ return newSimpleInstance(
+ context, renderersFactory, trackSelector, loadControl, drmSessionManager);
+ }
+
+ /**
+ * @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,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
+ return newSimpleInstance(
+ context, renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager);
+ }
+
+ /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public static SimpleExoPlayer newSimpleInstance(
+ Context context,
+ RenderersFactory renderersFactory,
+ TrackSelector trackSelector,
+ LoadControl 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<FrameworkMediaCrypto> 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<FrameworkMediaCrypto> 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<FrameworkMediaCrypto> 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<FrameworkMediaCrypto> 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<FrameworkMediaCrypto> 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<FrameworkMediaCrypto> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImpl.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImpl.java
new file mode 100644
index 0000000000..eb9eaae2cf
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -0,0 +1,848 @@
+/*
+ * 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 android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+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.ArrayDeque;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayer.Builder}.
+ */
+/* 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 Handler eventHandler;
+ private final ExoPlayerImplInternal internalPlayer;
+ private final Handler internalPlayerHandler;
+ private final CopyOnWriteArrayList<ListenerHolder> listeners;
+ private final Timeline.Period period;
+ private final ArrayDeque<Runnable> pendingListenerNotifications;
+
+ private MediaSource mediaSource;
+ private boolean playWhenReady;
+ @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;
+
+ // Playback information when there is a pending seek/set source operation.
+ private int maskingWindowIndex;
+ private int maskingPeriodIndex;
+ private long maskingWindowPositionMs;
+
+ /**
+ * Constructs an 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.
+ * @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,
+ 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.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();
+ playbackParameters = PlaybackParameters.DEFAULT;
+ 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);
+ }
+ }
+ }
+
+ @Override
+ @State
+ public int getPlaybackState() {
+ 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, /* resetPosition= */ true, /* resetState= */ true);
+ }
+
+ @Override
+ public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
+ 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) {
+ 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);
+ }
+ });
+ }
+ }
+
+ @Override
+ public boolean getPlayWhenReady() {
+ 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 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);
+ }
+ 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 {
+ long windowPositionUs = positionMs == C.TIME_UNSET
+ ? timeline.getWindow(windowIndex, window).getDefaultPositionUs() : C.msToUs(positionMs);
+ Pair<Object, Long> 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
+ public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
+ 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
+ public PlaybackParameters getPlaybackParameters() {
+ return playbackParameters;
+ }
+
+ @Override
+ 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 PlayerMessage createMessage(Target target) {
+ return new PlayerMessage(
+ internalPlayer,
+ target,
+ playbackInfo.timeline,
+ getCurrentWindowIndex(),
+ internalPlayerHandler);
+ }
+
+ @Override
+ public int getCurrentPeriodIndex() {
+ if (shouldMaskPosition()) {
+ return maskingPeriodIndex;
+ } else {
+ return playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid);
+ }
+ }
+
+ @Override
+ public int getCurrentWindowIndex() {
+ if (shouldMaskPosition()) {
+ return maskingWindowIndex;
+ } else {
+ return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period)
+ .windowIndex;
+ }
+ }
+
+ @Override
+ public long getDuration() {
+ 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 getContentDuration();
+ }
+
+ @Override
+ public long getCurrentPosition() {
+ if (shouldMaskPosition()) {
+ return maskingWindowPositionMs;
+ } else if (playbackInfo.periodId.isAd()) {
+ return C.usToMs(playbackInfo.positionUs);
+ } else {
+ return periodPositionUsToWindowPositionMs(playbackInfo.periodId, playbackInfo.positionUs);
+ }
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ 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 {
+ return getCurrentPosition();
+ }
+ }
+
+ @Override
+ public long getContentBufferedPosition() {
+ if (shouldMaskPosition()) {
+ return maskingWindowPositionMs;
+ }
+ 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
+ public int getRendererCount() {
+ return renderers.length;
+ }
+
+ @Override
+ public int getRendererType(int index) {
+ return renderers[index].getTrackType();
+ }
+
+ @Override
+ public TrackGroupArray getCurrentTrackGroups() {
+ return playbackInfo.trackGroups;
+ }
+
+ @Override
+ public TrackSelectionArray getCurrentTrackSelections() {
+ return playbackInfo.trackSelectorResult.selections;
+ }
+
+ @Override
+ public Timeline getCurrentTimeline() {
+ 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_PLAYBACK_INFO_CHANGED:
+ handlePlaybackInfo(
+ (PlaybackInfo) msg.obj,
+ /* operationAcks= */ msg.arg1,
+ /* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET,
+ /* positionDiscontinuityReason= */ msg.arg2);
+ break;
+ case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED:
+ handlePlaybackParameters((PlaybackParameters) msg.obj, /* operationAck= */ msg.arg1 != 0);
+ 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<ListenerHolder> 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<ListenerHolder> 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<ListenerHolder> 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<ListenerHolder> listeners, ListenerInvocation listenerInvocation) {
+ for (ListenerHolder listenerHolder : listeners) {
+ listenerHolder.invoke(listenerInvocation);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java
new file mode 100644
index 0000000000..a4462ad1c4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -0,0 +1,2045 @@
+/*
+ * 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 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.Pair;
+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.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.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,
+ MediaSourceCaller,
+ PlaybackParameterListener,
+ PlayerMessage.Sender {
+
+ private static final String TAG = "ExoPlayerImplInternal";
+
+ // External messages
+ 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;
+ private static final int MSG_SET_PLAY_WHEN_READY = 1;
+ 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_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 ACTIVE_INTERVAL_MS = 10;
+ private static final int IDLE_INTERVAL_MS = 1000;
+
+ private final Renderer[] renderers;
+ private final RendererCapabilities[] rendererCapabilities;
+ private final TrackSelector trackSelector;
+ private final TrackSelectorResult emptyTrackSelectorResult;
+ private final LoadControl loadControl;
+ private final BandwidthMeter bandwidthMeter;
+ private final HandlerWrapper handler;
+ private final HandlerThread internalPlaybackThread;
+ private final Handler eventHandler;
+ 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<PendingMessageInfo> pendingMessages;
+ private final Clock clock;
+ private final MediaPeriodQueue queue;
+
+ @SuppressWarnings("unused")
+ private SeekParameters seekParameters;
+
+ private PlaybackInfo playbackInfo;
+ private MediaSource mediaSource;
+ private Renderer[] enabledRenderers;
+ private boolean released;
+ private boolean playWhenReady;
+ private boolean rebuffering;
+ private boolean shouldContinueLoading;
+ @Player.RepeatMode private int repeatMode;
+ private boolean shuffleModeEnabled;
+ private boolean foregroundMode;
+
+ private int pendingPrepareCount;
+ private SeekPosition pendingInitialSeekPosition;
+ private long rendererPositionUs;
+ private int nextPendingMessageIndex;
+ private boolean deliverPendingMessageAtStartPositionRequired;
+
+ 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.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();
+ }
+ mediaClock = new DefaultMediaClock(this, clock);
+ pendingMessages = new ArrayList<>();
+ enabledRenderers = new Renderer[0];
+ window = new Timeline.Window();
+ period = new Timeline.Period();
+ 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.start();
+ handler = clock.createHandler(internalPlaybackThread.getLooper(), this);
+ deliverPendingMessageAtStartPositionRequired = true;
+ }
+
+ public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
+ handler
+ .obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource)
+ .sendToTarget();
+ }
+
+ public void setPlayWhenReady(boolean playWhenReady) {
+ 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))
+ .sendToTarget();
+ }
+
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {
+ handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget();
+ }
+
+ public void setSeekParameters(SeekParameters seekParameters) {
+ handler.obtainMessage(MSG_SET_SEEK_PARAMETERS, seekParameters).sendToTarget();
+ }
+
+ public void stop(boolean reset) {
+ handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget();
+ }
+
+ @Override
+ public synchronized void sendMessage(PlayerMessage message) {
+ if (released || !internalPlaybackThread.isAlive()) {
+ Log.w(TAG, "Ignoring messages sent after release.");
+ message.markAsProcessed(/* isDelivered= */ false);
+ return;
+ }
+ 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 || !internalPlaybackThread.isAlive()) {
+ return;
+ }
+ handler.sendEmptyMessage(MSG_RELEASE);
+ boolean wasInterrupted = false;
+ while (!released) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ wasInterrupted = true;
+ }
+ }
+ if (wasInterrupted) {
+ // Restore the interrupted status.
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ public Looper getPlaybackLooper() {
+ return internalPlaybackThread.getLooper();
+ }
+
+ // MediaSource.MediaSourceCaller implementation.
+
+ @Override
+ public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) {
+ handler
+ .obtainMessage(MSG_REFRESH_SOURCE_INFO, new MediaSourceRefreshInfo(source, timeline))
+ .sendToTarget();
+ }
+
+ // MediaPeriod.Callback implementation.
+
+ @Override
+ public void onPrepared(MediaPeriod source) {
+ handler.obtainMessage(MSG_PERIOD_PREPARED, source).sendToTarget();
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod source) {
+ handler.obtainMessage(MSG_SOURCE_CONTINUE_LOADING_REQUESTED, source).sendToTarget();
+ }
+
+ // TrackSelector.InvalidationListener implementation.
+
+ @Override
+ public void onTrackSelectionsInvalidated() {
+ handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED);
+ }
+
+ // DefaultMediaClock.PlaybackParameterListener implementation.
+
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ sendPlaybackParametersChangedInternal(playbackParameters, /* acknowledgeCommand= */ false);
+ }
+
+ // Handler.Callback implementation.
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ try {
+ switch (msg.what) {
+ 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);
+ 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();
+ break;
+ case MSG_SEEK_TO:
+ seekToInternal((SeekPosition) msg.obj);
+ break;
+ case MSG_SET_PLAYBACK_PARAMETERS:
+ setPlaybackParametersInternal((PlaybackParameters) msg.obj);
+ 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);
+ break;
+ case MSG_REFRESH_SOURCE_INFO:
+ handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj);
+ break;
+ case MSG_SOURCE_CONTINUE_LOADING_REQUESTED:
+ handleContinueLoadingRequested((MediaPeriod) msg.obj);
+ 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;
+ default:
+ return false;
+ }
+ maybeNotifyPlaybackInfoChanged();
+ } catch (ExoPlaybackException e) {
+ 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);
+ 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 (playbackInfo.playbackState != state) {
+ playbackInfo = playbackInfo.copyWithPlaybackState(state);
+ }
+ }
+
+ 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, boolean resetState) {
+ pendingPrepareCount++;
+ resetInternal(
+ /* resetRenderers= */ false,
+ /* releaseMediaSource= */ true,
+ resetPosition,
+ resetState,
+ /* resetError= */ true);
+ loadControl.onPrepared();
+ this.mediaSource = mediaSource;
+ setState(Player.STATE_BUFFERING);
+ mediaSource.prepareSource(/* caller= */ this, bandwidthMeter.getTransferListener());
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ }
+
+ private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
+ rebuffering = false;
+ this.playWhenReady = playWhenReady;
+ if (!playWhenReady) {
+ stopRenderers();
+ updatePlaybackPositions();
+ } else {
+ if (playbackInfo.playbackState == Player.STATE_READY) {
+ startRenderers();
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ } 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;
+ mediaClock.start();
+ for (Renderer renderer : enabledRenderers) {
+ renderer.start();
+ }
+ }
+
+ private void stopRenderers() throws ExoPlaybackException {
+ 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 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);
+ }
+ } else {
+ rendererPositionUs =
+ mediaClock.syncAndGetPositionUs(
+ /* isReadingAhead= */ playingPeriodHolder != queue.getReadingPeriod());
+ long periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs);
+ maybeTriggerPendingMessages(playbackInfo.positionUs, periodPositionUs);
+ playbackInfo.positionUs = periodPositionUs;
+ }
+
+ // 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 = 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 until the playing period is available.
+ scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);
+ return;
+ }
+
+ TraceUtil.beginSection("doSomeWork");
+
+ updatePlaybackPositions();
+
+ 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 {
+ playingPeriodHolder.mediaPeriod.maybeThrowPrepareError();
+ }
+
+ 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 && 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);
+ }
+
+ TraceUtil.endSection();
+ }
+
+ private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) {
+ handler.removeMessages(MSG_DO_SOME_WORK);
+ handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs);
+ }
+
+ private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
+ playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
+
+ MediaPeriodId periodId;
+ long periodPositionUs;
+ long contentPositionUs;
+ boolean seekPositionAdjusted;
+ Pair<Object, Long> 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 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;
+ }
+ }
+
+ try {
+ 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;
+ }
+ } finally {
+ playbackInfo = copyWithNewPosition(periodId, periodPositionUs, contentPositionUs);
+ if (seekPositionAdjusted) {
+ playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT);
+ }
+ }
+ }
+
+ 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;
+ if (playbackInfo.playbackState != Player.STATE_IDLE && !playbackInfo.timeline.isEmpty()) {
+ setState(Player.STATE_BUFFERING);
+ }
+
+ // 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) {
+ disableRenderer(renderer);
+ }
+ enabledRenderers = new Renderer[0];
+ oldPlayingPeriodHolder = null;
+ if (newPlayingPeriodHolder != null) {
+ newPlayingPeriodHolder.setRendererOffset(/* rendererPositionOffsetUs= */ 0);
+ }
+ }
+
+ // Update the holders.
+ if (newPlayingPeriodHolder != null) {
+ updatePlayingPeriodRenderers(oldPlayingPeriodHolder);
+ if (newPlayingPeriodHolder.hasEnabledTracks) {
+ periodPositionUs = newPlayingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs);
+ newPlayingPeriodHolder.mediaPeriod.discardBuffer(
+ periodPositionUs - backBufferDurationUs, retainBackBufferFromKeyframe);
+ }
+ resetRendererPosition(periodPositionUs);
+ maybeContinueLoading();
+ } else {
+ 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 {
+ 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) {
+ mediaClock.setPlaybackParameters(playbackParameters);
+ sendPlaybackParametersChangedInternal(
+ mediaClock.getPlaybackParameters(), /* acknowledgeCommand= */ 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(Player.STATE_IDLE);
+ }
+
+ private void releaseInternal() {
+ resetInternal(
+ /* resetRenderers= */ true,
+ /* releaseMediaSource= */ true,
+ /* resetPosition= */ true,
+ /* resetState= */ true,
+ /* resetError= */ false);
+ loadControl.onReleased();
+ setState(Player.STATE_IDLE);
+ internalPlaybackThread.quit();
+ synchronized (this) {
+ released = true;
+ notifyAll();
+ }
+ }
+
+ private void resetInternal(
+ boolean resetRenderers,
+ boolean releaseMediaSource,
+ boolean resetPosition,
+ boolean resetState,
+ boolean resetError) {
+ handler.removeMessages(MSG_DO_SOME_WORK);
+ rebuffering = false;
+ mediaClock.stop();
+ rendererPositionUs = 0;
+ for (Renderer renderer : enabledRenderers) {
+ try {
+ disableRenderer(renderer);
+ } catch (ExoPlaybackException | RuntimeException e) {
+ // There's nothing we can do.
+ 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];
+
+ 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(/* caller= */ this);
+ mediaSource = null;
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+
+ 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 {
+ 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<Object, Long> 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 {
+ if (renderer.getState() == Renderer.STATE_STARTED) {
+ renderer.stop();
+ }
+ }
+
+ private void disableRenderer(Renderer renderer) throws ExoPlaybackException {
+ mediaClock.onRendererDisabled(renderer);
+ ensureStopped(renderer);
+ renderer.disable();
+ }
+
+ private void reselectTracksInternal() throws ExoPlaybackException {
+ float playbackSpeed = mediaClock.getPlaybackParameters().speed;
+ // Reselect tracks on each period in turn, until the selection changes.
+ 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;
+ }
+ newTrackSelectorResult = periodHolder.selectTracks(playbackSpeed, playbackInfo.timeline);
+ if (!newTrackSelectorResult.isEquivalent(periodHolder.getTrackSelectorResult())) {
+ // Selected tracks have changed for this period.
+ break;
+ }
+ if (periodHolder == readingPeriodHolder) {
+ // The track reselection didn't affect any period that has been read.
+ selectionsChangedForReadPeriod = false;
+ }
+ periodHolder = periodHolder.getNext();
+ }
+
+ if (selectionsChangedForReadPeriod) {
+ // Update streams and rebuffer for the new selection, recreating all streams if reading ahead.
+ MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
+ boolean recreateStreams = queue.removeAfter(playingPeriodHolder);
+
+ boolean[] streamResetFlags = new boolean[renderers.length];
+ 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);
+ }
+
+ 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;
+ SampleStream sampleStream = playingPeriodHolder.sampleStreams[i];
+ if (sampleStream != null) {
+ enabledRendererCount++;
+ }
+ if (rendererWasEnabledFlags[i]) {
+ if (sampleStream != renderer.getStream()) {
+ // We need to disable the renderer.
+ disableRenderer(renderer);
+ } else if (streamResetFlags[i]) {
+ // The renderer will continue to consume from its current stream, but needs to be reset.
+ renderer.resetPosition(rendererPositionUs);
+ }
+ }
+ }
+ playbackInfo =
+ playbackInfo.copyWithTrackInfo(
+ playingPeriodHolder.getTrackGroups(), playingPeriodHolder.getTrackSelectorResult());
+ enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
+ } else {
+ // Release and re-prepare/buffer periods after the one whose selection changed.
+ queue.removeAfter(periodHolder);
+ if (periodHolder.prepared) {
+ long loadingPeriodPositionUs =
+ Math.max(
+ periodHolder.info.startPositionUs, periodHolder.toPeriodTime(rendererPositionUs));
+ periodHolder.applyTrackSelection(newTrackSelectorResult, loadingPeriodPositionUs, false);
+ }
+ }
+ handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ true);
+ if (playbackInfo.playbackState != Player.STATE_ENDED) {
+ maybeContinueLoading();
+ updatePlaybackPositions();
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ }
+
+ private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo)
+ throws ExoPlaybackException {
+ if (sourceRefreshInfo.source != mediaSource) {
+ // Stale event.
+ return;
+ }
+ playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount);
+ pendingPrepareCount = 0;
+
+ 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<Object, Long> 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;
+ }
+ 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<Object, Long> 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<Object, Long> 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.
+ long newPositionUs = newPeriodId.isAd() ? 0 : newContentPositionUs;
+ long seekedToPositionUs = seekToPeriodPosition(newPeriodId, newPositionUs);
+ playbackInfo = copyWithNewPosition(newPeriodId, seekedToPositionUs, newContentPositionUs);
+ }
+ handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
+ }
+
+ private long getMaxRendererReadPositionUs() {
+ MediaPeriodHolder readingHolder = queue.getReadingPeriod();
+ if (readingHolder == null) {
+ return 0;
+ }
+ 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 {
+ maxReadPositionUs = Math.max(readingPositionUs, maxReadPositionUs);
+ }
+ }
+ return maxReadPositionUs;
+ }
+
+ 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(
+ /* 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 uid of this period in the new timeline is returned.
+ *
+ * @param oldPeriodUid The index of the period in the old timeline.
+ * @param oldTimeline The old timeline.
+ * @param newTimeline The new timeline.
+ * @return The uid in the new timeline of the first subsequent period, or null if no such period
+ * was found.
+ */
+ private @Nullable Object resolveSubsequentPeriod(
+ Object oldPeriodUid, Timeline oldTimeline, Timeline newTimeline) {
+ int oldPeriodIndex = oldTimeline.getIndexOfPeriod(oldPeriodUid);
+ int newPeriodIndex = C.INDEX_UNSET;
+ 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 == C.INDEX_UNSET ? null : newTimeline.getUidOfPeriod(newPeriodIndex);
+ }
+
+ /**
+ * 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.
+ */
+ @Nullable
+ private Pair<Object, Long> 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 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<Object, Long> periodPosition;
+ try {
+ 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.
+ 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(periodPosition.first);
+ if (periodIndex != C.INDEX_UNSET) {
+ // We successfully located the period in the internal timeline.
+ return periodPosition;
+ }
+ 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 Timeline#getPeriodPosition(Timeline.Window, Timeline.Period, int, long)} using the
+ * current timeline.
+ */
+ private Pair<Object, Long> getPeriodPosition(
+ Timeline timeline, int windowIndex, long windowPositionUs) {
+ return timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
+ }
+
+ private void updatePeriods() throws ExoPlaybackException, IOException {
+ 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;
+ }
+ maybeUpdateLoadingPeriod();
+ 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();
+ }
+ }
+
+ private void maybeUpdateReadingPeriod() throws ExoPlaybackException {
+ MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
+ if (readingPeriodHolder == null) {
+ return;
+ }
+
+ 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())) {
+ // The current reading period is still being read by at least one renderer.
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void setAllRendererStreamsFinal() {
+ for (Renderer renderer : renderers) {
+ if (renderer.getStream() != null) {
+ renderer.setCurrentStreamFinal();
+ }
+ }
+ }
+
+ private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackException {
+ if (!queue.isLoading(mediaPeriod)) {
+ // Stale event.
+ return;
+ }
+ 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 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() {
+ 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) {
+ 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 PlaybackInfo copyWithNewPosition(
+ MediaPeriodId mediaPeriodId, long positionUs, long contentPositionUs) {
+ deliverPendingMessageAtStartPositionRequired = true;
+ return playbackInfo.copyWithNewPosition(
+ mediaPeriodId, positionUs, contentPositionUs, getTotalBufferedDurationUs());
+ }
+
+ @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;
+ if (newPlayingPeriodHolder.getTrackSelectorResult().isRendererEnabled(i)) {
+ enabledRendererCount++;
+ }
+ 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.
+ disableRenderer(renderer);
+ }
+ }
+ playbackInfo =
+ playbackInfo.copyWithTrackInfo(
+ newPlayingPeriodHolder.getTrackGroups(),
+ newPlayingPeriodHolder.getTrackSelectorResult());
+ enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
+ }
+
+ private void enableRenderers(boolean[] rendererWasEnabledFlags, int totalEnabledRendererCount)
+ throws ExoPlaybackException {
+ 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++) {
+ 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++);
+ }
+ }
+ }
+
+ 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 {
+
+ public final Timeline timeline;
+ public final int windowIndex;
+ public final long windowPositionUs;
+
+ public SeekPosition(Timeline timeline, int windowIndex, long windowPositionUs) {
+ this.timeline = timeline;
+ this.windowIndex = windowIndex;
+ this.windowPositionUs = windowPositionUs;
+ }
+ }
+
+ private static final class PendingMessageInfo implements Comparable<PendingMessageInfo> {
+
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
new file mode 100644
index 0000000000..545017a215
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
@@ -0,0 +1,86 @@
+/*
+ * 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 java.util.HashSet;
+
+/**
+ * Information about the ExoPlayer library.
+ */
+public final class ExoPlayerLibraryInfo {
+
+ /**
+ * 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.
+ public static final String VERSION = "2.11.4";
+
+ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
+ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
+ public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.4";
+
+ /**
+ * The version of the library expressed as an integer, for example 1002003.
+ *
+ * <p>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.
+ 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.
+ */
+ 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.
+ */
+ public static final boolean TRACE_ENABLED = true;
+
+ private static final HashSet<String> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Format.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Format.java
new file mode 100644
index 0000000000..9d7518f6f0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Format.java
@@ -0,0 +1,1750 @@
+/*
+ * 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 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.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Representation of a media format.
+ */
+public final class Format implements Parcelable {
+
+ /**
+ * A value for various fields to indicate that the field's value is unknown or not applicable.
+ */
+ public static final int NO_VALUE = -1;
+
+ /**
+ * A value for {@link #subsampleOffsetUs} to indicate that subsample timestamps are relative to
+ * the timestamps of their parent samples.
+ */
+ public static final long OFFSET_SAMPLE_RELATIVE = Long.MAX_VALUE;
+
+ /** 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. */
+ @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. */
+ @Nullable public final String containerMimeType;
+
+ // Elementary stream specific.
+
+ /**
+ * The mime type of the elementary stream (i.e. the individual samples), or null if unknown or not
+ * applicable.
+ */
+ @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.
+ */
+ public final int maxInputSize;
+ /**
+ * Initialization data that must be provided to the decoder. Will not be null, but may be empty
+ * if initialization data is not required.
+ */
+ public final List<byte[]> initializationData;
+ /** DRM initialization data if the stream is protected, or null otherwise. */
+ @Nullable public final DrmInitData drmInitData;
+
+ /**
+ * 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;
+
+ // Video specific.
+
+ /**
+ * The width of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable.
+ */
+ public final int width;
+ /**
+ * The height of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable.
+ */
+ public final int height;
+ /**
+ * The frame rate in frames per second, or {@link #NO_VALUE} if unknown or not applicable.
+ */
+ public final float frameRate;
+ /**
+ * The clockwise rotation that should be applied to the video for it to be rendered in the correct
+ * 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 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
+ * modes are {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link
+ * C#STEREO_MODE_LEFT_RIGHT}, {@link C#STEREO_MODE_STEREO_MESH}.
+ */
+ @C.StereoMode
+ public final int stereoMode;
+ /** 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.
+
+ /**
+ * The number of audio channels, or {@link #NO_VALUE} if unknown or not applicable.
+ */
+ public final int channelCount;
+ /**
+ * 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 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 frames to trim from the end of the decoded audio stream, or 0 if not applicable.
+ */
+ public final int encoderPadding;
+
+ // Audio and text specific.
+
+ /** 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<? extends ExoMediaCrypto> exoMediaCryptoType;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ // Video.
+
+ /**
+ * @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<byte[]> 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 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<byte[]> 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(
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ int maxInputSize,
+ int width,
+ int height,
+ float frameRate,
+ @Nullable List<byte[]> 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(
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ int maxInputSize,
+ int width,
+ int height,
+ float frameRate,
+ @Nullable List<byte[]> 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<byte[]> 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.
+
+ /**
+ * @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<byte[]> 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 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<byte[]> 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(
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ int maxInputSize,
+ int channelCount,
+ int sampleRate,
+ @Nullable List<byte[]> 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(
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ int maxInputSize,
+ int channelCount,
+ int sampleRate,
+ @C.PcmEncoding int pcmEncoding,
+ @Nullable List<byte[]> 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<byte[]> 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(
+ @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(
+ @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(
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable String language) {
+ return createTextSampleFormat(id, sampleMimeType, selectionFlags, language, null);
+ }
+
+ 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(
+ @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,
+ /* accessibilityChannel= */ NO_VALUE,
+ drmInitData,
+ subsampleOffsetUs,
+ Collections.emptyList());
+ }
+
+ 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<byte[]> 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(
+ @Nullable String id,
+ @Nullable String sampleMimeType,
+ @Nullable String codecs,
+ int bitrate,
+ @C.SelectionFlags int selectionFlags,
+ @Nullable List<byte[]> 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.
+
+ /**
+ * @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 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(
+ @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);
+ }
+
+ 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<byte[]> 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<? extends ExoMediaCrypto> exoMediaCryptoType) {
+ this.id = id;
+ 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 == 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 == 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;
+ // Provided by source.
+ this.exoMediaCryptoType = exoMediaCryptoType;
+ }
+
+ @SuppressWarnings("ResourceType")
+ /* package */ Format(Parcel in) {
+ id = in.readString();
+ label = in.readString();
+ selectionFlags = in.readInt();
+ 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());
+ 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,
+ 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,
+ 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 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")
+ public Format copyWithManifestFormatInfo(Format manifestFormat) {
+ if (this == manifestFormat) {
+ // No need to copy from ourselves.
+ return this;
+ }
+
+ int trackType = MimeTypes.getTrackType(sampleMimeType);
+
+ // Use manifest value only.
+ String id = manifestFormat.id;
+
+ // 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;
+ 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,
+ 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 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 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<? extends ExoMediaCrypto> 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);
+ }
+
+ /**
+ * Returns the number of pixels if this is a video format whose {@link #width} and {@link #height}
+ * are known, or {@link #NO_VALUE} otherwise
+ */
+ public int getPixelCount() {
+ return width == NO_VALUE || height == NO_VALUE ? NO_VALUE : (width * height);
+ }
+
+ @Override
+ public String toString() {
+ 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 + (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;
+ // Provided by source.
+ result = 31 * result + (exoMediaCryptoType == null ? 0 : exoMediaCryptoType.hashCode());
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ Format other = (Format) obj;
+ 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++) {
+ if (!Arrays.equals(initializationData.get(i), other.initializationData.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // Utility methods
+
+ /** Returns a prettier {@link String} than {@link #toString()}, intended for logging. */
+ public static String toLogString(@Nullable Format format) {
+ if (format == null) {
+ return "null";
+ }
+ StringBuilder builder = new StringBuilder();
+ builder.append("id=").append(format.id).append(", mimeType=").append(format.sampleMimeType);
+ 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);
+ }
+ if (format.frameRate != Format.NO_VALUE) {
+ builder.append(", fps=").append(format.frameRate);
+ }
+ if (format.channelCount != Format.NO_VALUE) {
+ builder.append(", channels=").append(format.channelCount);
+ }
+ if (format.sampleRate != Format.NO_VALUE) {
+ builder.append(", sample_rate=").append(format.sampleRate);
+ }
+ if (format.language != null) {
+ builder.append(", language=").append(format.language);
+ }
+ if (format.label != null) {
+ builder.append(", label=").append(format.label);
+ }
+ return builder.toString();
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeString(label);
+ dest.writeInt(selectionFlags);
+ 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.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<Format> CREATOR = new Creator<Format>() {
+
+ @Override
+ public Format createFromParcel(Parcel in) {
+ return new Format(in);
+ }
+
+ @Override
+ public Format[] newArray(int size) {
+ return new Format[size];
+ }
+
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/FormatHolder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/FormatHolder.java
new file mode 100644
index 0000000000..35e87f1271
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/FormatHolder.java
@@ -0,0 +1,43 @@
+/*
+ * 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.drm.DrmSession;
+
+/**
+ * Holds a {@link Format}.
+ */
+public final class FormatHolder {
+
+ /** 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/IllegalSeekPositionException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/IllegalSeekPositionException.java
new file mode 100644
index 0000000000..fd1423fc90
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/IllegalSeekPositionException.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/**
+ * Thrown when an attempt is made to seek to a position that does not exist in the player's
+ * {@link Timeline}.
+ */
+public final class IllegalSeekPositionException extends IllegalStateException {
+
+ /**
+ * The {@link Timeline} in which the seek was attempted.
+ */
+ public final Timeline timeline;
+ /**
+ * The index of the window being seeked to.
+ */
+ public final int windowIndex;
+ /**
+ * The seek position in the specified window.
+ */
+ public final long positionMs;
+
+ /**
+ * @param timeline The {@link Timeline} in which the seek was attempted.
+ * @param windowIndex The index of the window being seeked to.
+ * @param positionMs The seek position in the specified window.
+ */
+ public IllegalSeekPositionException(Timeline timeline, int windowIndex, long positionMs) {
+ this.timeline = timeline;
+ this.windowIndex = windowIndex;
+ this.positionMs = positionMs;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/LoadControl.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/LoadControl.java
new file mode 100644
index 0000000000..5076018d65
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/LoadControl.java
@@ -0,0 +1,113 @@
+/*
+ * 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 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.TrackSelectionArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+
+/**
+ * Controls buffering of media.
+ */
+public interface LoadControl {
+
+ /**
+ * Called by the player when prepared with a new source.
+ */
+ void onPrepared();
+
+ /**
+ * Called by the player when a track selection occurs.
+ *
+ * @param renderers The renderers.
+ * @param trackGroups The {@link TrackGroup}s from which the selection was made.
+ * @param trackSelections The track selections that were made.
+ */
+ void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
+ TrackSelectionArray trackSelections);
+
+ /**
+ * Called by the player when stopped.
+ */
+ void onStopped();
+
+ /**
+ * Called by the player when released.
+ */
+ void onReleased();
+
+ /**
+ * Returns the {@link Allocator} that should be used to obtain media buffer allocations.
+ */
+ Allocator getAllocator();
+
+ /**
+ * Returns the duration of media to retain in the buffer prior to the current playback position,
+ * for fast backward seeking.
+ * <p>
+ * 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.
+ * <p>
+ * Note: Implementations should return a single value. Dynamic changes to the back-buffer are not
+ * currently supported.
+ *
+ * @return The duration of media to retain in the buffer prior to the current playback position,
+ * in microseconds.
+ */
+ 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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, 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodHolder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodHolder.java
new file mode 100644
index 0000000000..66cb9a1fce
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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}.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodInfo.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodInfo.java
new file mode 100644
index 0000000000..b240fe0f91
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodQueue.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodQueue.java
new file mode 100644
index 0000000000..941fb61848
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<Object, Long> 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<Object, Long> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/NoSampleRenderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/NoSampleRenderer.java
new file mode 100644
index 0000000000..c4662f1544
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * The default implementation is a no-op.
+ */
+ protected void onDisabled() {
+ // Do nothing.
+ }
+
+ /**
+ * Called when the renderer is reset.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ParserException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ParserException.java
new file mode 100644
index 0000000000..abbe6e8fee
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ParserException.java
@@ -0,0 +1,51 @@
+/*
+ * 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 java.io.IOException;
+
+/**
+ * Thrown when an error occurs parsing media data and metadata.
+ */
+public class ParserException extends IOException {
+
+ public ParserException() {
+ super();
+ }
+
+ /**
+ * @param message The detail message for the exception.
+ */
+ public ParserException(String message) {
+ super(message);
+ }
+
+ /**
+ * @param cause The cause for the exception.
+ */
+ public ParserException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * @param message The detail message for the exception.
+ * @param cause The cause for the exception.
+ */
+ public ParserException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackInfo.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackInfo.java
new file mode 100644
index 0000000000..c743e35661
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackParameters.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackParameters.java
new file mode 100644
index 0000000000..fd47117aba
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackParameters.java
@@ -0,0 +1,113 @@
+/*
+ * 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;
+
+/**
+ * The parameters that apply to playback.
+ */
+public final class PlaybackParameters {
+
+ /**
+ * The default playback parameters: real-time playback with no pitch modification or silence
+ * skipping.
+ */
+ public static final PlaybackParameters DEFAULT = new PlaybackParameters(/* speed= */ 1f);
+
+ /** The factor by which playback will be sped up. */
+ public final float speed;
+
+ /** 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 that set the playback speed.
+ *
+ * @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);
+ }
+
+ /**
+ * 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 getMediaTimeUsForPlayoutTimeMs(long timeMs) {
+ return timeMs * scaledUsPerMs;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ PlaybackParameters other = (PlaybackParameters) obj;
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackPreparer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlaybackPreparer.java
new file mode 100644
index 0000000000..831a28aa47
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Player.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Player.java
new file mode 100644
index 0000000000..89059dc2ea
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ * <p>
+ * Some important properties of media players that implement this interface are:
+ * <ul>
+ * <li>They can provide a {@link Timeline} representing the structure of the media being played,
+ * which can be obtained by calling {@link #getCurrentTimeline()}.</li>
+ * <li>They can provide a {@link TrackGroupArray} defining the currently available tracks,
+ * which can be obtained by calling {@link #getCurrentTrackGroups()}.</li>
+ * <li>They contain a number of renderers, each of which is able to render tracks of a single
+ * type (e.g. audio, video or text). The number of renderers and their respective track types
+ * can be obtained by calling {@link #getRendererCount()} and {@link #getRendererType(int)}.
+ * </li>
+ * <li>They can provide a {@link TrackSelectionArray} defining which of the currently available
+ * tracks are selected to be rendered by each renderer. This can be obtained by calling
+ * {@link #getCurrentTrackSelections()}}.</li>
+ * </ul>
+ */
+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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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)}.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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 <em>not</em> 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.
+ *
+ * <p>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 <em>not</em> 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).
+ *
+ * <p>When a position discontinuity occurs as a result of a change to the timeline this method
+ * is <em>not</em> 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.
+ *
+ * <p>If {@code false}, then at least one of the following is true:
+ *
+ * <ul>
+ * <li>The {@link #getPlaybackState() playback state} is not {@link #STATE_READY ready}.
+ * <li>There is no {@link #getPlayWhenReady() intention to play}.
+ * <li>Playback is {@link #getPlaybackSuppressionReason() suppressed for other reasons}.
+ * </ul>
+ *
+ * @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.
+ *
+ * <p>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}.
+ * <p>
+ * 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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlayerMessage.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/PlayerMessage.java
new file mode 100644
index 0000000000..69740220e5
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Renderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Renderer.java
new file mode 100644
index 0000000000..d06afb5d3c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Renderer.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.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}.
+ *
+ * <p>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.
+ *
+ * <p style="align:center"><img src="doc-files/renderer-states.svg" alt="Renderer state
+ * transitions">
+ */
+public interface Renderer extends PlayerMessage.Target {
+
+ /**
+ * 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 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;
+ /**
+ * The renderer is started. Calls to {@link #render(long, long)} will cause media to be rendered.
+ */
+ int STATE_STARTED = 2;
+
+ /**
+ * Returns the track type that the {@link Renderer} handles. For example, a video renderer will
+ * return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a
+ * text renderer will return {@link C#TRACK_TYPE_TEXT}, and so on.
+ *
+ * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
+ */
+ int getTrackType();
+
+ /**
+ * Returns the capabilities of the renderer.
+ *
+ * @return The capabilities of the renderer.
+ */
+ RendererCapabilities getCapabilities();
+
+ /**
+ * Sets the index of this renderer within the player.
+ *
+ * @param index The renderer index.
+ */
+ void setIndex(int index);
+
+ /**
+ * 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.
+ *
+ * @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 {@link #STATE_DISABLED}, {@link #STATE_ENABLED} and {@link
+ * #STATE_STARTED}.
+ */
+ @State
+ int getState();
+
+ /**
+ * Enables the renderer to consume from the specified {@link SampleStream}.
+ * <p>
+ * 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.
+ * @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 to be added to timestamps of buffers read from {@code stream}
+ * before they are rendered.
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ void enable(RendererConfiguration configuration, Format[] formats, SampleStream stream,
+ long positionUs, boolean joining, long offsetUs) throws ExoPlaybackException;
+
+ /**
+ * Starts the renderer, meaning that calls to {@link #render(long, long)} will cause media to be
+ * rendered.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}.
+ *
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ void start() throws ExoPlaybackException;
+
+ /**
+ * Replaces the {@link SampleStream} from which samples will be consumed.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+ *
+ * @param formats The enabled formats.
+ * @param stream The {@link SampleStream} from which the renderer should consume.
+ * @param offsetUs The offset to be added to timestamps of buffers read from {@code stream} before
+ * they are rendered.
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ void replaceStream(Format[] formats, SampleStream stream, long offsetUs)
+ throws ExoPlaybackException;
+
+ /** Returns the {@link SampleStream} being consumed, or null if the renderer is disabled. */
+ @Nullable
+ SampleStream getStream();
+
+ /**
+ * Returns whether the renderer has read the current {@link SampleStream} to the end.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+ */
+ 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.
+ *
+ * <p>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.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+ */
+ void setCurrentStreamFinal();
+
+ /**
+ * Returns whether the current {@link SampleStream} will be the final one supplied before the
+ * renderer is next disabled or reset.
+ */
+ boolean isCurrentStreamFinal();
+
+ /**
+ * Throws an error that's preventing the renderer from reading from its {@link SampleStream}. Does
+ * nothing if no such error exists.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+ *
+ * @throws IOException An error that's preventing the renderer from making progress or buffering
+ * more data.
+ */
+ void maybeThrowStreamError() throws IOException;
+
+ /**
+ * Signals to the renderer that a position discontinuity has occurred.
+ * <p>
+ * After a position discontinuity, the renderer's {@link SampleStream} is guaranteed to provide
+ * samples starting from a key frame.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+ *
+ * @param positionUs The new playback position in microseconds.
+ * @throws ExoPlaybackException If an error occurs handling the reset.
+ */
+ 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.
+ *
+ * <p>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}.
+ * <p>
+ * If the renderer is in the {@link #STATE_ENABLED} state then each call to this method will do
+ * work toward being ready to render the {@link SampleStream} when the renderer is started. It may
+ * also render the very start of the media, for example the first frame of a video stream. If the
+ * renderer is in the {@link #STATE_STARTED} state then calls to this method will render the
+ * {@link SampleStream} in sync with the specified media positions.
+ * <p>
+ * This method should return quickly, and should not block if the renderer is unable to make
+ * useful progress.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+ *
+ * @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.
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException;
+
+ /**
+ * Whether the renderer is able to immediately render media from the current position.
+ * <p>
+ * If the renderer is in the {@link #STATE_STARTED} state then returning true indicates that the
+ * renderer has everything that it needs to continue playback. Returning false indicates that
+ * the player should pause until the renderer is ready.
+ * <p>
+ * If the renderer is in the {@link #STATE_ENABLED} state then returning true indicates that the
+ * renderer is ready for playback to be started. Returning false indicates that it is not.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+ *
+ * @return Whether the renderer is ready to render media.
+ */
+ boolean isReady();
+
+ /**
+ * Whether the renderer is ready for the {@link ExoPlayer} instance to transition to
+ * {@link Player#STATE_ENDED}. The player will make this transition as soon as {@code true} is
+ * returned by all of its {@link Renderer}s.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+ *
+ * @return Whether the renderer is ready for the player to transition to the ended state.
+ */
+ boolean isEnded();
+
+ /**
+ * Stops the renderer, transitioning it to the {@link #STATE_ENABLED} state.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_STARTED}.
+ *
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ void stop() throws ExoPlaybackException;
+
+ /**
+ * Disable the renderer, transitioning it to the {@link #STATE_DISABLED} state.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}.
+ */
+ 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.
+ *
+ * <p>This method may be called when the renderer is in the following states: {@link
+ * #STATE_DISABLED}.
+ */
+ void reset();
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RendererCapabilities.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RendererCapabilities.java
new file mode 100644
index 0000000000..6f34afc7b8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RendererCapabilities.java
@@ -0,0 +1,293 @@
+/*
+ * 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 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}.
+ */
+public interface RendererCapabilities {
+
+ /**
+ * 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}.
+ */
+ @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 = 0b100;
+ /**
+ * The {@link Renderer} is capable of rendering formats with the same mime type, but the
+ * 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.
+ * <p>
+ * 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 = 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.
+ * <p>
+ * 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
+ * the sub-type is not supported.
+ * <p>
+ * 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 = 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
+ * mime type.
+ * <p>
+ * Example: The {@link Renderer} is a general purpose video renderer, but the format has an
+ * audio mime type.
+ */
+ int FORMAT_UNSUPPORTED_TYPE = 0b000;
+
+ /**
+ * Level of renderer support for adaptive format switches. One of {@link #ADAPTIVE_SEAMLESS},
+ * {@link #ADAPTIVE_NOT_SEAMLESS} or {@link #ADAPTIVE_NOT_SUPPORTED}.
+ */
+ @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 = 0b10000;
+ /**
+ * The {@link Renderer} can adapt between formats, but may suffer a brief discontinuity
+ * (~50-100ms) when adaptation occurs.
+ */
+ int ADAPTIVE_NOT_SEAMLESS = 0b01000;
+ /**
+ * The {@link Renderer} does not support adaptation between formats.
+ */
+ int ADAPTIVE_NOT_SUPPORTED = 0b00000;
+
+ /**
+ * Level of renderer support for tunneling. One of {@link #TUNNELING_SUPPORTED} or {@link
+ * #TUNNELING_NOT_SUPPORTED}.
+ */
+ @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 = 0b100000;
+ /**
+ * The {@link Renderer} does not support tunneled output.
+ */
+ int TUNNELING_NOT_SUPPORTED = 0b000000;
+
+ /**
+ * Combined renderer capabilities.
+ *
+ * <p>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.
+ *
+ * <p>Possible values:
+ *
+ * <ul>
+ * <li>{@link FormatSupport}: The level of support for the format itself. One of {@link
+ * #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM},
+ * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}.
+ * <li>{@link AdaptiveSupport}: The level of support for adapting from the format to another
+ * format of the same mime type. One of {@link #ADAPTIVE_SEAMLESS}, {@link
+ * #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}. Only set if the level of
+ * support for the format itself is {@link #FORMAT_HANDLED} or {@link
+ * #FORMAT_EXCEEDS_CAPABILITIES}.
+ * <li>{@link TunnelingSupport}: The level of support for tunneling. One of {@link
+ * #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of
+ * support for the format itself is {@link #FORMAT_HANDLED} or {@link
+ * #FORMAT_EXCEEDS_CAPABILITIES}.
+ * </ul>
+ */
+ @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}.
+ *
+ * <p>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
+ * return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a
+ * text renderer will return {@link C#TRACK_TYPE_TEXT}, and so on.
+ *
+ * @see Renderer#getTrackType()
+ * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
+ */
+ int getTrackType();
+
+ /**
+ * Returns the extent to which the {@link Renderer} supports a given format.
+ *
+ * @param format The 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.
+ *
+ * @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RendererConfiguration.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RendererConfiguration.java
new file mode 100644
index 0000000000..d12e2b9fb6
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RendererConfiguration.java
@@ -0,0 +1,62 @@
+/*
+ * 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;
+
+/**
+ * The configuration of a {@link Renderer}.
+ */
+public final class RendererConfiguration {
+
+ /**
+ * The default configuration.
+ */
+ public static final RendererConfiguration DEFAULT =
+ new RendererConfiguration(C.AUDIO_SESSION_ID_UNSET);
+
+ /**
+ * The audio session id to use for tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling
+ * should not be enabled.
+ */
+ public final int tunnelingAudioSessionId;
+
+ /**
+ * @param tunnelingAudioSessionId The audio session id to use for tunneling, or
+ * {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled.
+ */
+ public RendererConfiguration(int tunnelingAudioSessionId) {
+ this.tunnelingAudioSessionId = tunnelingAudioSessionId;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ RendererConfiguration other = (RendererConfiguration) obj;
+ return tunnelingAudioSessionId == other.tunnelingAudioSessionId;
+ }
+
+ @Override
+ public int hashCode() {
+ return tunnelingAudioSessionId;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RenderersFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RenderersFactory.java
new file mode 100644
index 0000000000..ed46d27fa3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/RenderersFactory.java
@@ -0,0 +1,50 @@
+/*
+ * 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 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.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;
+
+/**
+ * Builds {@link Renderer} instances for use by a {@link SimpleExoPlayer}.
+ */
+public interface RenderersFactory {
+
+ /**
+ * Builds the {@link Renderer} instances for a {@link SimpleExoPlayer}.
+ *
+ * @param eventHandler A handler to use when invoking event listeners and outputs.
+ * @param videoRendererEventListener An event listener for video 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,
+ VideoRendererEventListener videoRendererEventListener,
+ AudioRendererEventListener audioRendererEventListener,
+ TextOutput textRendererOutput,
+ MetadataOutput metadataRendererOutput,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SeekParameters.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SeekParameters.java
new file mode 100644
index 0000000000..03c1d0165d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SimpleExoPlayer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SimpleExoPlayer.java
new file mode 100644
index 0000000000..7b632ed051
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SimpleExoPlayer.java
@@ -0,0 +1,1845 @@
+/*
+ * 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 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.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.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.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 SimpleExoPlayer.Builder}.
+ */
+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 builder for {@link SimpleExoPlayer} instances.
+ *
+ * <p>See {@link #Builder(Context)} for the list of default values.
+ */
+ 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;
+
+ /**
+ * Creates a builder.
+ *
+ * <p>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.
+ *
+ * <p>The builder uses the following default values:
+ *
+ * <ul>
+ * <li>{@link RenderersFactory}: {@link DefaultRenderersFactory}
+ * <li>{@link TrackSelector}: {@link DefaultTrackSelector}
+ * <li>{@link LoadControl}: {@link DefaultLoadControl}
+ * <li>{@link BandwidthMeter}: {@link DefaultBandwidthMeter#getSingletonInstance(Context)}
+ * <li>{@link Looper}: 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}
+ * <li>{@link AnalyticsCollector}: {@link AnalyticsCollector} with {@link Clock#DEFAULT}
+ * <li>{@code useLazyPreparation}: {@code true}
+ * <li>{@link Clock}: {@link Clock#DEFAULT}
+ * </ul>
+ *
+ * @param context A {@link Context}.
+ */
+ public Builder(Context context) {
+ this(context, new DefaultRenderersFactory(context));
+ }
+
+ /**
+ * Creates a builder with a custom {@link RenderersFactory}.
+ *
+ * <p>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.
+ */
+ 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.
+ *
+ * <p>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.
+ *
+ * <p>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 ExoPlayerImpl player;
+ private final Handler eventHandler;
+ private final ComponentListener componentListener;
+ private final CopyOnWriteArraySet<org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener>
+ videoListeners;
+ private final CopyOnWriteArraySet<AudioListener> audioListeners;
+ private final CopyOnWriteArraySet<TextOutput> textOutputs;
+ private final CopyOnWriteArraySet<MetadataOutput> metadataOutputs;
+ private final CopyOnWriteArraySet<VideoRendererEventListener> videoDebugListeners;
+ private final CopyOnWriteArraySet<AudioRendererEventListener> audioDebugListeners;
+ private final BandwidthMeter bandwidthMeter;
+ private final AnalyticsCollector analyticsCollector;
+
+ private final AudioBecomingNoisyManager audioBecomingNoisyManager;
+ private final AudioFocusManager audioFocusManager;
+ private final WakeLockManager wakeLockManager;
+ private final WifiLockManager wifiLockManager;
+
+ @Nullable private Format videoFormat;
+ @Nullable private Format audioFormat;
+
+ @Nullable private VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer;
+ @Nullable private Surface surface;
+ private boolean ownsSurface;
+ 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;
+ private AudioAttributes audioAttributes;
+ private float audioVolume;
+ @Nullable private MediaSource mediaSource;
+ private List<Cue> currentCues;
+ @Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
+ @Nullable private CameraMotionListener cameraMotionListener;
+ private boolean hasNotifiedFullWrongThreadWarning;
+ @Nullable private PriorityTaskManager priorityTaskManager;
+ private boolean isPriorityTaskManagerRegistered;
+ private boolean playerReleased;
+
+ /**
+ * @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<FrameworkMediaCrypto> drmSessionManager,
+ BandwidthMeter bandwidthMeter,
+ AnalyticsCollector analyticsCollector,
+ Clock clock,
+ Looper looper) {
+ this.bandwidthMeter = bandwidthMeter;
+ this.analyticsCollector = analyticsCollector;
+ componentListener = new ComponentListener();
+ 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;
+ audioAttributes = AudioAttributes.DEFAULT;
+ videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
+ currentCues = Collections.emptyList();
+
+ // Build the player and associated objects.
+ 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.
+ *
+ * <p>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;
+ for (Renderer renderer : renderers) {
+ if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {
+ player
+ .createMessage(renderer)
+ .setType(C.MSG_SET_SCALING_MODE)
+ .setPayload(videoScalingMode)
+ .send();
+ }
+ }
+ }
+
+ @Override
+ public @C.VideoScalingMode int getVideoScalingMode() {
+ return videoScalingMode;
+ }
+
+ @Override
+ public void clearVideoSurface() {
+ verifyApplicationThread();
+ removeSurfaceCallbacks();
+ setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false);
+ maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
+ }
+
+ @Override
+ public void clearVideoSurface(@Nullable Surface surface) {
+ verifyApplicationThread();
+ if (surface != null && surface == this.surface) {
+ clearVideoSurface();
+ }
+ }
+
+ @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, /* ownsSurface= */ false);
+ maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
+ } else {
+ 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);
+ }
+ }
+ }
+
+ @Override
+ public void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {
+ verifyApplicationThread();
+ if (surfaceHolder != null && surfaceHolder == this.surfaceHolder) {
+ setVideoSurfaceHolder(null);
+ }
+ }
+
+ @Override
+ public void setVideoSurfaceView(@Nullable SurfaceView surfaceView) {
+ setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder());
+ }
+
+ @Override
+ public void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) {
+ clearVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder());
+ }
+
+ @Override
+ public void setVideoTextureView(@Nullable TextureView textureView) {
+ verifyApplicationThread();
+ removeSurfaceCallbacks();
+ if (textureView != null) {
+ clearVideoDecoderOutputBufferRenderer();
+ }
+ this.textureView = textureView;
+ if (textureView == null) {
+ setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true);
+ maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
+ } else {
+ if (textureView.getSurfaceTextureListener() != null) {
+ Log.w(TAG, "Replacing existing SurfaceTextureListener.");
+ }
+ 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());
+ }
+ }
+ }
+
+ @Override
+ public void clearVideoTextureView(@Nullable TextureView textureView) {
+ verifyApplicationThread();
+ if (textureView != null && textureView == this.textureView) {
+ setVideoTextureView(null);
+ }
+ }
+
+ @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);
+ }
+ }
+
+ 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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * @deprecated Use {@link #getAudioAttributes()}.
+ */
+ @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;
+ }
+
+ /**
+ * Adds an {@link AnalyticsListener} to receive analytics events.
+ *
+ * @param listener The listener to be added.
+ */
+ 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 <a
+ * href="https://developer.android.com/guide/topics/media-apps/volume-and-earphones#becoming-noisy">audio
+ * becoming noisy</a> documentation for more information.
+ *
+ * <p>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.
+ *
+ * <p>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;
+ }
+
+ /**
+ * Sets the {@link PlaybackParams} governing audio playback.
+ *
+ * @deprecated Use {@link #setPlaybackParameters(PlaybackParameters)}.
+ * @param params The {@link PlaybackParams}, or null to clear any previously set parameters.
+ */
+ @Deprecated
+ @TargetApi(23)
+ public void setPlaybackParams(@Nullable PlaybackParams params) {
+ PlaybackParameters playbackParameters;
+ if (params != null) {
+ params.allowDefaults();
+ playbackParameters = new PlaybackParameters(params.getSpeed(), params.getPitch());
+ } else {
+ playbackParameters = null;
+ }
+ setPlaybackParameters(playbackParameters);
+ }
+
+ /** 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. */
+ @Nullable
+ public Format getAudioFormat() {
+ return audioFormat;
+ }
+
+ /** 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. */
+ @Nullable
+ public DecoderCounters getAudioDecoderCounters() {
+ return audioDecoderCounters;
+ }
+
+ @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();
+ }
+ }
+ }
+
+ /**
+ * 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) {
+ 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, removing all existing outputs.
+ *
+ * @param output The output.
+ * @deprecated Use {@link #addTextOutput(TextOutput)}.
+ */
+ @Deprecated
+ public void setTextOutput(TextOutput output) {
+ textOutputs.clear();
+ if (output != null) {
+ addTextOutput(output);
+ }
+ }
+
+ /**
+ * 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)}.
+ */
+ @Deprecated
+ public void setMetadataOutput(MetadataOutput output) {
+ metadataOutputs.retainAll(Collections.singleton(analyticsCollector));
+ if (output != null) {
+ addMetadataOutput(output);
+ }
+ }
+
+ /**
+ * Equivalent to {@link #removeMetadataOutput(MetadataOutput)}.
+ *
+ * @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) {
+ videoDebugListeners.retainAll(Collections.singleton(analyticsCollector));
+ if (listener != null) {
+ addVideoDebugListener(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) {
+ 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 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(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) {
+ 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) {
+ 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 seekTo(int windowIndex, long positionMs) {
+ verifyApplicationThread();
+ analyticsCollector.notifySeekStarted();
+ player.seekTo(windowIndex, positionMs);
+ }
+
+ @Override
+ public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
+ verifyApplicationThread();
+ player.setPlaybackParameters(playbackParameters);
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ verifyApplicationThread();
+ return player.getPlaybackParameters();
+ }
+
+ @Override
+ 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) {
+ if (ownsSurface) {
+ surface.release();
+ }
+ 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 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 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 long getTotalBufferedDuration() {
+ verifyApplicationThread();
+ return player.getTotalBufferedDuration();
+ }
+
+ @Override
+ public boolean isPlayingAd() {
+ verifyApplicationThread();
+ return player.isPlayingAd();
+ }
+
+ @Override
+ 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.
+ *
+ * <p>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).
+ *
+ * <p>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.
+ *
+ * <p>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).
+ *
+ * <p>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.
+
+ private void removeSurfaceCallbacks() {
+ if (textureView != null) {
+ if (textureView.getSurfaceTextureListener() != componentListener) {
+ Log.w(TAG, "SurfaceTextureListener already unset or replaced.");
+ } else {
+ textureView.setSurfaceTextureListener(null);
+ }
+ textureView = null;
+ }
+ if (surfaceHolder != null) {
+ surfaceHolder.removeCallback(componentListener);
+ surfaceHolder = null;
+ }
+ }
+
+ 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.
+ List<PlayerMessage> messages = new ArrayList<>();
+ for (Renderer renderer : renderers) {
+ if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {
+ messages.add(
+ player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setPayload(surface).send());
+ }
+ }
+ if (this.surface != null && this.surface != surface) {
+ // 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();
+ }
+ }
+ this.surface = surface;
+ this.ownsSurface = ownsSurface;
+ }
+
+ 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;
+ for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
+ videoDebugListener.onVideoEnabled(counters);
+ }
+ }
+
+ @Override
+ 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;
+ for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
+ videoDebugListener.onVideoInputFormatChanged(format);
+ }
+ }
+
+ @Override
+ public void onDroppedFrames(int count, long elapsed) {
+ for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
+ videoDebugListener.onDroppedFrames(count, elapsed);
+ }
+ }
+
+ @Override
+ 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);
+ }
+ }
+ for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
+ videoDebugListener.onVideoSizeChanged(
+ width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
+ }
+ }
+
+ @Override
+ public void onRenderedFirstFrame(Surface surface) {
+ if (SimpleExoPlayer.this.surface == surface) {
+ for (org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) {
+ videoListener.onRenderedFirstFrame();
+ }
+ }
+ for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
+ videoDebugListener.onRenderedFirstFrame(surface);
+ }
+ }
+
+ @Override
+ public void onVideoDisabled(DecoderCounters counters) {
+ for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
+ videoDebugListener.onVideoDisabled(counters);
+ }
+ videoFormat = null;
+ videoDecoderCounters = null;
+ }
+
+ // AudioRendererEventListener implementation
+
+ @Override
+ public void onAudioEnabled(DecoderCounters counters) {
+ audioDecoderCounters = counters;
+ for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
+ audioDebugListener.onAudioEnabled(counters);
+ }
+ }
+
+ @Override
+ public void onAudioSessionId(int sessionId) {
+ if (audioSessionId == sessionId) {
+ return;
+ }
+ audioSessionId = sessionId;
+ 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) {
+ for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
+ audioDebugListener.onAudioDecoderInitialized(
+ decoderName, initializedTimestampMs, initializationDurationMs);
+ }
+ }
+
+ @Override
+ public void onAudioInputFormatChanged(Format format) {
+ audioFormat = format;
+ for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
+ audioDebugListener.onAudioInputFormatChanged(format);
+ }
+ }
+
+ @Override
+ public void onAudioSinkUnderrun(
+ int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
+ for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
+ audioDebugListener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+ }
+ }
+
+ @Override
+ public void onAudioDisabled(DecoderCounters counters) {
+ for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
+ audioDebugListener.onAudioDisabled(counters);
+ }
+ audioFormat = null;
+ audioDecoderCounters = null;
+ audioSessionId = C.AUDIO_SESSION_ID_UNSET;
+ }
+
+ // TextOutput implementation
+
+ @Override
+ public void onCues(List<Cue> cues) {
+ currentCues = cues;
+ for (TextOutput textOutput : textOutputs) {
+ textOutput.onCues(cues);
+ }
+ }
+
+ // MetadataOutput implementation
+
+ @Override
+ public void onMetadata(Metadata metadata) {
+ for (MetadataOutput metadataOutput : metadataOutputs) {
+ metadataOutput.onMetadata(metadata);
+ }
+ }
+
+ // SurfaceHolder.Callback implementation
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ setVideoSurfaceInternal(holder.getSurface(), false);
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ maybeNotifySurfaceSizeChanged(width, height);
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ 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), /* ownsSurface= */ true);
+ maybeNotifySurfaceSizeChanged(width, height);
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
+ maybeNotifySurfaceSizeChanged(width, height);
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+ setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true);
+ maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+ // 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Timeline.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Timeline.java
new file mode 100644
index 0000000000..c9e3d16ff7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/Timeline.java
@@ -0,0 +1,837 @@
+/*
+ * 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 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 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.
+ *
+ * <p>A timeline consists of {@link Window Windows} and {@link Period Periods}.
+ *
+ * <ul>
+ * <li>A {@link Window} usually corresponds to one playlist item. It may span one or more periods
+ * and it defines the region within those periods that's currently available for playback. The
+ * window also provides additional information such as whether seeking is supported within the
+ * window and the default position, which is the position from which playback will start when
+ * the player starts playing the window.
+ * <li>A {@link Period} defines a single logical piece 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.
+ * </ul>
+ *
+ * <p>The following examples illustrate timelines for various use cases.
+ *
+ * <h3 id="single-file">Single media file or on-demand stream</h3>
+ *
+ * <p style="align:center"><img src="doc-files/timeline-single-file.svg" alt="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).
+ *
+ * <h3>Playlist of media files or on-demand streams</h3>
+ *
+ * <p style="align:center"><img src="doc-files/timeline-playlist.svg" alt="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.
+ *
+ * <h3 id="live-limited">Live stream with limited availability</h3>
+ *
+ * <p style="align:center"><img src="doc-files/timeline-live-limited.svg" alt="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).
+ *
+ * <h3>Live stream with indefinite availability</h3>
+ *
+ * <p style="align:center"><img src="doc-files/timeline-live-indefinite.svg" alt="Example timeline
+ * for a live stream with indefinite availability"> A timeline for a live stream with indefinite
+ * availability is similar to the <a href="#live-limited">Live stream with limited availability</a>
+ * 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.
+ *
+ * <h3 id="live-multi-period">Live stream with multiple periods</h3>
+ *
+ * <p style="align:center"><img src="doc-files/timeline-live-multi-period.svg" alt="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 <a
+ * href="#live-limited">Live stream with limited availability</a> case, except that the window may
+ * span more than one period. Multiple periods are also possible in the indefinite availability
+ * case.
+ *
+ * <h3>On-demand stream followed by live stream</h3>
+ *
+ * <p style="align:center"><img src="doc-files/timeline-advanced.svg" alt="Example timeline for an
+ * on-demand stream followed by a live stream"> This case is the concatenation of the <a
+ * href="#single-file">Single media file or on-demand stream</a> and <a href="#multi-period">Live
+ * stream with multiple periods</a> cases. When playback of the on-demand stream ends, playback of
+ * the live stream will start from its default position near the live edge.
+ *
+ * <h3 id="single-file-midrolls">On-demand stream with mid-roll ads</h3>
+ *
+ * <p style="align:center"><img src="doc-files/timeline-single-file-midrolls.svg" alt="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 {
+
+ /**
+ * 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.
+ *
+ * <p style="align:center"><img src="doc-files/timeline-window.svg" alt="Information defined by a
+ * timeline window">
+ */
+ public static final class Window {
+
+ /**
+ * A {@link #uid} for a window that must be used for single-window {@link Timeline Timelines}.
+ */
+ 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
+ * epoch, or {@link C#TIME_UNSET} if unknown or not applicable. For informational purposes only.
+ */
+ public long presentationStartTimeMs;
+
+ /**
+ * The window's start time in milliseconds since the epoch, or {@link C#TIME_UNSET} if unknown
+ * or not applicable. For informational purposes only.
+ */
+ public long windowStartTimeMs;
+
+ /**
+ * Whether it's possible to seek within this window.
+ */
+ public boolean isSeekable;
+
+ // 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;
+
+ /**
+ * Whether the media in this window is live. For informational purposes only.
+ *
+ * <p>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;
+
+ /**
+ * The index of the last period that belongs to this window.
+ */
+ public int lastPeriodIndex;
+
+ /**
+ * The default position relative to the start of the window at which to begin playback, in
+ * microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a
+ * non-zero default position projection, and if the specified projection cannot be performed
+ * whilst remaining within the bounds of the window.
+ */
+ public long defaultPositionUs;
+
+ /**
+ * The duration of this window in microseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public long durationUs;
+
+ /**
+ * The position of the start of this window relative to the start of the first period belonging
+ * to it, in microseconds.
+ */
+ public long positionInFirstPeriodUs;
+
+ /** 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;
+ this.lastPeriodIndex = lastPeriodIndex;
+ this.positionInFirstPeriodUs = positionInFirstPeriodUs;
+ return this;
+ }
+
+ /**
+ * Returns the default position relative to the start of the window at which to begin playback,
+ * in milliseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a
+ * non-zero default position projection, and if the specified projection cannot be performed
+ * whilst remaining within the bounds of the window.
+ */
+ public long getDefaultPositionMs() {
+ return C.usToMs(defaultPositionUs);
+ }
+
+ /**
+ * Returns the default position relative to the start of the window at which to begin playback,
+ * in microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a
+ * non-zero default position projection, and if the specified projection cannot be performed
+ * whilst remaining within the bounds of the window.
+ */
+ public long getDefaultPositionUs() {
+ return defaultPositionUs;
+ }
+
+ /**
+ * Returns the duration of the window in milliseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public long getDurationMs() {
+ return C.usToMs(durationUs);
+ }
+
+ /**
+ * Returns the duration of this window in microseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ /**
+ * Returns the position of the start of this window relative to the start of the first period
+ * belonging to it, in milliseconds.
+ */
+ public long getPositionInFirstPeriodMs() {
+ return C.usToMs(positionInFirstPeriodUs);
+ }
+
+ /**
+ * Returns the position of the start of this window relative to the start of the first period
+ * belonging to it, in microseconds.
+ */
+ public long getPositionInFirstPeriodUs() {
+ return positionInFirstPeriodUs;
+ }
+
+ }
+
+ /**
+ * Holds information about a period in a {@link Timeline}. A period defines a single logical piece
+ * 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.
+ *
+ * <p>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.
+ *
+ * <p style="align:center"><img src="doc-files/timeline-period.svg" alt="Information defined by a
+ * period">
+ */
+ public static final class Period {
+
+ /**
+ * An identifier for the period. Not necessarily unique. May be null if the ids of the period
+ * are not required.
+ */
+ @Nullable public Object id;
+
+ /**
+ * A unique identifier for the period. May be null if the ids of the period are not required.
+ */
+ @Nullable public Object uid;
+
+ /**
+ * The index of the window to which this period belongs.
+ */
+ public int windowIndex;
+
+ /**
+ * The duration of this period in microseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public long durationUs;
+
+ 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(
+ @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.adPlaybackState = adPlaybackState;
+ return this;
+ }
+
+ /**
+ * Returns the duration of the period in milliseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public long getDurationMs() {
+ return C.usToMs(durationUs);
+ }
+
+ /**
+ * Returns the duration of this period in microseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ /**
+ * Returns 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.
+ */
+ public long getPositionInWindowMs() {
+ return C.usToMs(positionInWindowUs);
+ }
+
+ /**
+ * Returns the position of the start of this period relative to the start of the window to which
+ * it belongs, in microseconds. May be negative if the start of the period is not within the
+ * window.
+ */
+ public long getPositionInWindowUs() {
+ 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<Object, Long> 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<Object, Long> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/WakeLockManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/WakeLockManager.java
new file mode 100644
index 0000000000..368eb8aa0d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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}.
+ *
+ * <p>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}.
+ *
+ * <p>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.
+ *
+ * <p>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}.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/WifiLockManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/WifiLockManager.java
new file mode 100644
index 0000000000..1081dd39a8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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}
+ *
+ * <p>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}.
+ *
+ * <p>By default, wifi lock handling is not enabled. Enabling will acquire the wifi lock if
+ * necessary. Disabling will release the wifi lock if held.
+ *
+ * <p>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}.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
new file mode 100644
index 0000000000..6bdb4c7727
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<AnalyticsListener> 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<MediaPeriodInfo> 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<AnalyticsListener> 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<MediaPeriodInfo> mediaPeriodInfoQueue;
+ private final HashMap<MediaPeriodId, MediaPeriodInfo> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsListener.java
new file mode 100644
index 0000000000..a265268c19
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>All events are recorded with an {@link EventTime} specifying the elapsed real time and media
+ * time at the time of the event.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java
new file mode 100644
index 0000000000..f56ac3fef0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java
new file mode 100644
index 0000000000..710934bd36
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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<String, SessionDescriptor> 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<SessionDescriptor> 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<SessionDescriptor> 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.
+ *
+ * <p>The session may be described in one of three ways:
+ *
+ * <ul>
+ * <li>A window index with unset window sequence number and a null ad media period id
+ * <li>A content window with index and sequence number, but a null ad media period id.
+ * <li>An ad with all values set.
+ * </ul>
+ */
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java
new file mode 100644
index 0000000000..d3c6f7dd20
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStats.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStats.java
new file mode 100644
index 0000000000..eef0f6e7ce
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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}.
+ *
+ * <p>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<Pair<EventTime, @PlaybackState Integer>> 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<long[]> 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.
+ *
+ * <p>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<Pair<EventTime, @NullableType Format>> 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<Pair<EventTime, @NullableType Format>> 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<Pair<EventTime, Exception>> 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<Pair<EventTime, Exception>> nonFatalErrorHistory;
+
+ private final long[] playbackStateDurationsMs;
+
+ /* package */ PlaybackStats(
+ int playbackCount,
+ long[] playbackStateDurationsMs,
+ List<Pair<EventTime, @PlaybackState Integer>> playbackStateHistory,
+ List<long[]> 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<Pair<EventTime, @NullableType Format>> videoFormatHistory,
+ List<Pair<EventTime, @NullableType Format>> 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<Pair<EventTime, Exception>> fatalErrorHistory,
+ List<Pair<EventTime, Exception>> 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<EventTime, @PlaybackState Integer> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java
new file mode 100644
index 0000000000..058a3a97c1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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}.
+ *
+ * <p>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<String, PlaybackStatsTracker> playbackStatsTrackers;
+ private final Map<String, EventTime> 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.
+ *
+ * <p>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<String, PlaybackStatsTracker> 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<Pair<EventTime, @PlaybackState Integer>> playbackStateHistory;
+ private final List<long[]> mediaTimeHistory;
+ private final List<Pair<EventTime, @NullableType Format>> videoFormatHistory;
+ private final List<Pair<EventTime, @NullableType Format>> audioFormatHistory;
+ private final List<Pair<EventTime, Exception>> fatalErrorHistory;
+ private final List<Pair<EventTime, Exception>> 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<long[]> 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<Pair<EventTime, @NullableType Format>> videoHistory =
+ isFinal ? videoFormatHistory : new ArrayList<>(videoFormatHistory);
+ List<Pair<EventTime, @NullableType Format>> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/package-info.java
new file mode 100644
index 0000000000..08556b00b0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Ac3Util.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Ac3Util.java
new file mode 100644
index 0000000000..c68e49dea1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Ac3Util.java
@@ -0,0 +1,584 @@
+/*
+ * 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 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 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 SyncFrameInfo {
+
+ /**
+ * 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}.
+ */
+ @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.
+ */
+ 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(
+ @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;
+ this.sampleCount = sampleCount;
+ }
+
+ }
+
+ /**
+ * 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 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.
+ */
+ private static final int[] BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD = new int[] {1, 2, 3, 6};
+ /**
+ * Sample rates, indexed by fscod.
+ */
+ private static final int[] SAMPLE_RATE_BY_FSCOD = new int[] {48000, 44100, 32000};
+ /**
+ * Sample rates, indexed by fscod2 (E-AC-3).
+ */
+ private static final int[] SAMPLE_RATE_BY_FSCOD2 = new int[] {24000, 22050, 16000};
+ /**
+ * 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 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 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.
+ * @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, @Nullable DrmInitData drmInitData) {
+ int fscod = (data.readUnsignedByte() & 0xC0) >> 6;
+ int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
+ int nextByte = data.readUnsignedByte();
+ int channelCount = CHANNEL_COUNT_BY_ACMOD[(nextByte & 0x38) >> 3];
+ if ((nextByte & 0x04) != 0) { // lfeon
+ channelCount++;
+ }
+ 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 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.
+ * @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, @Nullable DrmInitData drmInitData) {
+ data.skipBytes(2); // data_rate, num_ind_sub
+
+ // Read the first independent substream.
+ int fscod = (data.readUnsignedByte() & 0xC0) >> 6;
+ int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
+ int nextByte = data.readUnsignedByte();
+ int channelCount = CHANNEL_COUNT_BY_ACMOD[(nextByte & 0x0E) >> 1];
+ if ((nextByte & 0x01) != 0) { // lfeon
+ channelCount++;
+ }
+
+ // 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);
+ }
+
+ /**
+ * Returns (E-)AC-3 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 (E-)AC-3 format data parsed from the header.
+ */
+ public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) {
+ int initialPosition = data.getPosition();
+ data.skipBits(40);
+ // 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);
+ @Nullable String mimeType;
+ @StreamType int streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED;
+ int sampleRate;
+ int acmod;
+ int frameSize;
+ int sampleCount;
+ boolean lfeon;
+ int channelCount;
+ if (isEac3) {
+ // 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 {
+ 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
+ acmod = data.readBits(3);
+ if ((acmod & 0x01) != 0 && acmod != 1) {
+ data.skipBits(2); // cmixlev
+ }
+ if ((acmod & 0x04) != 0) {
+ data.skipBits(2); // surmixlev
+ }
+ if (acmod == 2) {
+ data.skipBits(2); // dsurmod
+ }
+ 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);
+ }
+ return new SyncFrameInfo(
+ mimeType, streamType, channelCount, sampleRate, frameSize, sampleCount);
+ }
+
+ /**
+ * 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 < 6) {
+ return C.LENGTH_UNSET;
+ }
+ // 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);
+ }
+ }
+
+ /**
+ * 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 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) {
+ int halfFrmsizecod = frmsizecod / 2;
+ if (fscod < 0 || fscod >= SAMPLE_RATE_BY_FSCOD.length || frmsizecod < 0
+ || halfFrmsizecod >= SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1.length) {
+ // Invalid values provided.
+ return C.LENGTH_UNSET;
+ }
+ int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
+ if (sampleRate == 44100) {
+ return 2 * (SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1[halfFrmsizecod] + (frmsizecod % 2));
+ }
+ int bitrate = BITRATE_BY_HALF_FRMSIZECOD[halfFrmsizecod];
+ if (sampleRate == 32000) {
+ return 6 * bitrate;
+ } else { // sampleRate == 48000
+ return 4 * bitrate;
+ }
+ }
+
+ private Ac3Util() {}
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Ac4Util.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Ac4Util.java
new file mode 100644
index 0000000000..a921346e90
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioAttributes.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioAttributes.java
new file mode 100644
index 0000000000..d0f3fcb438
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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}.
+ * <p>
+ * 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.
+ * <p>
+ * 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}.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilities.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilities.java
new file mode 100644
index 0000000000..f985891465
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilities.java
@@ -0,0 +1,161 @@
+/*
+ * 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.content.Context;
+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. */
+@TargetApi(21)
+public final class AudioCapabilities {
+
+ 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}, 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.
+ *
+ * @param context A context for obtaining the current audio capabilities.
+ * @return The current audio capabilities for the device.
+ */
+ @SuppressWarnings("InlinedApi")
+ public static AudioCapabilities getCapabilities(Context context) {
+ Intent intent =
+ context.registerReceiver(
+ /* receiver= */ null, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG));
+ return getCapabilities(context, intent);
+ }
+
+ @SuppressLint("InlinedApi")
+ /* 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, /* 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;
+ private final int maxChannelCount;
+
+ /**
+ * Constructs new audio capabilities based on a set of supported encodings and a maximum channel
+ * count.
+ *
+ * <p>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. Passing {@code null} indicates that no encodings are
+ * supported.
+ * @param maxChannelCount The maximum number of audio channels that can be played simultaneously.
+ */
+ public AudioCapabilities(@Nullable int[] supportedEncodings, int maxChannelCount) {
+ if (supportedEncodings != null) {
+ this.supportedEncodings = Arrays.copyOf(supportedEncodings, supportedEncodings.length);
+ Arrays.sort(this.supportedEncodings);
+ } else {
+ this.supportedEncodings = new int[0];
+ }
+ this.maxChannelCount = maxChannelCount;
+ }
+
+ /**
+ * Returns whether this device supports playback of the specified audio {@code encoding}.
+ *
+ * @param encoding One of {@link android.media.AudioFormat}'s {@code ENCODING_*} constants.
+ * @return Whether this device supports playback the specified audio {@code encoding}.
+ */
+ public boolean supportsEncoding(int encoding) {
+ return Arrays.binarySearch(supportedEncodings, encoding) >= 0;
+ }
+
+ /**
+ * Returns the maximum number of channels the device can play at the same time.
+ */
+ public int getMaxChannelCount() {
+ return maxChannelCount;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof AudioCapabilities)) {
+ return false;
+ }
+ AudioCapabilities audioCapabilities = (AudioCapabilities) other;
+ return Arrays.equals(supportedEncodings, audioCapabilities.supportedEncodings)
+ && maxChannelCount == audioCapabilities.maxChannelCount;
+ }
+
+ @Override
+ public int hashCode() {
+ return maxChannelCount + 31 * Arrays.hashCode(supportedEncodings);
+ }
+
+ @Override
+ public String toString() {
+ return "AudioCapabilities[maxChannelCount=" + maxChannelCount
+ + ", supportedEncodings=" + Arrays.toString(supportedEncodings) + "]";
+ }
+
+ private static boolean deviceMaySetExternalSurroundSoundGlobalSetting() {
+ return Util.SDK_INT >= 17 && "Amazon".equals(Util.MANUFACTURER);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java
new file mode 100644
index 0000000000..d96fd32f53
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java
@@ -0,0 +1,166 @@
+/*
+ * 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.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;
+
+/**
+ * Receives broadcast events indicating changes to the device's audio capabilities, notifying a
+ * {@link Listener} when audio capability changes occur.
+ */
+public final class AudioCapabilitiesReceiver {
+
+ /**
+ * Listener notified when audio capabilities change.
+ */
+ public interface Listener {
+
+ /**
+ * Called when the audio capabilities change.
+ *
+ * @param audioCapabilities The current audio capabilities for the device.
+ */
+ void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities);
+
+ }
+
+ private final Context context;
+ private final Listener listener;
+ private final Handler handler;
+ @Nullable private final BroadcastReceiver receiver;
+ @Nullable private final ExternalSurroundSoundSettingObserver externalSurroundSoundSettingObserver;
+
+ /* 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) {
+ context = context.getApplicationContext();
+ this.context = context;
+ this.listener = Assertions.checkNotNull(listener);
+ 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;
+ }
+
+ /**
+ * Registers the receiver, meaning it will notify the listener when audio capability changes
+ * occur. The current audio capabilities will be returned. It is important to call
+ * {@link #unregister} when the receiver is no longer required.
+ *
+ * @return The current audio capabilities for the device.
+ */
+ @SuppressWarnings("InlinedApi")
+ public AudioCapabilities register() {
+ 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;
+ }
+
+ /**
+ * Unregisters the receiver, meaning it will no longer notify the listener when audio capability
+ * 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 {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!isInitialStickyBroadcast()) {
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioDecoderException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioDecoderException.java
new file mode 100644
index 0000000000..0f4ac159b9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioDecoderException.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/** Thrown when an audio decoder error occurs. */
+public class AudioDecoderException extends Exception {
+
+ /** @param message The detail message for this exception. */
+ public AudioDecoderException(String message) {
+ super(message);
+ }
+
+ /**
+ * @param message The detail message for this exception.
+ * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
+ * A <tt>null</tt> value is permitted, and indicates that the cause is nonexistent or unknown.
+ */
+ public AudioDecoderException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioListener.java
new file mode 100644
index 0000000000..457f52b887
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioProcessor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioProcessor.java
new file mode 100644
index 0000000000..e0814314ca
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioProcessor.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.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, which take audio data as input and transform it, potentially
+ * modifying its channel count, encoding and/or sample rate.
+ *
+ * <p>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 {
+
+ /** 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);
+
+ /** 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}. */
+ ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder());
+
+ /**
+ * Configures the processor to process input audio with the specified format. After calling this
+ * method, call {@link #isActive()} to determine whether the audio processor is active. Returns
+ * the configured output audio format if this instance is active.
+ *
+ * <p>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.
+ */
+ AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException;
+
+ /** Returns whether the processor is configured and will process input buffers. */
+ boolean isActive();
+
+ /**
+ * 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
+ * read-only. Its position will be advanced by the number of bytes consumed (which may be zero).
+ * The caller retains ownership of the provided buffer. Calling this method invalidates any
+ * previous buffer returned by {@link #getOutput()}.
+ *
+ * @param buffer The input buffer to process.
+ */
+ void queueInput(ByteBuffer buffer);
+
+ /**
+ * Queues an end of stream signal. After this method has been called,
+ * {@link #queueInput(ByteBuffer)} may not be called until after the next call to
+ * {@link #flush()}. Calling {@link #getOutput()} will return any remaining output data. Multiple
+ * calls may be required to read all of the remaining output data. {@link #isEnded()} will return
+ * {@code true} once all remaining output data has been read.
+ */
+ void queueEndOfStream();
+
+ /**
+ * Returns a buffer containing processed output data between its position and limit. The buffer
+ * will always be a direct byte buffer with native byte order. Calling this method invalidates any
+ * previously returned buffer. The buffer will be empty if no output is available.
+ *
+ * @return A buffer containing processed output data between its position and limit.
+ */
+ ByteBuffer getOutput();
+
+ /**
+ * Returns whether this processor will return no more output from {@link #getOutput()} until it
+ * has been {@link #flush()}ed and more input has been queued.
+ */
+ boolean isEnded();
+
+ /**
+ * 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 unconfigured state, releasing any resources. */
+ void reset();
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioRendererEventListener.java
new file mode 100644
index 0000000000..bb1ae72855
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioRendererEventListener.java
@@ -0,0 +1,174 @@
+/*
+ * 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 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;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderCounters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * Listener of audio {@link Renderer} events. All methods have no-op default implementations to
+ * allow selective overrides.
+ */
+public interface AudioRendererEventListener {
+
+ /**
+ * Called when the renderer is enabled.
+ *
+ * @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it
+ * remains enabled.
+ */
+ default void onAudioEnabled(DecoderCounters counters) {}
+
+ /**
+ * Called when the audio session is set.
+ *
+ * @param audioSessionId The audio session id.
+ */
+ default void onAudioSessionId(int audioSessionId) {}
+
+ /**
+ * Called when a decoder is created.
+ *
+ * @param decoderName The decoder that was created.
+ * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization
+ * finished.
+ * @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
+ */
+ 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.
+ */
+ default void onAudioInputFormatChanged(Format format) {}
+
+ /**
+ * Called when an {@link AudioSink} underrun occurs.
+ *
+ * @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 onAudioSinkUnderrun(
+ int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {}
+
+ /**
+ * Called when the renderer is disabled.
+ *
+ * @param counters {@link DecoderCounters} that were updated by the renderer.
+ */
+ default void onAudioDisabled(DecoderCounters counters) {}
+
+ /**
+ * Dispatches events to a {@link AudioRendererEventListener}.
+ */
+ final class EventDispatcher {
+
+ @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(@Nullable Handler handler,
+ @Nullable AudioRendererEventListener listener) {
+ this.handler = listener != null ? Assertions.checkNotNull(handler) : null;
+ this.listener = listener;
+ }
+
+ /**
+ * Invokes {@link AudioRendererEventListener#onAudioEnabled(DecoderCounters)}.
+ */
+ public void enabled(final DecoderCounters decoderCounters) {
+ if (handler != null) {
+ handler.post(() -> castNonNull(listener).onAudioEnabled(decoderCounters));
+ }
+ }
+
+ /**
+ * Invokes {@link AudioRendererEventListener#onAudioDecoderInitialized(String, long, long)}.
+ */
+ public void decoderInitialized(final String decoderName,
+ final long initializedTimestampMs, final long initializationDurationMs) {
+ if (handler != null) {
+ handler.post(
+ () ->
+ castNonNull(listener)
+ .onAudioDecoderInitialized(
+ decoderName, initializedTimestampMs, initializationDurationMs));
+ }
+ }
+
+ /**
+ * Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}.
+ */
+ public void inputFormatChanged(final Format format) {
+ if (handler != null) {
+ handler.post(() -> castNonNull(listener).onAudioInputFormatChanged(format));
+ }
+ }
+
+ /**
+ * Invokes {@link AudioRendererEventListener#onAudioSinkUnderrun(int, long, long)}.
+ */
+ public void audioTrackUnderrun(final int bufferSize, final long bufferSizeMs,
+ final long elapsedSinceLastFeedMs) {
+ if (handler != null) {
+ handler.post(
+ () ->
+ castNonNull(listener)
+ .onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs));
+ }
+ }
+
+ /**
+ * Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}.
+ */
+ public void disabled(final DecoderCounters counters) {
+ counters.ensureUpdated();
+ if (handler != null) {
+ handler.post(
+ () -> {
+ counters.ensureUpdated();
+ castNonNull(listener).onAudioDisabled(counters);
+ });
+ }
+ }
+
+ /**
+ * Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}.
+ */
+ public void audioSessionId(final int audioSessionId) {
+ if (handler != null) {
+ handler.post(() -> castNonNull(listener).onAudioSessionId(audioSessionId));
+ }
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioSink.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioSink.java
new file mode 100644
index 0000000000..db87e28e7f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>Before starting playback, specify the input audio format by calling {@link #configure(int,
+ * int, int, int, int[], int, int)}.
+ *
+ * <p>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.
+ *
+ * <p>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)}.
+ *
+ * <p>Call {@link #flush()} to prepare the sink to receive audio data from a new playback position.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ * <p>
+ * 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.
+ *
+ * <p>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.
+ * <p>
+ * 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 &lt; 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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTimestampPoller.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTimestampPoller.java
new file mode 100644
index 0000000000..153947fec0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java
new file mode 100644
index 0000000000..e62e8cf2c5
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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)}.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AuxEffectInfo.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/AuxEffectInfo.java
new file mode 100644
index 0000000000..6039a8c1a8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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}.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/BaseAudioProcessor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/BaseAudioProcessor.java
new file mode 100644
index 0000000000..189d8f0265
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java
new file mode 100644
index 0000000000..e8496d4608
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java
@@ -0,0 +1,99 @@
+/*
+ * 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 androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.nio.ByteBuffer;
+
+/**
+ * An {@link AudioProcessor} that applies a mapping from input channels onto specified output
+ * channels. This can be used to reorder, duplicate or discard channels.
+ */
+@SuppressWarnings("nullness:initialization.fields.uninitialized")
+/* package */ final class ChannelMappingAudioProcessor extends BaseAudioProcessor {
+
+ @Nullable private int[] pendingOutputChannels;
+ @Nullable private int[] outputChannels;
+
+ /**
+ * Resets the channel mapping. After calling this method, call {@link #configure(AudioFormat)} to
+ * start using the new channel map.
+ *
+ * @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(@Nullable int[] outputChannels) {
+ pendingOutputChannels = outputChannels;
+ }
+
+ @Override
+ public AudioFormat onConfigure(AudioFormat inputAudioFormat)
+ throws UnhandledAudioFormatException {
+ @Nullable int[] outputChannels = pendingOutputChannels;
+ if (outputChannels == null) {
+ return AudioFormat.NOT_SET;
+ }
+
+ 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 >= inputAudioFormat.channelCount) {
+ throw new UnhandledAudioFormatException(inputAudioFormat);
+ }
+ active |= (channelIndex != i);
+ }
+ 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) / 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 += inputAudioFormat.bytesPerFrame;
+ }
+ inputBuffer.position(limit);
+ buffer.flip();
+ }
+
+ @Override
+ protected void onFlush() {
+ outputChannels = pendingOutputChannels;
+ }
+
+ @Override
+ protected void onReset() {
+ outputChannels = null;
+ pendingOutputChannels = null;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DefaultAudioSink.java
new file mode 100644
index 0000000000..9fc3fbbfd8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ * <p>
+ * 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} &lt; 21.
+ * <p>
+ * 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}.
+ * <p>
+ * 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<PlaybackParametersCheckpoint> 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<AudioProcessor> 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<AudioProcessor> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DtsUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DtsUtil.java
new file mode 100644
index 0000000000..6e5d749fdf
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DtsUtil.java
@@ -0,0 +1,217 @@
+/*
+ * 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 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.
+ */
+ private static final int[] CHANNELS_BY_AMODE = new int[] {1, 2, 2, 2, 2, 3, 3, 4, 4, 5, 6, 6, 6,
+ 7, 8, 8};
+
+ /**
+ * Maps SFREQ to the sampling frequency in Hz. See ETSI TS 102 144 table 5.5.
+ */
+ private static final int[] SAMPLE_RATE_BY_SFREQ = new int[] {-1, 8000, 16000, 32000, -1, -1,
+ 11025, 22050, 44100, -1, -1, 12000, 24000, 48000, -1, -1};
+
+ /**
+ * Maps RATE to 2 * bitrate in kbit/s. See ETSI TS 102 144 table 5.7.
+ */
+ private static final int[] TWICE_BITRATE_KBPS_BY_RATE = new int[] {64, 112, 128, 192, 224, 256,
+ 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.
+ * @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, @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);
+ int sampleRate = SAMPLE_RATE_BY_SFREQ[sfreq];
+ int rate = frameBits.readBits(5);
+ int bitrate = rate >= TWICE_BITRATE_KBPS_BY_RATE.length ? Format.NO_VALUE
+ : TWICE_BITRATE_KBPS_BY_RATE[rate] * 1000 / 2;
+ frameBits.skipBits(10); // MIX, DYNF, TIMEF, AUXF, HDCD, EXT_AUDIO_ID, EXT_AUDIO, ASPF
+ channelCount += frameBits.readBits(2) > 0 ? 1 : 0; // LFF
+ return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_DTS, null, bitrate,
+ Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language);
+ }
+
+ /**
+ * Returns the number of audio samples represented by the given DTS frame.
+ *
+ * @param data The frame to parse.
+ * @return The number of audio samples represented by the frame.
+ */
+ public static int parseDtsAudioSampleCount(byte[] data) {
+ 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;
+ }
+
+ /**
+ * Like {@link #parseDtsAudioSampleCount(byte[])} but reads from a {@link ByteBuffer}. The
+ * buffer's position is not modified.
+ *
+ * @param buffer The {@link ByteBuffer} from which to read.
+ * @return The number of audio samples represented by the syncframe.
+ */
+ public static int parseDtsAudioSampleCount(ByteBuffer buffer) {
+ // See ETSI TS 102 114 subsection 5.4.1.
+ int position = buffer.position();
+ 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;
+ }
+
+ /**
+ * Returns the size in bytes of the given DTS frame.
+ *
+ * @param data The frame to parse.
+ * @return The frame's size in bytes.
+ */
+ public static int getDtsFrameSize(byte[] data) {
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java
new file mode 100644
index 0000000000..c2eb62a0ad
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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:
+ *
+ * <ul>
+ * <li>{@link C#ENCODING_PCM_24BIT}
+ * <li>{@link C#ENCODING_PCM_32BIT}
+ * <li>{@link C#ENCODING_PCM_FLOAT} ({@link #isActive()} will return {@code false})
+ * </ul>
+ */
+/* 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ForwardingAudioSink.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ForwardingAudioSink.java
new file mode 100644
index 0000000000..4e7f9d69f9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
new file mode 100644
index 0000000000..42f7e99b78
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
@@ -0,0 +1,1036 @@
+/*
+ * 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.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 an {@link AudioSink}.
+ *
+ * <p>This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)}
+ * on the playback thread:
+ *
+ * <ul>
+ * <li>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.
+ * <li>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.
+ * <li>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.
+ * </ul>
+ */
+public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {
+
+ /**
+ * 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;
+ @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.
+ */
+ @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.
+ * @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.
+ * @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<FrameworkMediaCrypto> drmSessionManager,
+ boolean playClearSamplesWithoutKeys) {
+ 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.
+ */
+ @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.
+ * @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.
+ * @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<FrameworkMediaCrypto> 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.
+ * @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 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 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.
+ */
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public MediaCodecAudioRenderer(
+ Context context,
+ MediaCodecSelector mediaCodecSelector,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ boolean playClearSamplesWithoutKeys,
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ @Nullable AudioCapabilities audioCapabilities,
+ AudioProcessor... audioProcessors) {
+ 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<FrameworkMediaCrypto> 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<FrameworkMediaCrypto> 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
+ @Capabilities
+ protected int supportsFormat(
+ MediaCodecSelector mediaCodecSelector,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ Format format)
+ throws DecoderQueryException {
+ String mimeType = format.sampleMimeType;
+ if (!MimeTypes.isAudio(mimeType)) {
+ return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
+ }
+ @TunnelingSupport
+ int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
+ 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);
+ }
+ 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);
+ }
+ List<MediaCodecInfo> 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 List<MediaCodecInfo> 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) {
+ return Collections.singletonList(passthroughDecoderInfo);
+ }
+ }
+ List<MediaCodecInfo> 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<MediaCodecInfo> 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 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 is supported.
+ */
+ protected boolean allowPassthrough(int channelCount, String mimeType) {
+ return getPassthroughEncoding(channelCount, mimeType) != C.ENCODING_INVALID;
+ }
+
+ @Override
+ 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) {
+ // Store the input MIME type if we're using the passthrough codec.
+ passthroughMediaFormat = mediaFormat;
+ passthroughMediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType);
+ } else {
+ 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) {
+ eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs);
+ }
+
+ @Override
+ protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {
+ super.onInputFormatChanged(formatHolder);
+ inputFormat = formatHolder.format;
+ eventDispatcher.inputFormatChanged(inputFormat);
+ }
+
+ @Override
+ protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputMediaFormat)
+ throws ExoPlaybackException {
+ @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 && inputFormat.channelCount < 6) {
+ channelMap = new int[inputFormat.channelCount];
+ for (int i = 0; i < inputFormat.channelCount; i++) {
+ channelMap[i] = i;
+ }
+ } else {
+ channelMap = null;
+ }
+
+ try {
+ 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;
+ }
+ }
+
+ /**
+ * Called when the audio session id becomes known. The default implementation is a no-op. One
+ * reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in
+ * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances
+ * should be released in {@link #onDisabled()} (if not before).
+ *
+ * @see AudioSink.Listener#onAudioSessionId(int)
+ */
+ protected void onAudioSessionId(int audioSessionId) {
+ // Do nothing.
+ }
+
+ /**
+ * @see AudioSink.Listener#onPositionDiscontinuity()
+ */
+ protected void onAudioTrackPositionDiscontinuity() {
+ // Do nothing.
+ }
+
+ /**
+ * @see AudioSink.Listener#onUnderrun(int, long, long)
+ */
+ protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
+ long elapsedSinceLastFeedMs) {
+ // Do nothing.
+ }
+
+ @Override
+ protected void onEnabled(boolean joining) throws ExoPlaybackException {
+ super.onEnabled(joining);
+ eventDispatcher.enabled(decoderCounters);
+ int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
+ if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
+ audioSink.enableTunnelingV21(tunnelingAudioSessionId);
+ } else {
+ 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);
+ audioSink.flush();
+ currentPositionUs = positionUs;
+ allowFirstBufferPositionDiscontinuity = true;
+ allowPositionDiscontinuity = true;
+ lastInputTimeUs = C.TIME_UNSET;
+ pendingStreamChangeCount = 0;
+ }
+
+ @Override
+ protected void onStarted() {
+ super.onStarted();
+ audioSink.play();
+ }
+
+ @Override
+ protected void onStopped() {
+ updateCurrentPosition();
+ audioSink.pause();
+ super.onStopped();
+ }
+
+ @Override
+ protected void onDisabled() {
+ try {
+ lastInputTimeUs = C.TIME_UNSET;
+ pendingStreamChangeCount = 0;
+ audioSink.flush();
+ } finally {
+ try {
+ super.onDisabled();
+ } finally {
+ eventDispatcher.disabled(decoderCounters);
+ }
+ }
+ }
+
+ @Override
+ protected void onReset() {
+ try {
+ super.onReset();
+ } finally {
+ audioSink.reset();
+ }
+ }
+
+ @Override
+ public boolean isEnded() {
+ return super.isEnded() && audioSink.isEnded();
+ }
+
+ @Override
+ public boolean isReady() {
+ return audioSink.hasPendingData() || super.isReady();
+ }
+
+ @Override
+ public long getPositionUs() {
+ if (getState() == STATE_STARTED) {
+ updateCurrentPosition();
+ }
+ return currentPositionUs;
+ }
+
+ @Override
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {
+ audioSink.setPlaybackParameters(playbackParameters);
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return audioSink.getPlaybackParameters();
+ }
+
+ @Override
+ 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 (isDecodeOnlyBuffer) {
+ codec.releaseOutputBuffer(bufferIndex, false);
+ decoderCounters.skippedOutputBufferCount++;
+ audioSink.handleDiscontinuity();
+ return true;
+ }
+
+ try {
+ if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) {
+ codec.releaseOutputBuffer(bufferIndex, false);
+ decoderCounters.renderedOutputBufferCount++;
+ return true;
+ }
+ } catch (AudioSink.InitializationException | AudioSink.WriteException e) {
+ // TODO(internal: b/145658993) Use outputFormat instead.
+ throw createRendererException(e, inputFormat);
+ }
+ return false;
+ }
+
+ @Override
+ protected void renderToEndOfStream() throws ExoPlaybackException {
+ try {
+ audioSink.playToEndOfStream();
+ } catch (AudioSink.WriteException e) {
+ // TODO(internal: b/145658993) Use outputFormat instead.
+ throw createRendererException(e, inputFormat);
+ }
+ }
+
+ @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;
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * <p>See <a href="https://github.com/google/ExoPlayer/issues/5821">GitHub issue #5821</a>.
+ */
+ 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.
+ * <p>
+ * See [Internal: b/35655036].
+ */
+ private static boolean codecNeedsDiscardChannelsWorkaround(String codecName) {
+ // The workaround applies to Samsung Galaxy S6 and Samsung Galaxy S7.
+ return Util.SDK_INT < 24 && "OMX.SEC.aac.dec".equals(codecName)
+ && "samsung".equals(Util.MANUFACTURER)
+ && (Util.DEVICE.startsWith("zeroflte") || Util.DEVICE.startsWith("herolte")
+ || Util.DEVICE.startsWith("heroqlte"));
+ }
+
+ /**
+ * Returns whether the decoder may output a non-empty buffer with timestamp 0 as the end of stream
+ * buffer.
+ *
+ * <p>See <a href="https://github.com/google/ExoPlayer/issues/5045">GitHub issue #5045</a>.
+ */
+ 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) {
+ eventDispatcher.audioSessionId(audioSessionId);
+ MediaCodecAudioRenderer.this.onAudioSessionId(audioSessionId);
+ }
+
+ @Override
+ public void onPositionDiscontinuity() {
+ onAudioTrackPositionDiscontinuity();
+ // We are out of sync so allow currentPositionUs to jump backwards.
+ MediaCodecAudioRenderer.this.allowPositionDiscontinuity = true;
+ }
+
+ @Override
+ public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
+ eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+ onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java
new file mode 100644
index 0000000000..efd8a30d61
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java
@@ -0,0 +1,134 @@
+/*
+ * 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.Format;
+import java.nio.ByteBuffer;
+
+/**
+ * An {@link AudioProcessor} that converts different PCM audio encodings to 16-bit integer PCM. The
+ * following encodings are supported as input:
+ *
+ * <ul>
+ * <li>{@link C#ENCODING_PCM_8BIT}
+ * <li>{@link C#ENCODING_PCM_16BIT} ({@link #isActive()} will return {@code false})
+ * <li>{@link C#ENCODING_PCM_16BIT_BIG_ENDIAN}
+ * <li>{@link C#ENCODING_PCM_24BIT}
+ * <li>{@link C#ENCODING_PCM_32BIT}
+ * <li>{@link C#ENCODING_PCM_FLOAT}
+ * </ul>
+ */
+/* package */ final class ResamplingAudioProcessor extends BaseAudioProcessor {
+
+ @Override
+ 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);
+ }
+ return encoding != C.ENCODING_PCM_16BIT
+ ? new AudioFormat(
+ inputAudioFormat.sampleRate, inputAudioFormat.channelCount, C.ENCODING_PCM_16BIT)
+ : AudioFormat.NOT_SET;
+ }
+
+ @Override
+ public void queueInput(ByteBuffer inputBuffer) {
+ // Prepare the output buffer.
+ int position = inputBuffer.position();
+ int limit = inputBuffer.limit();
+ int size = limit - position;
+ int resampledSize;
+ 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:
+ case C.ENCODING_INVALID:
+ case Format.NO_VALUE:
+ default:
+ throw new IllegalStateException();
+ }
+
+ // Resample the little endian input and update the input/output buffers.
+ 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.
+ 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.
+ 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.
+ 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:
+ default:
+ // Never happens.
+ throw new IllegalStateException();
+ }
+ inputBuffer.position(inputBuffer.limit());
+ buffer.flip();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java
new file mode 100644
index 0000000000..6a2c5ae9a6
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
new file mode 100644
index 0000000000..5e86e0ad78
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
@@ -0,0 +1,758 @@
+/*
+ * 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.media.audiofx.Virtualizer;
+import android.os.Handler;
+import android.os.SystemClock;
+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;
+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}.
+ *
+ * <p>This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)}
+ * on the playback thread:
+ *
+ * <ul>
+ * <li>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.
+ * <li>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.
+ * <li>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.
+ * </ul>
+ */
+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
+ })
+ 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 DrmSessionManager<ExoMediaCrypto> drmSessionManager;
+ private final boolean playClearSamplesWithoutKeys;
+ private final EventDispatcher eventDispatcher;
+ 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<DecoderInputBuffer, ? extends SimpleOutputBuffer,
+ ? extends AudioDecoderException> decoder;
+ private DecoderInputBuffer inputBuffer;
+ private SimpleOutputBuffer outputBuffer;
+ @Nullable private DrmSession<ExoMediaCrypto> decoderDrmSession;
+ @Nullable private DrmSession<ExoMediaCrypto> 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(/* eventHandler= */ null, /* eventListener= */ 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 audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
+ */
+ public SimpleDecoderAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ AudioProcessor... audioProcessors) {
+ this(
+ eventHandler,
+ eventListener,
+ /* audioCapabilities= */ null,
+ /* drmSessionManager= */ null,
+ /* playClearSamplesWithoutKeys= */ false,
+ 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 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(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ @Nullable AudioCapabilities audioCapabilities) {
+ this(
+ eventHandler,
+ eventListener,
+ audioCapabilities,
+ /* drmSessionManager= */ null,
+ /* playClearSamplesWithoutKeys= */ false);
+ }
+
+ /**
+ * @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 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 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 audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
+ */
+ public SimpleDecoderAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ @Nullable AudioCapabilities audioCapabilities,
+ @Nullable DrmSessionManager<ExoMediaCrypto> 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<ExoMediaCrypto> drmSessionManager,
+ boolean playClearSamplesWithoutKeys,
+ AudioSink audioSink) {
+ super(C.TRACK_TYPE_AUDIO);
+ this.drmSessionManager = drmSessionManager;
+ this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
+ eventDispatcher = new EventDispatcher(eventHandler, eventListener);
+ 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) {
+ 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 RendererCapabilities.create(formatSupport, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport);
+ }
+
+ /**
+ * Returns the {@link FormatSupport} for the given {@link Format}.
+ *
+ * @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}.
+ */
+ @FormatSupport
+ protected abstract int supportsFormatInternal(
+ @Nullable DrmSessionManager<ExoMediaCrypto> 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 {
+ audioSink.playToEndOfStream();
+ } catch (AudioSink.WriteException e) {
+ throw createRendererException(e, inputFormat);
+ }
+ return;
+ }
+
+ // 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);
+ } else if (result == C.RESULT_BUFFER_READ) {
+ // End of stream read having not read a format.
+ Assertions.checkState(flagsOnlyBuffer.isEndOfStream());
+ inputStreamEnded = true;
+ processEndOfStream();
+ 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()) {}
+ while (feedInputBuffer()) {}
+ TraceUtil.endSection();
+ } catch (AudioDecoderException | AudioSink.ConfigurationException
+ | AudioSink.InitializationException | AudioSink.WriteException e) {
+ throw createRendererException(e, inputFormat);
+ }
+ decoderCounters.ensureUpdated();
+ }
+ }
+
+ /**
+ * Called when the audio session id becomes known. The default implementation is a no-op. One
+ * reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in
+ * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances
+ * should be released in {@link #onDisabled()} (if not before).
+ *
+ * @see AudioSink.Listener#onAudioSessionId(int)
+ */
+ protected void onAudioSessionId(int audioSessionId) {
+ // Do nothing.
+ }
+
+ /**
+ * @see AudioSink.Listener#onPositionDiscontinuity()
+ */
+ protected void onAudioTrackPositionDiscontinuity() {
+ // Do nothing.
+ }
+
+ /**
+ * @see AudioSink.Listener#onUnderrun(int, long, long)
+ */
+ protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
+ long elapsedSinceLastFeedMs) {
+ // Do nothing.
+ }
+
+ /**
+ * 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.
+ * Maybe null and can be ignored if decoder does not handle encrypted content.
+ * @return The decoder.
+ * @throws AudioDecoderException If an error occurred creating a suitable decoder.
+ */
+ 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.
+ */
+ 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,
+ AudioSink.ConfigurationException, AudioSink.InitializationException,
+ AudioSink.WriteException {
+ if (outputBuffer == null) {
+ outputBuffer = decoder.dequeueOutputBuffer();
+ if (outputBuffer == null) {
+ return false;
+ }
+ if (outputBuffer.skippedOutputBufferCount > 0) {
+ decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
+ audioSink.handleDiscontinuity();
+ }
+ }
+
+ 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();
+ // The audio track may need to be recreated once the new output format is known.
+ audioTrackNeedsConfigure = true;
+ } else {
+ outputBuffer.release();
+ outputBuffer = null;
+ processEndOfStream();
+ }
+ return false;
+ }
+
+ if (audioTrackNeedsConfigure) {
+ Format outputFormat = getOutputFormat();
+ audioSink.configure(outputFormat.pcmEncoding, outputFormat.channelCount,
+ outputFormat.sampleRate, 0, null, encoderDelay, encoderPadding);
+ audioTrackNeedsConfigure = false;
+ }
+
+ if (audioSink.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) {
+ decoderCounters.renderedOutputBufferCount++;
+ outputBuffer.release();
+ outputBuffer = null;
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean feedInputBuffer() throws AudioDecoderException, 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;
+ }
+ inputBuffer.flip();
+ onQueueInputBuffer(inputBuffer);
+ decoder.queueInputBuffer(inputBuffer);
+ decoderReceivedBuffers = true;
+ decoderCounters.inputBufferCount++;
+ inputBuffer = null;
+ return true;
+ }
+
+ 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 processEndOfStream() throws ExoPlaybackException {
+ outputStreamEnded = true;
+ try {
+ audioSink.playToEndOfStream();
+ } catch (AudioSink.WriteException e) {
+ // TODO(internal: b/145658993) Use outputFormat for the call from drainOutputBuffer.
+ throw createRendererException(e, inputFormat);
+ }
+ }
+
+ private void flushDecoder() throws ExoPlaybackException {
+ waitingForKeys = false;
+ if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
+ releaseDecoder();
+ maybeInitDecoder();
+ } else {
+ inputBuffer = null;
+ if (outputBuffer != null) {
+ outputBuffer.release();
+ outputBuffer = null;
+ }
+ decoder.flush();
+ decoderReceivedBuffers = false;
+ }
+ }
+
+ @Override
+ public boolean isEnded() {
+ return outputStreamEnded && audioSink.isEnded();
+ }
+
+ @Override
+ public boolean isReady() {
+ return audioSink.hasPendingData()
+ || (inputFormat != null && !waitingForKeys && (isSourceReady() || outputBuffer != null));
+ }
+
+ @Override
+ public long getPositionUs() {
+ if (getState() == STATE_STARTED) {
+ updateCurrentPosition();
+ }
+ return currentPositionUs;
+ }
+
+ @Override
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {
+ audioSink.setPlaybackParameters(playbackParameters);
+ }
+
+ @Override
+ public PlaybackParameters 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) {
+ audioSink.enableTunnelingV21(tunnelingAudioSessionId);
+ } else {
+ audioSink.disableTunneling();
+ }
+ }
+
+ @Override
+ protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+ audioSink.flush();
+ currentPositionUs = positionUs;
+ allowFirstBufferPositionDiscontinuity = true;
+ allowPositionDiscontinuity = true;
+ inputStreamEnded = false;
+ outputStreamEnded = false;
+ if (decoder != null) {
+ flushDecoder();
+ }
+ }
+
+ @Override
+ protected void onStarted() {
+ audioSink.play();
+ }
+
+ @Override
+ protected void onStopped() {
+ updateCurrentPosition();
+ audioSink.pause();
+ }
+
+ @Override
+ protected void onDisabled() {
+ inputFormat = null;
+ audioTrackNeedsConfigure = true;
+ waitingForKeys = false;
+ try {
+ setSourceDrmSession(null);
+ releaseDecoder();
+ audioSink.reset();
+ } finally {
+ 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;
+ }
+ }
+
+ 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 codecInitializingTimestamp = SystemClock.elapsedRealtime();
+ TraceUtil.beginSection("createAudioDecoder");
+ decoder = createDecoder(inputFormat, mediaCrypto);
+ TraceUtil.endSection();
+ long codecInitializedTimestamp = SystemClock.elapsedRealtime();
+ eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
+ codecInitializedTimestamp - codecInitializingTimestamp);
+ decoderCounters.decoderInitCount++;
+ } catch (AudioDecoderException e) {
+ throw createRendererException(e, inputFormat);
+ }
+ }
+
+ private void releaseDecoder() {
+ inputBuffer = null;
+ outputBuffer = null;
+ decoderReinitializationState = REINITIALIZATION_STATE_NONE;
+ decoderReceivedBuffers = false;
+ if (decoder != null) {
+ decoder.release();
+ decoder = null;
+ decoderCounters.decoderReleaseCount++;
+ }
+ setDecoderDrmSession(null);
+ }
+
+ private void setSourceDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
+ DrmSession.replaceSession(sourceDrmSession, session);
+ sourceDrmSession = session;
+ }
+
+ private void setDecoderDrmSession(@Nullable DrmSession<ExoMediaCrypto> 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<ExoMediaCrypto>) formatHolder.drmSession);
+ } else {
+ sourceDrmSession =
+ getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession);
+ }
+ Format oldFormat = inputFormat;
+ inputFormat = newFormat;
+
+ 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 {
+ // There aren't any final output buffers, so release the decoder immediately.
+ releaseDecoder();
+ maybeInitDecoder();
+ audioTrackNeedsConfigure = true;
+ }
+ }
+
+ encoderDelay = inputFormat.encoderDelay;
+ encoderPadding = inputFormat.encoderPadding;
+
+ eventDispatcher.inputFormatChanged(inputFormat);
+ }
+
+ 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 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) {
+ eventDispatcher.audioSessionId(audioSessionId);
+ SimpleDecoderAudioRenderer.this.onAudioSessionId(audioSessionId);
+ }
+
+ @Override
+ public void onPositionDiscontinuity() {
+ onAudioTrackPositionDiscontinuity();
+ // We are out of sync so allow currentPositionUs to jump backwards.
+ SimpleDecoderAudioRenderer.this.allowPositionDiscontinuity = true;
+ }
+
+ @Override
+ public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
+ eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+ onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Sonic.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Sonic.java
new file mode 100644
index 0000000000..1a0dad4b45
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/Sonic.java
@@ -0,0 +1,506 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ * Copyright (C) 2010 Bill Cox, Sonic Library
+ *
+ * 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.util.Assertions;
+import java.nio.ShortBuffer;
+import java.util.Arrays;
+
+/**
+ * Sonic audio stream processor for time/pitch stretching.
+ * <p>
+ * Based on https://github.com/waywardgeek/sonic.
+ */
+/* package */ final class Sonic {
+
+ 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 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 maxRequiredFrameCount;
+ private final short[] downSampleBuffer;
+
+ private short[] inputBuffer;
+ private int inputFrameCount;
+ private short[] outputBuffer;
+ private int outputFrameCount;
+ private short[] pitchBuffer;
+ private int pitchFrameCount;
+ private int oldRatePosition;
+ private int newRatePosition;
+ private int remainingInputToCopyFrameCount;
+ private int prevPeriod;
+ private int prevMinDiff;
+ private int minDiff;
+ private int maxDiff;
+
+ /**
+ * Creates a new Sonic audio stream processor.
+ *
+ * @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 inputSampleRateHz, int channelCount, float speed, float pitch, int outputSampleRateHz) {
+ this.inputSampleRateHz = inputSampleRateHz;
+ this.channelCount = channelCount;
+ this.speed = speed;
+ this.pitch = 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];
+ }
+
+ /**
+ * Queues remaining data from {@code buffer}, and advances its position by the number of bytes
+ * consumed.
+ *
+ * @param buffer A {@link ShortBuffer} containing input data between its position and limit.
+ */
+ public void queueInput(ShortBuffer buffer) {
+ 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();
+ }
+
+ /**
+ * Gets available output, outputting to the start of {@code buffer}. The buffer's position will be
+ * advanced by the number of bytes written.
+ *
+ * @param buffer A {@link ShortBuffer} into which output will be written.
+ */
+ public void getOutput(ShortBuffer buffer) {
+ 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);
+ }
+
+ /**
+ * Forces generating output using whatever data has been queued already. No extra delay will be
+ * added to the output, but flushing in the middle of words could introduce distortion.
+ */
+ public void queueEndOfStream() {
+ int remainingFrameCount = inputFrameCount;
+ float s = speed / pitch;
+ float r = rate * pitch;
+ int expectedOutputFrames =
+ outputFrameCount + (int) ((remainingFrameCount / s + pitchFrameCount) / r + 0.5f);
+
+ // Add enough silence to flush both input and pitch buffers.
+ inputBuffer =
+ ensureSpaceForAdditionalFrames(
+ inputBuffer, inputFrameCount, remainingFrameCount + 2 * maxRequiredFrameCount);
+ for (int xSample = 0; xSample < 2 * maxRequiredFrameCount * channelCount; xSample++) {
+ inputBuffer[remainingFrameCount * channelCount + xSample] = 0;
+ }
+ inputFrameCount += 2 * maxRequiredFrameCount;
+ processStreamInput();
+ // Throw away any extra frames we generated due to the silence we added.
+ if (outputFrameCount > expectedOutputFrames) {
+ outputFrameCount = expectedOutputFrames;
+ }
+ // Empty input and pitch buffers.
+ inputFrameCount = 0;
+ remainingInputToCopyFrameCount = 0;
+ pitchFrameCount = 0;
+ }
+
+ /** 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.
+
+ /**
+ * 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 removeProcessedInputFrames(int positionFrames) {
+ int remainingFrames = inputFrameCount - positionFrames;
+ System.arraycopy(
+ inputBuffer, positionFrames * channelCount, inputBuffer, 0, remainingFrames * channelCount);
+ inputFrameCount = remainingFrames;
+ }
+
+ 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 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 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];
+ }
+ value /= samplesPerValue;
+ downSampleBuffer[i] = (short) value;
+ }
+ }
+
+ private int findPitchPeriodInRange(short[] samples, int position, int minPeriod, int maxPeriod) {
+ // Find the best frequency match in the range, and given a sample skip multiple. For now, just
+ // find the pitch of the first channel.
+ int bestPeriod = 0;
+ int worstPeriod = 255;
+ int minDiff = 1;
+ int maxDiff = 0;
+ 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 += 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
+ // without overflow.
+ if (diff * bestPeriod < minDiff * period) {
+ minDiff = diff;
+ bestPeriod = period;
+ }
+ if (diff * worstPeriod > maxDiff * period) {
+ maxDiff = diff;
+ worstPeriod = period;
+ }
+ }
+ this.minDiff = minDiff / bestPeriod;
+ this.maxDiff = maxDiff / worstPeriod;
+ return bestPeriod;
+ }
+
+ /**
+ * 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) {
+ if (minDiff == 0 || prevPeriod == 0) {
+ 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) {
+ // 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 = inputSampleRateHz > AMDF_FREQUENCY ? inputSampleRateHz / AMDF_FREQUENCY : 1;
+ if (channelCount == 1 && skip == 1) {
+ period = findPitchPeriodInRange(samples, position, minPeriod, maxPeriod);
+ } else {
+ downSampleInput(samples, position, skip);
+ period = findPitchPeriodInRange(downSampleBuffer, 0, minPeriod / skip, maxPeriod / skip);
+ if (skip != 1) {
+ period *= skip;
+ int minP = period - (skip * 4);
+ int maxP = period + (skip * 4);
+ if (minP < minPeriod) {
+ minP = minPeriod;
+ }
+ if (maxP > maxPeriod) {
+ maxP = maxPeriod;
+ }
+ if (channelCount == 1) {
+ period = findPitchPeriodInRange(samples, position, minP, maxP);
+ } else {
+ downSampleInput(samples, position, 1);
+ period = findPitchPeriodInRange(downSampleBuffer, 0, minP, maxP);
+ }
+ }
+ }
+ if (previousPeriodBetter(minDiff, maxDiff)) {
+ retPeriod = prevPeriod;
+ } else {
+ retPeriod = period;
+ }
+ prevMinDiff = minDiff;
+ prevPeriod = period;
+ return retPeriod;
+ }
+
+ 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 removePitchFrames(int frameCount) {
+ if (frameCount == 0) {
+ return;
+ }
+ 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];
+ short right = in[inPos + channelCount];
+ int position = newRatePosition * oldSampleRate;
+ int leftPosition = oldRatePosition * newSampleRate;
+ int rightPosition = (oldRatePosition + 1) * newSampleRate;
+ int ratio = rightPosition - position;
+ int width = rightPosition - leftPosition;
+ return (short) ((ratio * left + (width - ratio) * right) / width);
+ }
+
+ private void adjustRate(float rate, int originalOutputFrameCount) {
+ if (outputFrameCount == originalOutputFrameCount) {
+ return;
+ }
+ 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(originalOutputFrameCount);
+ // Leave at least one pitch sample in the buffer.
+ for (int position = 0; position < pitchFrameCount - 1; position++) {
+ while ((oldRatePosition + 1) * newSampleRate > newRatePosition * oldSampleRate) {
+ 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++;
+ outputFrameCount++;
+ }
+ oldRatePosition++;
+ if (oldRatePosition == oldSampleRate) {
+ oldRatePosition = 0;
+ Assertions.checkState(newRatePosition == newSampleRate);
+ newRatePosition = 0;
+ }
+ }
+ 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 newFrameCount;
+ if (speed >= 2.0f) {
+ newFrameCount = (int) (period / (speed - 1.0f));
+ } else {
+ newFrameCount = period;
+ remainingInputToCopyFrameCount = (int) (period * (2.0f - speed) / (speed - 1.0f));
+ }
+ outputBuffer = ensureSpaceForAdditionalFrames(outputBuffer, outputFrameCount, newFrameCount);
+ overlapAdd(
+ newFrameCount,
+ channelCount,
+ outputBuffer,
+ outputFrameCount,
+ samples,
+ position,
+ samples,
+ position + period);
+ 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 newFrameCount;
+ if (speed < 0.5f) {
+ newFrameCount = (int) (period * speed / (1.0f - speed));
+ } else {
+ newFrameCount = period;
+ remainingInputToCopyFrameCount = (int) (period * (2.0f * speed - 1.0f) / (1.0f - speed));
+ }
+ 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 (inputFrameCount < maxRequiredFrameCount) {
+ return;
+ }
+ int frameCount = inputFrameCount;
+ int positionFrames = 0;
+ do {
+ if (remainingInputToCopyFrameCount > 0) {
+ positionFrames += copyInputToOutput(positionFrames);
+ } else {
+ int period = findPitchPeriod(inputBuffer, positionFrames);
+ if (speed > 1.0) {
+ positionFrames += period + skipPitchPeriod(inputBuffer, positionFrames, speed, period);
+ } else {
+ positionFrames += insertPitchPeriod(inputBuffer, positionFrames, speed, period);
+ }
+ }
+ } while (positionFrames + maxRequiredFrameCount <= frameCount);
+ removeProcessedInputFrames(positionFrames);
+ }
+
+ private void processStreamInput() {
+ // Resample as many pitch periods as we have buffered on the input.
+ int originalOutputFrameCount = outputFrameCount;
+ float s = speed / pitch;
+ float r = rate * pitch;
+ if (s > 1.00001 || s < 0.99999) {
+ changeSpeed(s);
+ } else {
+ copyToOutput(inputBuffer, 0, inputFrameCount);
+ inputFrameCount = 0;
+ }
+ if (r != 1.0f) {
+ adjustRate(r, originalOutputFrameCount);
+ }
+ }
+
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SonicAudioProcessor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SonicAudioProcessor.java
new file mode 100644
index 0000000000..88a4d884bf
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SonicAudioProcessor.java
@@ -0,0 +1,277 @@
+/*
+ * 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 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;
+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 audio speed/pitch/sample rate.
+ */
+public final class SonicAudioProcessor implements AudioProcessor {
+
+ /**
+ * The maximum allowed playback speed in {@link #setSpeed(float)}.
+ */
+ public static final float MAXIMUM_SPEED = 8.0f;
+ /**
+ * The minimum allowed playback speed in {@link #setSpeed(float)}.
+ */
+ public static final float MINIMUM_SPEED = 0.1f;
+ /**
+ * The maximum allowed pitch in {@link #setPitch(float)}.
+ */
+ public static final float MAXIMUM_PITCH = 8.0f;
+ /**
+ * 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;
+
+ /**
+ * 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 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;
+ private long inputBytes;
+ private long outputBytes;
+ private boolean inputEnded;
+
+ /**
+ * Creates a new Sonic audio processor.
+ */
+ public SonicAudioProcessor() {
+ 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;
+ pendingOutputSampleRate = SAMPLE_RATE_NO_CHANGE;
+ }
+
+ /**
+ * 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) {
+ speed = Util.constrainValue(speed, MINIMUM_SPEED, MAXIMUM_SPEED);
+ if (this.speed != speed) {
+ this.speed = speed;
+ pendingSonicRecreation = true;
+ }
+ return speed;
+ }
+
+ /**
+ * 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) {
+ pitch = Util.constrainValue(pitch, MINIMUM_PITCH, MAXIMUM_PITCH);
+ if (this.pitch != pitch) {
+ this.pitch = pitch;
+ pendingSonicRecreation = true;
+ }
+ return pitch;
+ }
+
+ /**
+ * 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 void setOutputSampleRateHz(int sampleRateHz) {
+ pendingOutputSampleRate = sampleRateHz;
+ }
+
+ /**
+ * 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 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 AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException {
+ if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {
+ throw new UnhandledAudioFormatException(inputAudioFormat);
+ }
+ 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 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();
+ inputBytes += inputSize;
+ sonic.queueInput(shortBuffer);
+ inputBuffer.position(inputBuffer.position() + inputSize);
+ }
+ int outputSize = sonic.getOutputSize();
+ if (outputSize > 0) {
+ if (buffer.capacity() < outputSize) {
+ buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());
+ shortBuffer = buffer.asShortBuffer();
+ } else {
+ buffer.clear();
+ shortBuffer.clear();
+ }
+ sonic.getOutput(shortBuffer);
+ outputBytes += outputSize;
+ buffer.limit(outputSize);
+ outputBuffer = buffer;
+ }
+ }
+
+ @Override
+ public void queueEndOfStream() {
+ if (sonic != null) {
+ sonic.queueEndOfStream();
+ }
+ inputEnded = true;
+ }
+
+ @Override
+ public ByteBuffer getOutput() {
+ ByteBuffer outputBuffer = this.outputBuffer;
+ this.outputBuffer = EMPTY_BUFFER;
+ return outputBuffer;
+ }
+
+ @Override
+ public boolean isEnded() {
+ return inputEnded && (sonic == null || sonic.getOutputSize() == 0);
+ }
+
+ @Override
+ public void flush() {
+ 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;
+ inputEnded = false;
+ }
+
+ @Override
+ public void reset() {
+ 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;
+ pendingOutputSampleRate = SAMPLE_RATE_NO_CHANGE;
+ pendingSonicRecreation = false;
+ sonic = null;
+ inputBytes = 0;
+ outputBytes = 0;
+ inputEnded = false;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/TeeAudioProcessor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/TeeAudioProcessor.java
new file mode 100644
index 0000000000..42f151c5be
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java
new file mode 100644
index 0000000000..1326cf63ee
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/WavUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/WavUtil.java
new file mode 100644
index 0000000000..d1245761aa
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/package-info.java
new file mode 100644
index 0000000000..95c29d7333
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DatabaseIOException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DatabaseIOException.java
new file mode 100644
index 0000000000..4c03addf22
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DatabaseProvider.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DatabaseProvider.java
new file mode 100644
index 0000000000..81deccaf93
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DefaultDatabaseProvider.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/DefaultDatabaseProvider.java
new file mode 100644
index 0000000000..8da3de15c8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/ExoDatabaseProvider.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/ExoDatabaseProvider.java
new file mode 100644
index 0000000000..037442b102
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/VersionTable.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/VersionTable.java
new file mode 100644
index 0000000000..d3174e67b2
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/database/package-info.java
new file mode 100644
index 0000000000..85e0dfa5e3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/Buffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/Buffer.java
new file mode 100644
index 0000000000..ac254fae96
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/Buffer.java
@@ -0,0 +1,100 @@
+/*
+ * 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.decoder;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+
+/**
+ * Base class for buffers with flags.
+ */
+public abstract class Buffer {
+
+ @C.BufferFlags
+ private int flags;
+
+ /**
+ * Clears the buffer.
+ */
+ public void clear() {
+ flags = 0;
+ }
+
+ /**
+ * Returns whether the {@link C#BUFFER_FLAG_DECODE_ONLY} flag is set.
+ */
+ public final boolean isDecodeOnly() {
+ return getFlag(C.BUFFER_FLAG_DECODE_ONLY);
+ }
+
+ /**
+ * Returns whether the {@link C#BUFFER_FLAG_END_OF_STREAM} flag is set.
+ */
+ public final boolean isEndOfStream() {
+ return getFlag(C.BUFFER_FLAG_END_OF_STREAM);
+ }
+
+ /**
+ * Returns whether the {@link C#BUFFER_FLAG_KEY_FRAME} flag is set.
+ */
+ public final boolean isKeyFrame() {
+ 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}.
+ *
+ * @param flags The flags to set, which should be a combination of the {@code C.BUFFER_FLAG_*}
+ * constants.
+ */
+ public final void setFlags(@C.BufferFlags int flags) {
+ this.flags = flags;
+ }
+
+ /**
+ * Adds the {@code flag} to this buffer's flags.
+ *
+ * @param flag The flag to add to this buffer's flags, which should be one of the
+ * {@code C.BUFFER_FLAG_*} constants.
+ */
+ public final void addFlag(@C.BufferFlags int flag) {
+ flags |= flag;
+ }
+
+ /**
+ * Removes the {@code flag} from this buffer's flags, if it is set.
+ *
+ * @param flag The flag to remove.
+ */
+ public final void clearFlag(@C.BufferFlags int flag) {
+ flags &= ~flag;
+ }
+
+ /**
+ * Returns whether the specified flag has been set on this buffer.
+ *
+ * @param flag The flag to check.
+ * @return Whether the flag is set.
+ */
+ protected final boolean getFlag(@C.BufferFlags int flag) {
+ return (flags & flag) == flag;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/CryptoInfo.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/CryptoInfo.java
new file mode 100644
index 0000000000..1bfb0fb06e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/CryptoInfo.java
@@ -0,0 +1,146 @@
+/*
+ * 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.decoder;
+
+import android.annotation.TargetApi;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+/**
+ * Compatibility wrapper for {@link android.media.MediaCodec.CryptoInfo}.
+ */
+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;
+ /**
+ * 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 encryptedBlocks;
+ /**
+ * @see android.media.MediaCodec.CryptoInfo.Pattern
+ */
+ public int clearBlocks;
+
+ private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo;
+ private final PatternHolderV24 patternHolder;
+
+ public CryptoInfo() {
+ frameworkCryptoInfo = new android.media.MediaCodec.CryptoInfo();
+ patternHolder = Util.SDK_INT >= 24 ? new PatternHolderV24(frameworkCryptoInfo) : null;
+ }
+
+ /**
+ * @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, int encryptedBlocks, int clearBlocks) {
+ this.numSubSamples = numSubSamples;
+ this.numBytesOfClearData = numBytesOfClearData;
+ this.numBytesOfEncryptedData = numBytesOfEncryptedData;
+ this.key = key;
+ this.iv = iv;
+ this.mode = mode;
+ 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;
+ frameworkCryptoInfo.key = key;
+ frameworkCryptoInfo.iv = iv;
+ frameworkCryptoInfo.mode = mode;
+ if (Util.SDK_INT >= 24) {
+ patternHolder.set(encryptedBlocks, clearBlocks);
+ }
+ }
+
+ /**
+ * Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
+ *
+ * <p>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;
+ 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);
+ }
+
+ private void set(int encryptedBlocks, int clearBlocks) {
+ pattern.set(encryptedBlocks, clearBlocks);
+ frameworkCryptoInfo.setPattern(pattern);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/Decoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/Decoder.java
new file mode 100644
index 0000000000..8040c04ebe
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/Decoder.java
@@ -0,0 +1,73 @@
+/*
+ * 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.decoder;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A media decoder.
+ *
+ * @param <I> The type of buffer input to the decoder.
+ * @param <O> The type of buffer output from the decoder.
+ * @param <E> The type of exception thrown from the decoder.
+ */
+public interface Decoder<I, O, E extends Exception> {
+
+ /**
+ * Returns the name of the decoder.
+ *
+ * @return The name of the decoder.
+ */
+ String getName();
+
+ /**
+ * Dequeues the next input buffer to be filled and queued to the 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;
+
+ /**
+ * Queues an input buffer to the decoder.
+ *
+ * @param inputBuffer The input buffer.
+ * @throws E If a decoder error has occurred.
+ */
+ void queueInputBuffer(I inputBuffer) throws E;
+
+ /**
+ * Dequeues the next output buffer from the 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;
+
+ /**
+ * Flushes the decoder. Ownership of dequeued input buffers is returned to the decoder. The caller
+ * is still responsible for releasing any dequeued output buffers.
+ */
+ void flush();
+
+ /**
+ * Releases the decoder. Must be called when the decoder is no longer needed.
+ */
+ void release();
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/DecoderCounters.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/DecoderCounters.java
new file mode 100644
index 0000000000..f8bdb9b29a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/DecoderCounters.java
@@ -0,0 +1,105 @@
+/*
+ * 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.decoder;
+
+/**
+ * Maintains decoder event counts, for debugging purposes only.
+ * <p>
+ * Counters should be written from the playback thread only. Counters may be read from any thread.
+ * To ensure that the counter values are made visible across threads, users of this class should
+ * invoke {@link #ensureUpdated()} prior to reading and after writing.
+ */
+public final class DecoderCounters {
+
+ /**
+ * The number of times a decoder has been initialized.
+ */
+ public int decoderInitCount;
+ /**
+ * The number of times a decoder has been released.
+ */
+ public int decoderReleaseCount;
+ /**
+ * The number of queued input buffers.
+ */
+ public int inputBufferCount;
+ /**
+ * The number of skipped input buffers.
+ * <p>
+ * 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.
+ */
+ public int renderedOutputBufferCount;
+ /**
+ * The number of skipped output buffers.
+ * <p>
+ * A skipped output buffer is an output buffer that was deliberately not rendered.
+ */
+ public int skippedOutputBufferCount;
+ /**
+ * The number of dropped buffers.
+ * <p>
+ * 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 droppedBufferCount;
+ /**
+ * The maximum number of dropped buffers without an interleaving rendered output buffer.
+ * <p>
+ * Skipped output buffers are ignored for the purposes of calculating this value.
+ */
+ public int maxConsecutiveDroppedBufferCount;
+ /**
+ * The number of times all buffers to a keyframe were dropped.
+ * <p>
+ * 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
+ * should call this method after updating the counter values. Any other thread should call this
+ * method before reading the counters.
+ */
+ public synchronized void ensureUpdated() {
+ // Do nothing. The use of synchronized ensures a memory barrier should another thread also
+ // call this method.
+ }
+
+ /**
+ * Merges the counts from {@code other} into this instance.
+ *
+ * @param other The {@link DecoderCounters} to merge into this instance.
+ */
+ public void merge(DecoderCounters other) {
+ decoderInitCount += other.decoderInitCount;
+ decoderReleaseCount += other.decoderReleaseCount;
+ inputBufferCount += other.inputBufferCount;
+ skippedInputBufferCount += other.skippedInputBufferCount;
+ renderedOutputBufferCount += other.renderedOutputBufferCount;
+ skippedOutputBufferCount += other.skippedOutputBufferCount;
+ droppedBufferCount += other.droppedBufferCount;
+ maxConsecutiveDroppedBufferCount = Math.max(maxConsecutiveDroppedBufferCount,
+ other.maxConsecutiveDroppedBufferCount);
+ droppedToKeyframeCount += other.droppedToKeyframeCount;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java
new file mode 100644
index 0000000000..254ecfdec8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java
@@ -0,0 +1,209 @@
+/*
+ * 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.decoder;
+
+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.
+ */
+public class DecoderInputBuffer extends Buffer {
+
+ /**
+ * 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
+ })
+ public @interface BufferReplacementMode {}
+ /**
+ * Disallows buffer replacement.
+ */
+ public static final int BUFFER_REPLACEMENT_MODE_DISABLED = 0;
+ /**
+ * Allows buffer replacement using {@link ByteBuffer#allocate(int)}.
+ */
+ public static final int BUFFER_REPLACEMENT_MODE_NORMAL = 1;
+ /**
+ * Allows buffer replacement using {@link ByteBuffer#allocateDirect(int)}.
+ */
+ public static final int BUFFER_REPLACEMENT_MODE_DIRECT = 2;
+
+ /**
+ * {@link CryptoInfo} for encrypted data.
+ */
+ 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.
+ /**
+ * 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 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;
+
+ /**
+ * Creates a new instance for which {@link #isFlagsOnly()} will return true.
+ *
+ * @return A new flags only input buffer.
+ */
+ public static DecoderInputBuffer newFlagsOnlyInstance() {
+ return new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED);
+ }
+
+ /**
+ * @param bufferReplacementMode Determines the behavior of {@link #ensureSpaceForWrite(int)}. One
+ * of {@link #BUFFER_REPLACEMENT_MODE_DISABLED}, {@link #BUFFER_REPLACEMENT_MODE_NORMAL} and
+ * {@link #BUFFER_REPLACEMENT_MODE_DIRECT}.
+ */
+ public DecoderInputBuffer(@BufferReplacementMode int bufferReplacementMode) {
+ this.cryptoInfo = new CryptoInfo();
+ 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.
+ *
+ * <p>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.
+ *
+ * @param length The length of the write that must be accommodated, in bytes.
+ * @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}.
+ */
+ @EnsuresNonNull("data")
+ public void ensureSpaceForWrite(int length) {
+ if (data == null) {
+ data = createReplacementByteBuffer(length);
+ return;
+ }
+ // Check whether the current buffer is sufficient.
+ int capacity = data.capacity();
+ int position = data.position();
+ int requiredCapacity = position + length;
+ if (capacity >= requiredCapacity) {
+ return;
+ }
+ // 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.flip();
+ newData.put(data);
+ }
+ // Set the new buffer.
+ data = newData;
+ }
+
+ /**
+ * Returns whether the buffer is only able to hold flags, meaning {@link #data} is null and
+ * its replacement mode is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}.
+ */
+ public final boolean isFlagsOnly() {
+ return data == null && bufferReplacementMode == BUFFER_REPLACEMENT_MODE_DISABLED;
+ }
+
+ /**
+ * Returns whether the {@link C#BUFFER_FLAG_ENCRYPTED} flag is set.
+ */
+ public final boolean isEncrypted() {
+ return getFlag(C.BUFFER_FLAG_ENCRYPTED);
+ }
+
+ /**
+ * 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
+ public void clear() {
+ super.clear();
+ if (data != null) {
+ data.clear();
+ }
+ if (supplementalData != null) {
+ supplementalData.clear();
+ }
+ waitingForKeys = false;
+ }
+
+ private ByteBuffer createReplacementByteBuffer(int requiredCapacity) {
+ if (bufferReplacementMode == BUFFER_REPLACEMENT_MODE_NORMAL) {
+ return ByteBuffer.allocate(requiredCapacity);
+ } else if (bufferReplacementMode == BUFFER_REPLACEMENT_MODE_DIRECT) {
+ return ByteBuffer.allocateDirect(requiredCapacity);
+ } else {
+ int currentCapacity = data == null ? 0 : data.capacity();
+ throw new IllegalStateException("Buffer too small (" + currentCapacity + " < "
+ + requiredCapacity + ")");
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/OutputBuffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/OutputBuffer.java
new file mode 100644
index 0000000000..73a8a7d2fd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/OutputBuffer.java
@@ -0,0 +1,38 @@
+/*
+ * 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.decoder;
+
+/**
+ * Output buffer decoded by a {@link Decoder}.
+ */
+public abstract class OutputBuffer extends Buffer {
+
+ /**
+ * The presentation timestamp for the buffer, in microseconds.
+ */
+ public long timeUs;
+
+ /**
+ * The number of buffers immediately prior to this one that were skipped in the {@link Decoder}.
+ */
+ public int skippedOutputBufferCount;
+
+ /**
+ * Releases the output buffer for reuse. Must be called when the buffer is no longer needed.
+ */
+ public abstract void release();
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/SimpleDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/SimpleDecoder.java
new file mode 100644
index 0000000000..a193ad3c8e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/SimpleDecoder.java
@@ -0,0 +1,314 @@
+/*
+ * 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.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.ArrayDeque;
+
+/** 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<I, O, E> {
+
+ private final Thread decodeThread;
+
+ private final Object lock;
+ private final ArrayDeque<I> queuedInputBuffers;
+ private final ArrayDeque<O> queuedOutputBuffers;
+ private final I[] availableInputBuffers;
+ private final O[] availableOutputBuffers;
+
+ private int availableInputBufferCount;
+ private int availableOutputBufferCount;
+ private I dequeuedInputBuffer;
+
+ private E exception;
+ private boolean flushed;
+ private boolean released;
+ private int skippedOutputBufferCount;
+
+ /**
+ * @param inputBuffers An array of nulls that will be used to store references to input buffers.
+ * @param outputBuffers An array of nulls that will be used to store references to output buffers.
+ */
+ protected SimpleDecoder(I[] inputBuffers, O[] outputBuffers) {
+ lock = new Object();
+ queuedInputBuffers = new ArrayDeque<>();
+ queuedOutputBuffers = new ArrayDeque<>();
+ availableInputBuffers = inputBuffers;
+ availableInputBufferCount = inputBuffers.length;
+ for (int i = 0; i < availableInputBufferCount; i++) {
+ availableInputBuffers[i] = createInputBuffer();
+ }
+ availableOutputBuffers = outputBuffers;
+ availableOutputBufferCount = outputBuffers.length;
+ for (int i = 0; i < availableOutputBufferCount; i++) {
+ availableOutputBuffers[i] = createOutputBuffer();
+ }
+ decodeThread = new Thread() {
+ @Override
+ public void run() {
+ SimpleDecoder.this.run();
+ }
+ };
+ decodeThread.start();
+ }
+
+ /**
+ * Sets the initial size of each input buffer.
+ * <p>
+ * This method should only be called before the decoder is used (i.e. before the first call to
+ * {@link #dequeueInputBuffer()}.
+ *
+ * @param size The required input buffer size.
+ */
+ protected final void setInitialInputBufferSize(int size) {
+ Assertions.checkState(availableInputBufferCount == availableInputBuffers.length);
+ for (I inputBuffer : availableInputBuffers) {
+ inputBuffer.ensureSpaceForWrite(size);
+ }
+ }
+
+ @Override
+ @Nullable
+ public final I dequeueInputBuffer() throws E {
+ synchronized (lock) {
+ maybeThrowException();
+ Assertions.checkState(dequeuedInputBuffer == null);
+ dequeuedInputBuffer = availableInputBufferCount == 0 ? null
+ : availableInputBuffers[--availableInputBufferCount];
+ return dequeuedInputBuffer;
+ }
+ }
+
+ @Override
+ public final void queueInputBuffer(I inputBuffer) throws E {
+ synchronized (lock) {
+ maybeThrowException();
+ Assertions.checkArgument(inputBuffer == dequeuedInputBuffer);
+ queuedInputBuffers.addLast(inputBuffer);
+ maybeNotifyDecodeLoop();
+ dequeuedInputBuffer = null;
+ }
+ }
+
+ @Override
+ @Nullable
+ public final O dequeueOutputBuffer() throws E {
+ synchronized (lock) {
+ maybeThrowException();
+ if (queuedOutputBuffers.isEmpty()) {
+ return null;
+ }
+ return queuedOutputBuffers.removeFirst();
+ }
+ }
+
+ /**
+ * Releases an output buffer back to the decoder.
+ *
+ * @param outputBuffer The output buffer being released.
+ */
+ @CallSuper
+ protected void releaseOutputBuffer(O outputBuffer) {
+ synchronized (lock) {
+ releaseOutputBufferInternal(outputBuffer);
+ maybeNotifyDecodeLoop();
+ }
+ }
+
+ @Override
+ public final void flush() {
+ synchronized (lock) {
+ flushed = true;
+ skippedOutputBufferCount = 0;
+ if (dequeuedInputBuffer != null) {
+ releaseInputBufferInternal(dequeuedInputBuffer);
+ dequeuedInputBuffer = null;
+ }
+ while (!queuedInputBuffers.isEmpty()) {
+ releaseInputBufferInternal(queuedInputBuffers.removeFirst());
+ }
+ while (!queuedOutputBuffers.isEmpty()) {
+ queuedOutputBuffers.removeFirst().release();
+ }
+ exception = null;
+ }
+ }
+
+ @CallSuper
+ @Override
+ public void release() {
+ synchronized (lock) {
+ released = true;
+ lock.notify();
+ }
+ try {
+ decodeThread.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Throws a decode exception, if there is one.
+ *
+ * @throws E The decode exception.
+ */
+ private void maybeThrowException() throws E {
+ if (exception != null) {
+ throw exception;
+ }
+ }
+
+ /**
+ * Notifies the decode loop if there exists a queued input buffer and an available output buffer
+ * to decode into.
+ * <p>
+ * Should only be called whilst synchronized on the lock object.
+ */
+ private void maybeNotifyDecodeLoop() {
+ if (canDecodeBuffer()) {
+ lock.notify();
+ }
+ }
+
+ private void run() {
+ try {
+ while (decode()) {
+ // Do nothing.
+ }
+ } catch (InterruptedException e) {
+ // Not expected.
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private boolean decode() throws InterruptedException {
+ I inputBuffer;
+ O outputBuffer;
+ boolean resetDecoder;
+
+ // Wait until we have an input buffer to decode, and an output buffer to decode into.
+ synchronized (lock) {
+ while (!released && !canDecodeBuffer()) {
+ lock.wait();
+ }
+ if (released) {
+ return false;
+ }
+ inputBuffer = queuedInputBuffers.removeFirst();
+ outputBuffer = availableOutputBuffers[--availableOutputBufferCount];
+ resetDecoder = flushed;
+ flushed = false;
+ }
+
+ if (inputBuffer.isEndOfStream()) {
+ outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
+ } else {
+ if (inputBuffer.isDecodeOnly()) {
+ outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
+ }
+ @Nullable E exception;
+ try {
+ exception = decode(inputBuffer, outputBuffer, resetDecoder);
+ } catch (RuntimeException e) {
+ // This can occur if a sample is malformed in a way that the decoder is not robust against.
+ // We don't want the process to die in this case, but we do want to propagate the error.
+ exception = createUnexpectedDecodeException(e);
+ } catch (OutOfMemoryError e) {
+ // This can occur if a sample is malformed in a way that causes the decoder 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 to propagate the error.
+ exception = createUnexpectedDecodeException(e);
+ }
+ if (exception != null) {
+ synchronized (lock) {
+ this.exception = exception;
+ }
+ return false;
+ }
+ }
+
+ synchronized (lock) {
+ if (flushed) {
+ outputBuffer.release();
+ } else if (outputBuffer.isDecodeOnly()) {
+ skippedOutputBufferCount++;
+ outputBuffer.release();
+ } else {
+ outputBuffer.skippedOutputBufferCount = skippedOutputBufferCount;
+ skippedOutputBufferCount = 0;
+ queuedOutputBuffers.addLast(outputBuffer);
+ }
+ // Make the input buffer available again.
+ releaseInputBufferInternal(inputBuffer);
+ }
+
+ return true;
+ }
+
+ private boolean canDecodeBuffer() {
+ return !queuedInputBuffers.isEmpty() && availableOutputBufferCount > 0;
+ }
+
+ private void releaseInputBufferInternal(I inputBuffer) {
+ inputBuffer.clear();
+ availableInputBuffers[availableInputBufferCount++] = inputBuffer;
+ }
+
+ private void releaseOutputBufferInternal(O outputBuffer) {
+ outputBuffer.clear();
+ availableOutputBuffers[availableOutputBufferCount++] = outputBuffer;
+ }
+
+ /**
+ * Creates a new input buffer.
+ */
+ protected abstract I createInputBuffer();
+
+ /**
+ * Creates a new output buffer.
+ */
+ protected abstract O createOutputBuffer();
+
+ /**
+ * Creates an exception to propagate for an unexpected decode error.
+ *
+ * @param error The unexpected decode error.
+ * @return The exception to propagate.
+ */
+ protected abstract E createUnexpectedDecodeException(Throwable error);
+
+ /**
+ * Decodes the {@code inputBuffer} and stores any decoded output in {@code outputBuffer}.
+ *
+ * @param inputBuffer The buffer to decode.
+ * @param outputBuffer The output buffer to store decoded data. The flag {@link
+ * C#BUFFER_FLAG_DECODE_ONLY} will be set if the same flag is set on {@code inputBuffer}, but
+ * may be set/unset as required. If the flag is set when the call returns then the output
+ * buffer will not be made available to dequeue. The output buffer may not have been populated
+ * in this case.
+ * @param reset Whether the decoder must be reset before decoding.
+ * @return A decoder exception if an error occurred, or null if decoding was successful.
+ */
+ @Nullable
+ protected abstract E decode(I inputBuffer, O outputBuffer, boolean reset);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java
new file mode 100644
index 0000000000..4b80d38e54
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java
@@ -0,0 +1,65 @@
+/*
+ * 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.decoder;
+
+import androidx.annotation.Nullable;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Buffer for {@link SimpleDecoder} output.
+ */
+public class SimpleOutputBuffer extends OutputBuffer {
+
+ private final SimpleDecoder<?, SimpleOutputBuffer, ?> owner;
+
+ @Nullable public ByteBuffer data;
+
+ public SimpleOutputBuffer(SimpleDecoder<?, SimpleOutputBuffer, ?> owner) {
+ this.owner = owner;
+ }
+
+ /**
+ * Initializes the buffer.
+ *
+ * @param timeUs The presentation timestamp for the buffer, in microseconds.
+ * @param size An upper bound on the size of the data that will be written to the buffer.
+ * @return The {@link #data} buffer, for convenience.
+ */
+ public ByteBuffer init(long timeUs, int size) {
+ this.timeUs = timeUs;
+ if (data == null || data.capacity() < size) {
+ data = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
+ }
+ data.position(0);
+ data.limit(size);
+ return data;
+ }
+
+ @Override
+ public void clear() {
+ super.clear();
+ if (data != null) {
+ data.clear();
+ }
+ }
+
+ @Override
+ public void release() {
+ owner.releaseOutputBuffer(this);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/decoder/package-info.java
new file mode 100644
index 0000000000..78a2c9f2e2
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ClearKeyUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ClearKeyUtil.java
new file mode 100644
index 0000000000..770b8511d9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DecryptionException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DecryptionException.java
new file mode 100644
index 0000000000..989e68befd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DecryptionException.java
@@ -0,0 +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;
+
+/**
+ * Thrown when a non-platform component fails to decrypt data.
+ */
+public class DecryptionException extends Exception {
+
+ /**
+ * 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;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSession.java
new file mode 100644
index 0000000000..ad7ed80580
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<T extends ExoMediaCrypto> implements DrmSession<T> {
+
+ /** 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<T extends ExoMediaCrypto> {
+
+ /**
+ * Called when a session requires provisioning. The manager <em>may</em> call {@link
+ * #provision()} to have this session perform the provisioning operation. The manager
+ * <em>will</em> call {@link DefaultDrmSession#onProvisionCompleted()} when provisioning has
+ * completed, or {@link DefaultDrmSession#onProvisionError} if provisioning fails.
+ *
+ * @param session The session.
+ */
+ void provisionRequired(DefaultDrmSession<T> 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<T extends ExoMediaCrypto> {
+
+ /**
+ * Called immediately after releasing session resources.
+ *
+ * @param session The session.
+ */
+ void onSessionReleased(DefaultDrmSession<T> 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<SchemeData> schemeDatas;
+
+ private final ExoMediaDrm<T> mediaDrm;
+ private final ProvisioningManager<T> provisioningManager;
+ private final ReleaseCallback<T> releaseCallback;
+ private final @DefaultDrmSessionManager.Mode int mode;
+ private final boolean playClearSamplesWithoutKeys;
+ private final boolean isPlaceholderSession;
+ private final HashMap<String, String> keyRequestParameters;
+ private final EventDispatcher<DefaultDrmSessionEventListener> 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<T> mediaDrm,
+ ProvisioningManager<T> provisioningManager,
+ ReleaseCallback<T> releaseCallback,
+ @Nullable List<SchemeData> schemeDatas,
+ @DefaultDrmSessionManager.Mode int mode,
+ boolean playClearSamplesWithoutKeys,
+ boolean isPlaceholderSession,
+ @Nullable byte[] offlineLicenseKeySetId,
+ HashMap<String, String> keyRequestParameters,
+ MediaDrmCallback callback,
+ Looper playbackLooper,
+ EventDispatcher<DefaultDrmSessionEventListener> 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<String, String> 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<Long, Long> 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<Object, Object> requestAndResponse = (Pair<Object, Object>) 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java
new file mode 100644
index 0000000000..35bc7faf28
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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
+ * <em>not</em> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
new file mode 100644
index 0000000000..683862b99a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
@@ -0,0 +1,691 @@
+/*
+ * 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.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+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.DrmSession.DrmSessionException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
+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.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 ExoMediaDrm}. */
+@TargetApi(18)
+public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T> {
+
+ /**
+ * Builder for {@link DefaultDrmSessionManager} instances.
+ *
+ * <p>See {@link #Builder} for the list of default values.
+ */
+ public static final class Builder {
+
+ private final HashMap<String, String> keyRequestParameters;
+ private UUID uuid;
+ private ExoMediaDrm.Provider<ExoMediaCrypto> exoMediaDrmProvider;
+ private boolean multiSession;
+ private int[] useDrmSessionsForClearContentTrackTypes;
+ private boolean playClearSamplesWithoutKeys;
+ private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+
+ /**
+ * Creates a builder with default values. The default values are:
+ *
+ * <ul>
+ * <li>{@link #setKeyRequestParameters keyRequestParameters}: An empty map.
+ * <li>{@link #setUuidAndExoMediaDrmProvider UUID}: {@link C#WIDEVINE_UUID}.
+ * <li>{@link #setUuidAndExoMediaDrmProvider ExoMediaDrm.Provider}: {@link
+ * FrameworkMediaDrm#DEFAULT_PROVIDER}.
+ * <li>{@link #setMultiSession multiSession}: {@code false}.
+ * <li>{@link #setUseDrmSessionsForClearContent useDrmSessionsForClearContent}: No tracks.
+ * <li>{@link #setPlayClearSamplesWithoutKeys playClearSamplesWithoutKeys}: {@code false}.
+ * <li>{@link #setLoadErrorHandlingPolicy LoadErrorHandlingPolicy}: {@link
+ * DefaultLoadErrorHandlingPolicy}.
+ * </ul>
+ */
+ @SuppressWarnings("unchecked")
+ public Builder() {
+ keyRequestParameters = new HashMap<>();
+ uuid = C.WIDEVINE_UUID;
+ exoMediaDrmProvider = (ExoMediaDrm.Provider) FrameworkMediaDrm.DEFAULT_PROVIDER;
+ loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
+ useDrmSessionsForClearContentTrackTypes = new int[0];
+ }
+
+ /**
+ * Sets the key request parameters to pass as the last argument to {@link
+ * ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}.
+ *
+ * <p>Custom data for PlayReady should be set under {@link #PLAYREADY_CUSTOM_DATA_KEY}.
+ *
+ * @param keyRequestParameters A map with parameters.
+ * @return This builder.
+ */
+ public Builder setKeyRequestParameters(Map<String, String> keyRequestParameters) {
+ this.keyRequestParameters.clear();
+ this.keyRequestParameters.putAll(Assertions.checkNotNull(keyRequestParameters));
+ return this;
+ }
+
+ /**
+ * 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.
+ */
+ @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.
+ *
+ * <p>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.
+ *
+ * <p>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<ExoMediaCrypto> build(MediaDrmCallback mediaDrmCallback) {
+ return new DefaultDrmSessionManager<>(
+ uuid,
+ exoMediaDrmProvider,
+ mediaDrmCallback,
+ keyRequestParameters,
+ multiSession,
+ useDrmSessionsForClearContentTrackTypes,
+ playClearSamplesWithoutKeys,
+ loadErrorHandlingPolicy);
+ }
+ }
+
+ /**
+ * 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. 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 {}
+ /**
+ * Loads and refreshes (if necessary) a license for playback. Supports streaming and offline
+ * licenses.
+ */
+ public static final int MODE_PLAYBACK = 0;
+ /** 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 = "DefaultDrmSessionMgr";
+
+ private final UUID uuid;
+ private final ExoMediaDrm.Provider<T> exoMediaDrmProvider;
+ private final MediaDrmCallback callback;
+ private final HashMap<String, String> keyRequestParameters;
+ private final EventDispatcher<DefaultDrmSessionEventListener> eventDispatcher;
+ private final boolean multiSession;
+ private final int[] useDrmSessionsForClearContentTrackTypes;
+ private final boolean playClearSamplesWithoutKeys;
+ private final ProvisioningManagerImpl provisioningManagerImpl;
+ private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+
+ private final List<DefaultDrmSession<T>> sessions;
+ private final List<DefaultDrmSession<T>> provisioningSessions;
+
+ private int prepareCallsCount;
+ @Nullable private ExoMediaDrm<T> exoMediaDrm;
+ @Nullable private DefaultDrmSession<T> placeholderDrmSession;
+ @Nullable private DefaultDrmSession<T> noMultiSessionDrmSession;
+ @Nullable private Looper playbackLooper;
+ private int mode;
+ @Nullable private byte[] offlineLicenseKeySetId;
+
+ /* package */ volatile @Nullable MediaDrmHandler mediaDrmHandler;
+
+ /**
+ * @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.
+ * @deprecated Use {@link Builder} instead.
+ */
+ @SuppressWarnings("deprecation")
+ @Deprecated
+ public DefaultDrmSessionManager(
+ UUID uuid,
+ ExoMediaDrm<T> exoMediaDrm,
+ MediaDrmCallback callback,
+ @Nullable HashMap<String, String> 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 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.
+ * @deprecated Use {@link Builder} instead.
+ */
+ @Deprecated
+ public DefaultDrmSessionManager(
+ UUID uuid,
+ ExoMediaDrm<T> exoMediaDrm,
+ MediaDrmCallback callback,
+ @Nullable HashMap<String, String> 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<T> exoMediaDrm,
+ MediaDrmCallback callback,
+ @Nullable HashMap<String, String> 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<T> exoMediaDrmProvider,
+ MediaDrmCallback callback,
+ HashMap<String, String> 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.exoMediaDrmProvider = exoMediaDrmProvider;
+ this.callback = callback;
+ 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<>();
+ }
+
+ /**
+ * Adds a {@link DefaultDrmSessionEventListener} to listen to drm session events.
+ *
+ * @param handler A handler to use when delivering events to {@code eventListener}.
+ * @param eventListener A listener of events.
+ */
+ public final void addListener(Handler handler, DefaultDrmSessionEventListener eventListener) {
+ eventDispatcher.addListener(handler, eventListener);
+ }
+
+ /**
+ * Removes a {@link DefaultDrmSessionEventListener} from the list of drm session event listeners.
+ *
+ * @param eventListener The listener to remove.
+ */
+ 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)} or {@link
+ * #acquirePlaceholderSession} is called.
+ *
+ * <p>By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when
+ * required.
+ *
+ * <p>{@code mode} must be one of these:
+ *
+ * <ul>
+ * <li>{@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is
+ * requested otherwise the offline license is restored.
+ * <li>{@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license
+ * is restored.
+ * <li>{@link #MODE_DOWNLOAD}: If {@code offlineLicenseKeySetId} is null, an offline license is
+ * requested otherwise the offline license is renewed.
+ * <li>{@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline
+ * license is released.
+ * </ul>
+ *
+ * @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, @Nullable byte[] offlineLicenseKeySetId) {
+ Assertions.checkState(sessions.isEmpty());
+ if (mode == MODE_QUERY || mode == MODE_RELEASE) {
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ }
+ this.mode = mode;
+ this.offlineLicenseKeySetId = offlineLicenseKeySetId;
+ }
+
+ // DrmSessionManager implementation.
+
+ @Override
+ public final void prepare() {
+ if (prepareCallsCount++ == 0) {
+ Assertions.checkState(exoMediaDrm == null);
+ exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid);
+ exoMediaDrm.setOnEventListener(new MediaDrmEventListener());
+ }
+ }
+
+ @Override
+ public final void release() {
+ if (--prepareCallsCount == 0) {
+ Assertions.checkNotNull(exoMediaDrm).release();
+ exoMediaDrm = null;
+ }
+ }
+
+ @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<SchemeData> 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;
+ }
+ }
+ 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<T> acquirePlaceholderSession(Looper playbackLooper, int trackType) {
+ assertExpectedPlaybackLooper(playbackLooper);
+ ExoMediaDrm<T> 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<T> placeholderDrmSession =
+ createNewDefaultSession(
+ /* schemeDatas= */ Collections.emptyList(), /* isPlaceholderSession= */ true);
+ sessions.add(placeholderDrmSession);
+ this.placeholderDrmSession = placeholderDrmSession;
+ }
+ placeholderDrmSession.acquire();
+ return placeholderDrmSession;
+ }
+
+ @Override
+ public DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData) {
+ assertExpectedPlaybackLooper(playbackLooper);
+ maybeCreateMediaDrmHandler(playbackLooper);
+
+ @Nullable List<SchemeData> 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<T> session;
+ if (!multiSession) {
+ session = noMultiSessionDrmSession;
+ } else {
+ // Only use an existing session if it has matching init data.
+ session = null;
+ for (DefaultDrmSession<T> existingSession : sessions) {
+ if (Util.areEqual(existingSession.schemeDatas, schemeDatas)) {
+ session = existingSession;
+ break;
+ }
+ }
+ }
+
+ if (session == null) {
+ // Create a new session.
+ session = createNewDefaultSession(schemeDatas, /* isPlaceholderSession= */ false);
+ if (!multiSession) {
+ noMultiSessionDrmSession = session;
+ }
+ sessions.add(session);
+ }
+ session.acquire();
+ return session;
+ }
+
+ @Override
+ @Nullable
+ public Class<T> getExoMediaCryptoType(DrmInitData drmInitData) {
+ return canAcquireSession(drmInitData)
+ ? Assertions.checkNotNull(exoMediaDrm).getExoMediaCryptoType()
+ : null;
+ }
+
+ // Internal methods.
+
+ 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<T> createNewDefaultSession(
+ @Nullable List<SchemeData> 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<T> 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<SchemeData> getSchemeDatas(
+ DrmInitData drmInitData, UUID uuid, boolean allowMissingData) {
+ // Look for matching scheme data (matching the Common PSSH box for ClearKey).
+ List<SchemeData> 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);
+ }
+ }
+ return matchingSchemeDatas;
+ }
+
+ @SuppressLint("HandlerLeak")
+ private class MediaDrmHandler extends Handler {
+
+ public MediaDrmHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ byte[] sessionId = (byte[]) msg.obj;
+ if (sessionId == null) {
+ // The event is not associated with any particular session.
+ return;
+ }
+ for (DefaultDrmSession<T> session : sessions) {
+ if (session.hasSessionId(sessionId)) {
+ session.onMediaDrmEvent(msg.what);
+ return;
+ }
+ }
+ }
+ }
+
+ private class ProvisioningManagerImpl implements DefaultDrmSession.ProvisioningManager<T> {
+ @Override
+ public void provisionRequired(DefaultDrmSession<T> 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<T> session : provisioningSessions) {
+ session.onProvisionCompleted();
+ }
+ provisioningSessions.clear();
+ }
+
+ @Override
+ public void onProvisionError(Exception error) {
+ for (DefaultDrmSession<T> session : provisioningSessions) {
+ session.onProvisionError(error);
+ }
+ provisioningSessions.clear();
+ }
+ }
+
+ private class MediaDrmEventListener implements OnEventListener<T> {
+
+ @Override
+ public void onEvent(
+ ExoMediaDrm<? extends T> md,
+ @Nullable byte[] sessionId,
+ int event,
+ int extra,
+ @Nullable byte[] data) {
+ Assertions.checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget();
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmInitData.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmInitData.java
new file mode 100644
index 0000000000..2a25d1deb4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmInitData.java
@@ -0,0 +1,425 @@
+/*
+ * 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.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;
+import java.util.UUID;
+
+/**
+ * Initialization data for one or more DRM schemes.
+ */
+public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
+
+ /**
+ * Merges {@link DrmInitData} obtained from a media manifest and a media stream.
+ *
+ * <p>The result is generated as follows.
+ *
+ * <ol>
+ * <li>Include all {@link SchemeData}s from {@code manifestData} where {@link
+ * SchemeData#hasData()} is true.
+ * <li>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.
+ * <li>If available, the scheme type from the manifest is used. If not, the scheme type from the
+ * media is used.
+ * </ol>
+ *
+ * @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<SchemeData> 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.
+ */
+ public final int schemeDataCount;
+
+ /**
+ * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.
+ */
+ public DrmInitData(List<SchemeData> schemeDatas) {
+ 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<SchemeData> schemeDatas) {
+ this(schemeType, false, schemeDatas.toArray(new SchemeData[0]));
+ }
+
+ /**
+ * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.
+ */
+ public DrmInitData(SchemeData... schemeDatas) {
+ this(null, 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();
+ }
+ 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) {
+ 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)) {
+ return schemeData;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Retrieves the {@link SchemeData} at a given 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) {
+ int result = (schemeType == null ? 0 : schemeType.hashCode());
+ result = 31 * result + Arrays.hashCode(schemeDatas);
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ DrmInitData other = (DrmInitData) obj;
+ return Util.areEqual(schemeType, other.schemeType)
+ && Arrays.equals(schemeDatas, other.schemeDatas);
+ }
+
+ @Override
+ public int compare(SchemeData first, SchemeData second) {
+ return C.UUID_NIL.equals(first.uuid) ? (C.UUID_NIL.equals(second.uuid) ? 0 : 1)
+ : first.uuid.compareTo(second.uuid);
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(schemeType);
+ dest.writeTypedArray(schemeDatas, 0);
+ }
+
+ public static final Parcelable.Creator<DrmInitData> CREATOR =
+ new Parcelable.Creator<DrmInitData>() {
+
+ @Override
+ public DrmInitData createFromParcel(Parcel in) {
+ return new DrmInitData(in);
+ }
+
+ @Override
+ public DrmInitData[] newArray(int size) {
+ return new DrmInitData[size];
+ }
+
+ };
+
+ // Internal methods.
+
+ private static boolean containsSchemeDataWithUuid(
+ ArrayList<SchemeData> 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.
+ */
+ public static final class SchemeData implements Parcelable {
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is universal (i.e.
+ * applies to all schemes).
+ */
+ private final UUID uuid;
+ /** 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. 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 See {@link #mimeType}.
+ * @param data See {@link #data}.
+ */
+ 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 licenseServerUrl See {@link #licenseServerUrl}.
+ * @param mimeType See {@link #mimeType}.
+ * @param data See {@link #data}.
+ */
+ 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 = data;
+ }
+
+ /* package */ SchemeData(Parcel in) {
+ uuid = new UUID(in.readLong(), in.readLong());
+ licenseServerUrl = in.readString();
+ mimeType = Util.castNonNull(in.readString());
+ data = in.createByteArray();
+ }
+
+ /**
+ * Returns whether this initialization data applies to the specified scheme.
+ *
+ * @param schemeUuid The scheme {@link UUID}.
+ * @return Whether this initialization data applies to the specified scheme.
+ */
+ public boolean matches(UUID schemeUuid) {
+ 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(@Nullable Object obj) {
+ if (!(obj instanceof SchemeData)) {
+ return false;
+ }
+ if (obj == this) {
+ return true;
+ }
+ SchemeData other = (SchemeData) obj;
+ return Util.areEqual(licenseServerUrl, other.licenseServerUrl)
+ && Util.areEqual(mimeType, other.mimeType)
+ && Util.areEqual(uuid, other.uuid)
+ && Arrays.equals(data, other.data);
+ }
+
+ @Override
+ 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;
+ }
+ return hashCode;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(uuid.getMostSignificantBits());
+ dest.writeLong(uuid.getLeastSignificantBits());
+ dest.writeString(licenseServerUrl);
+ dest.writeString(mimeType);
+ dest.writeByteArray(data);
+ }
+
+ public static final Parcelable.Creator<SchemeData> CREATOR =
+ new Parcelable.Creator<SchemeData>() {
+
+ @Override
+ public SchemeData createFromParcel(Parcel in) {
+ return new SchemeData(in);
+ }
+
+ @Override
+ public SchemeData[] newArray(int size) {
+ return new SchemeData[size];
+ }
+
+ };
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSession.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSession.java
new file mode 100644
index 0000000000..7a9af2684f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSession.java
@@ -0,0 +1,144 @@
+/*
+ * 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.media.MediaDrm;
+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;
+
+/**
+ * A DRM session.
+ */
+public interface DrmSession<T extends ExoMediaCrypto> {
+
+ /**
+ * 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 <T extends ExoMediaCrypto> void replaceSession(
+ @Nullable DrmSession<T> previousSession, @Nullable DrmSession<T> newSession) {
+ if (previousSession == newSession) {
+ // Do nothing.
+ return;
+ }
+ if (newSession != null) {
+ newSession.acquire();
+ }
+ if (previousSession != null) {
+ previousSession.release();
+ }
+ }
+
+ /** 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. 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_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 = 1;
+ /**
+ * The session is being opened.
+ */
+ int STATE_OPENING = 2;
+ /** The session is open, but does not have keys required for decryption. */
+ int STATE_OPENED = 3;
+ /** The session is open and has keys required for decryption. */
+ int STATE_OPENED_WITH_KEYS = 4;
+
+ /**
+ * 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 whether this session allows playback of clear samples prior to keys being loaded. */
+ default boolean playClearSamplesWithoutKeys() {
+ return false;
+ }
+
+ /**
+ * Returns the cause of the error state, or null if {@link #getState()} is not {@link
+ * #STATE_ERROR}.
+ */
+ @Nullable
+ DrmSessionException getError();
+
+ /**
+ * 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.
+ *
+ * <p>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 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<String, String> queryKeyStatus();
+
+ /**
+ * 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSessionManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSessionManager.java
new file mode 100644
index 0000000000..bf98a0a658
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSessionManager.java
@@ -0,0 +1,121 @@
+/*
+ * 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.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.
+ */
+public interface DrmSessionManager<T extends ExoMediaCrypto> {
+
+ /** Returns {@link #DUMMY}. */
+ @SuppressWarnings("unchecked")
+ static <T extends ExoMediaCrypto> DrmSessionManager<T> getDummyDrmSessionManager() {
+ return (DrmSessionManager<T>) DUMMY;
+ }
+
+ /** {@link DrmSessionManager} that supports no DRM schemes. */
+ DrmSessionManager<ExoMediaCrypto> DUMMY =
+ new DrmSessionManager<ExoMediaCrypto>() {
+
+ @Override
+ public boolean canAcquireSession(DrmInitData drmInitData) {
+ return false;
+ }
+
+ @Override
+ public DrmSession<ExoMediaCrypto> acquireSession(
+ Looper playbackLooper, DrmInitData drmInitData) {
+ return new ErrorStateDrmSession<>(
+ new DrmSession.DrmSessionException(
+ new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)));
+ }
+
+ @Override
+ @Nullable
+ public Class<ExoMediaCrypto> getExoMediaCryptoType(DrmInitData drmInitData) {
+ return null;
+ }
+ };
+
+ /**
+ * Acquires any required resources.
+ *
+ * <p>{@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.
+ *
+ * <p>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 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<T> 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<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData);
+
+ /**
+ * 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}.
+ */
+ @Nullable
+ Class<? extends ExoMediaCrypto> getExoMediaCryptoType(DrmInitData drmInitData);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java
new file mode 100644
index 0000000000..b6a66ceac0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<T extends ExoMediaCrypto> implements ExoMediaDrm<T> {
+
+ /** Returns a new instance. */
+ @SuppressWarnings("unchecked")
+ public static <T extends ExoMediaCrypto> DummyExoMediaDrm<T> getInstance() {
+ return (DummyExoMediaDrm<T>) new DummyExoMediaDrm<>();
+ }
+
+ @Override
+ public void setOnEventListener(OnEventListener<? super T> listener) {
+ // Do nothing.
+ }
+
+ @Override
+ public void setOnKeyStatusChangeListener(OnKeyStatusChangeListener<? super T> 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<DrmInitData.SchemeData> schemeDatas,
+ int keyType,
+ @Nullable HashMap<String, String> 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<String, String> 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<T> getExoMediaCryptoType() {
+ // No ExoMediaCrypto type is supported.
+ return null;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java
new file mode 100644
index 0000000000..97d0ecaaa4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<T extends ExoMediaCrypto> implements DrmSession<T> {
+
+ 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<String, String> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaCrypto.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaCrypto.java
new file mode 100644
index 0000000000..a12b212799
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaCrypto.java
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+/** An opaque {@link android.media.MediaCrypto} equivalent. */
+public interface ExoMediaCrypto {}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaDrm.java
new file mode 100644
index 0000000000..1e851a7c0b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaDrm.java
@@ -0,0 +1,342 @@
+/*
+ * 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.media.DeniedByServerException;
+import android.media.MediaCryptoException;
+import android.media.MediaDrm;
+import android.media.MediaDrmException;
+import android.media.NotProvisionedException;
+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}.
+ *
+ * <h3>Reference counting</h3>
+ *
+ * <p>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.
+ *
+ * <p>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<T extends ExoMediaCrypto> {
+
+ /** {@link ExoMediaDrm} instances provider. */
+ interface Provider<T extends ExoMediaCrypto> {
+
+ /**
+ * 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<T> acquireExoMediaDrm(UUID uuid);
+ }
+
+ /**
+ * Provides an {@link ExoMediaDrm} instance owned by the app.
+ *
+ * <p>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<T extends ExoMediaCrypto> implements Provider<T> {
+
+ private final ExoMediaDrm<T> exoMediaDrm;
+
+ /** Creates an instance that provides the given {@link ExoMediaDrm}. */
+ public AppManagedProvider(ExoMediaDrm<T> exoMediaDrm) {
+ this.exoMediaDrm = exoMediaDrm;
+ }
+
+ @Override
+ public ExoMediaDrm<T> 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
+ */
+ interface OnEventListener<T extends ExoMediaCrypto> {
+ /**
+ * 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 A secondary error code.
+ * @param data Optional byte array of data that may be associated with the event.
+ */
+ void onEvent(
+ ExoMediaDrm<? extends T> mediaDrm,
+ @Nullable byte[] sessionId,
+ int event,
+ int extra,
+ @Nullable byte[] data);
+ }
+
+ /**
+ * @see android.media.MediaDrm.OnKeyStatusChangeListener
+ */
+ interface OnKeyStatusChangeListener<T extends ExoMediaCrypto> {
+ /**
+ * 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<? extends T> mediaDrm,
+ byte[] sessionId,
+ List<KeyStatus> exoKeyInformation,
+ boolean hasNewUsableKey);
+ }
+
+ /** @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;
+ }
+
+ }
+
+ /**
+ * @see MediaDrm#setOnEventListener(MediaDrm.OnEventListener)
+ */
+ void setOnEventListener(OnEventListener<? super T> listener);
+
+ /**
+ * @see MediaDrm#setOnKeyStatusChangeListener(MediaDrm.OnKeyStatusChangeListener, Handler)
+ */
+ void setOnKeyStatusChangeListener(OnKeyStatusChangeListener<? super T> listener);
+
+ /**
+ * @see MediaDrm#openSession()
+ */
+ byte[] openSession() throws MediaDrmException;
+
+ /**
+ * @see MediaDrm#closeSession(byte[])
+ */
+ 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,
+ @Nullable List<SchemeData> schemeDatas,
+ int keyType,
+ @Nullable HashMap<String, String> optionalParameters)
+ throws NotProvisionedException;
+
+ /** @see MediaDrm#provideKeyResponse(byte[], byte[]) */
+ @Nullable
+ byte[] provideKeyResponse(byte[] scope, byte[] response)
+ throws NotProvisionedException, DeniedByServerException;
+
+ /**
+ * @see MediaDrm#getProvisionRequest()
+ */
+ ProvisionRequest getProvisionRequest();
+
+ /**
+ * @see MediaDrm#provideProvisionResponse(byte[])
+ */
+ void provideProvisionResponse(byte[] response) throws DeniedByServerException;
+
+ /**
+ * @see MediaDrm#queryKeyStatus(byte[])
+ */
+ Map<String, String> queryKeyStatus(byte[] sessionId);
+
+ /**
+ * Increments the reference count. When the caller no longer needs to use the instance, it must
+ * call {@link #release()} to decrement the reference count.
+ *
+ * <p>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();
+
+ /**
+ * @see MediaDrm#restoreKeys(byte[], byte[])
+ */
+ void restoreKeys(byte[] sessionId, byte[] keySetId);
+
+ /**
+ * Returns drm metrics. May be null if unavailable.
+ *
+ * @see MediaDrm#getMetrics()
+ */
+ @Nullable
+ PersistableBundle getMetrics();
+
+ /**
+ * @see MediaDrm#getPropertyString(String)
+ */
+ String getPropertyString(String propertyName);
+
+ /**
+ * @see MediaDrm#getPropertyByteArray(String)
+ */
+ byte[] getPropertyByteArray(String propertyName);
+
+ /**
+ * @see MediaDrm#setPropertyString(String, String)
+ */
+ void setPropertyString(String propertyName, String value);
+
+ /**
+ * @see MediaDrm#setPropertyByteArray(String, byte[])
+ */
+ void setPropertyByteArray(String propertyName, byte[] value);
+
+ /**
+ * @see android.media.MediaCrypto#MediaCrypto(UUID, byte[])
+ * @param sessionId The DRM session ID.
+ * @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data.
+ * @throws MediaCryptoException If the instance can't be created.
+ */
+ 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<T> getExoMediaCryptoType();
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java
new file mode 100644
index 0000000000..bb3a9b272b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java
@@ -0,0 +1,59 @@
+/*
+ * 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.media.MediaCrypto;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.UUID;
+
+/**
+ * An {@link ExoMediaCrypto} implementation that contains the necessary information to build or
+ * update a framework {@link MediaCrypto}.
+ */
+public final class FrameworkMediaCrypto implements ExoMediaCrypto {
+
+ /**
+ * 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
+
+ /** 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;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
new file mode 100644
index 0000000000..10ca857448
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
@@ -0,0 +1,440 @@
+/*
+ * 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.DeniedByServerException;
+import android.media.MediaCryptoException;
+import android.media.MediaDrm;
+import android.media.MediaDrmException;
+import android.media.NotProvisionedException;
+import android.media.UnsupportedSchemeException;
+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(23)
+@RequiresApi(18)
+public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto> {
+
+ private static final String TAG = "FrameworkMediaDrm";
+
+ /**
+ * {@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<FrameworkMediaCrypto> 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 = "<LA_URL>" + MOCK_LA_URL_VALUE + "</LA_URL>";
+ 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.
+ * @throws UnsupportedDrmException If the DRM scheme is unsupported or cannot be instantiated.
+ */
+ public static FrameworkMediaDrm newInstance(UUID uuid) throws UnsupportedDrmException {
+ try {
+ return new FrameworkMediaDrm(uuid);
+ } catch (UnsupportedSchemeException e) {
+ throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, e);
+ } catch (Exception e) {
+ throw new UnsupportedDrmException(UnsupportedDrmException.REASON_INSTANTIATION_ERROR, e);
+ }
+ }
+
+ private FrameworkMediaDrm(UUID uuid) throws UnsupportedSchemeException {
+ Assertions.checkNotNull(uuid);
+ Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
+ this.uuid = uuid;
+ this.mediaDrm = new MediaDrm(adjustUuid(uuid));
+ // Creators of an instance automatically acquire ownership of the created instance.
+ referenceCount = 1;
+ if (C.WIDEVINE_UUID.equals(uuid) && needsForceWidevineL3Workaround()) {
+ forceWidevineL3(mediaDrm);
+ }
+ }
+
+ @Override
+ public void setOnEventListener(
+ final ExoMediaDrm.OnEventListener<? super FrameworkMediaCrypto> listener) {
+ mediaDrm.setOnEventListener(
+ listener == null
+ ? null
+ : (mediaDrm, sessionId, event, extra, data) ->
+ listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data));
+ }
+
+ @Override
+ public void setOnKeyStatusChangeListener(
+ final ExoMediaDrm.OnKeyStatusChangeListener<? super FrameworkMediaCrypto> listener) {
+ if (Util.SDK_INT < 23) {
+ throw new UnsupportedOperationException();
+ }
+
+ mediaDrm.setOnKeyStatusChangeListener(
+ listener == null
+ ? null
+ : (mediaDrm, sessionId, keyInfo, hasNewUsableKey) -> {
+ List<KeyStatus> 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();
+ }
+
+ @Override
+ public void closeSession(byte[] sessionId) {
+ mediaDrm.closeSession(sessionId);
+ }
+
+ @Override
+ public KeyRequest getKeyRequest(
+ byte[] scope,
+ @Nullable List<DrmInitData.SchemeData> schemeDatas,
+ int keyType,
+ @Nullable HashMap<String, String> 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);
+
+ 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 request = mediaDrm.getProvisionRequest();
+ return new ProvisionRequest(request.getData(), request.getDefaultUrl());
+ }
+
+ @Override
+ public void provideProvisionResponse(byte[] response) throws DeniedByServerException {
+ mediaDrm.provideProvisionResponse(response);
+ }
+
+ @Override
+ public Map<String, String> queryKeyStatus(byte[] sessionId) {
+ return mediaDrm.queryKeyStatus(sessionId);
+ }
+
+ @Override
+ public synchronized void acquire() {
+ Assertions.checkState(referenceCount > 0);
+ referenceCount++;
+ }
+
+ @Override
+ public synchronized void release() {
+ if (--referenceCount == 0) {
+ mediaDrm.release();
+ }
+ }
+
+ @Override
+ public void restoreKeys(byte[] sessionId, byte[] keySetId) {
+ mediaDrm.restoreKeys(sessionId, keySetId);
+ }
+
+ @Override
+ @Nullable
+ @TargetApi(28)
+ public PersistableBundle getMetrics() {
+ if (Util.SDK_INT < 28) {
+ return null;
+ }
+ return mediaDrm.getMetrics();
+ }
+
+ @Override
+ public String getPropertyString(String propertyName) {
+ return mediaDrm.getPropertyString(propertyName);
+ }
+
+ @Override
+ public byte[] getPropertyByteArray(String propertyName) {
+ return mediaDrm.getPropertyByteArray(propertyName);
+ }
+
+ @Override
+ public void setPropertyString(String propertyName, String value) {
+ mediaDrm.setPropertyString(propertyName, value);
+ }
+
+ @Override
+ public void setPropertyByteArray(String propertyName, byte[] value) {
+ mediaDrm.setPropertyByteArray(propertyName, value);
+ }
+
+ @Override
+ public FrameworkMediaCrypto createMediaCrypto(byte[] initData) throws MediaCryptoException {
+ // Work around a bug prior to Lollipop where L1 Widevine forced into L3 mode would still
+ // indicate that it required secure video decoders [Internal ref: b/11428937].
+ boolean forceAllowInsecureDecoderComponents = Util.SDK_INT < 21
+ && C.WIDEVINE_UUID.equals(uuid) && "L3".equals(getPropertyString("securityLevel"));
+ return new FrameworkMediaCrypto(
+ adjustUuid(uuid), initData, forceAllowInsecureDecoderComponents);
+ }
+
+ @Override
+ public Class<FrameworkMediaCrypto> getExoMediaCryptoType() {
+ return FrameworkMediaCrypto.class;
+ }
+
+ private static SchemeData getSchemeData(UUID uuid, List<SchemeData> 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.
+ *
+ * <p>See <a href="https://github.com/google/ExoPlayer/issues/4413">GitHub issue #4413</a>.
+ */
+ 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>")) {
+ // 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("</DATA>");
+ if (endOfDataTagIndex == -1) {
+ Log.w(TAG, "Could not find the </DATA> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java
new file mode 100644
index 0000000000..baa5bf0916
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java
@@ -0,0 +1,195 @@
+/*
+ * 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.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.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;
+
+/**
+ * A {@link MediaDrmCallback} that makes requests using {@link HttpDataSource} instances.
+ */
+@TargetApi(18)
+public final class HttpMediaDrmCallback implements MediaDrmCallback {
+
+ private static final int MAX_MANUAL_REDIRECTS = 5;
+
+ private final HttpDataSource.Factory dataSourceFactory;
+ private final String defaultLicenseUrl;
+ private final boolean forceDefaultLicenseUrl;
+ private final Map<String, String> keyRequestProperties;
+
+ /**
+ * @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 defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) {
+ this(defaultLicenseUrl, false, dataSourceFactory);
+ }
+
+ /**
+ * @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.
+ */
+ public HttpMediaDrmCallback(String defaultLicenseUrl, boolean forceDefaultLicenseUrl,
+ HttpDataSource.Factory dataSourceFactory) {
+ this.dataSourceFactory = dataSourceFactory;
+ this.defaultLicenseUrl = defaultLicenseUrl;
+ this.forceDefaultLicenseUrl = forceDefaultLicenseUrl;
+ this.keyRequestProperties = new HashMap<>();
+ }
+
+ /**
+ * Sets a header for key requests made by the callback.
+ *
+ * @param name The name of the header field.
+ * @param value The value of the field.
+ */
+ public void setKeyRequestProperty(String name, String value) {
+ Assertions.checkNotNull(name);
+ Assertions.checkNotNull(value);
+ synchronized (keyRequestProperties) {
+ keyRequestProperties.put(name, value);
+ }
+ }
+
+ /**
+ * Clears a header for key requests made by the callback.
+ *
+ * @param name The name of the header field.
+ */
+ public void clearKeyRequestProperty(String name) {
+ Assertions.checkNotNull(name);
+ synchronized (keyRequestProperties) {
+ keyRequestProperties.remove(name);
+ }
+ }
+
+ /**
+ * Clears all headers for key requests made by the callback.
+ */
+ public void clearAllKeyRequestProperties() {
+ synchronized (keyRequestProperties) {
+ keyRequestProperties.clear();
+ }
+ }
+
+ @Override
+ public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
+ 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.getLicenseServerUrl();
+ if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) {
+ url = defaultLicenseUrl;
+ }
+ Map<String, String> requestProperties = new HashMap<>();
+ // 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.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,
+ @Nullable byte[] httpBody,
+ @Nullable Map<String, String> requestProperties)
+ throws IOException {
+ HttpDataSource dataSource = dataSourceFactory.createDataSource();
+ if (requestProperties != null) {
+ for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) {
+ dataSource.setRequestProperty(requestProperty.getKey(), requestProperty.getValue());
+ }
+ }
+
+ 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<String, List<String>> headerFields = exception.headerFields;
+ if (headerFields != null) {
+ List<String> locationHeaders = headerFields.get("Location");
+ if (locationHeaders != null && !locationHeaders.isEmpty()) {
+ return locationHeaders.get(0);
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/KeysExpiredException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/KeysExpiredException.java
new file mode 100644
index 0000000000..79208489c4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/KeysExpiredException.java
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+/**
+ * Thrown when the drm keys loaded into an open session expire.
+ */
+public final class KeysExpiredException extends Exception {
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java
new file mode 100644
index 0000000000..23e1859ca8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/MediaDrmCallback.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/MediaDrmCallback.java
new file mode 100644
index 0000000000..2bc41f6bec
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/MediaDrmCallback.java
@@ -0,0 +1,46 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
+import java.util.UUID;
+
+/**
+ * Performs {@link ExoMediaDrm} key and provisioning requests.
+ */
+public interface MediaDrmCallback {
+
+ /**
+ * Executes a provisioning request.
+ *
+ * @param uuid The UUID of the content protection scheme.
+ * @param request The request.
+ * @return The response data.
+ * @throws Exception If an error occurred executing the request.
+ */
+ byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws Exception;
+
+ /**
+ * Executes a key request.
+ *
+ * @param uuid The UUID of the content protection scheme.
+ * @param request The request.
+ * @return The response data.
+ * @throws Exception If an error occurred executing the request.
+ */
+ byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception;
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java
new file mode 100644
index 0000000000..3ce3879a76
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java
@@ -0,0 +1,266 @@
+/*
+ * 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.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.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.util.Collections;
+import java.util.Map;
+import java.util.UUID;
+
+/** Helper class to download, renew and release offline licenses. */
+@TargetApi(18)
+@RequiresApi(18)
+public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
+
+ private static final DrmInitData DUMMY_DRM_INIT_DATA = new DrmInitData();
+
+ private final ConditionVariable conditionVariable;
+ private final DefaultDrmSessionManager<T> drmSessionManager;
+ private final HandlerThread handlerThread;
+
+ /**
+ * 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 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<FrameworkMediaCrypto> newWidevineInstance(
+ 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 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.
+ */
+ public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(
+ String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory)
+ throws UnsupportedDrmException {
+ 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<FrameworkMediaCrypto> newWidevineInstance(
+ String defaultLicenseUrl,
+ boolean forceDefaultLicenseUrl,
+ Factory httpDataSourceFactory,
+ @Nullable Map<String, String> 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 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}. May be null.
+ * @see DefaultDrmSessionManager.Builder
+ */
+ @SuppressWarnings("unchecked")
+ public OfflineLicenseHelper(
+ UUID uuid,
+ ExoMediaDrm.Provider<T> mediaDrmProvider,
+ MediaDrmCallback callback,
+ @Nullable Map<String, String> optionalKeyRequestParameters) {
+ handlerThread = new HandlerThread("OfflineLicenseHelper");
+ handlerThread.start();
+ conditionVariable = new ConditionVariable();
+ DefaultDrmSessionEventListener eventListener =
+ new DefaultDrmSessionEventListener() {
+ @Override
+ public void onDrmKeysLoaded() {
+ conditionVariable.open();
+ }
+
+ @Override
+ public void onDrmSessionManagerError(Exception e) {
+ conditionVariable.open();
+ }
+
+ @Override
+ public void onDrmKeysRestored() {
+ conditionVariable.open();
+ }
+
+ @Override
+ public void onDrmKeysRemoved() {
+ conditionVariable.open();
+ }
+ };
+ if (optionalKeyRequestParameters == null) {
+ optionalKeyRequestParameters = Collections.emptyMap();
+ }
+ drmSessionManager =
+ (DefaultDrmSessionManager<T>)
+ new DefaultDrmSessionManager.Builder()
+ .setUuidAndExoMediaDrmProvider(uuid, mediaDrmProvider)
+ .setKeyRequestParameters(optionalKeyRequestParameters)
+ .build(callback);
+ drmSessionManager.addListener(new Handler(handlerThread.getLooper()), eventListener);
+ }
+
+ /**
+ * Downloads an offline license.
+ *
+ * @param drmInitData The {@link DrmInitData} for the content whose license is to be downloaded.
+ * @return The key set id for the downloaded license.
+ * @throws DrmSessionException Thrown when a DRM session error occurs.
+ */
+ public synchronized byte[] downloadLicense(DrmInitData drmInitData) throws DrmSessionException {
+ Assertions.checkArgument(drmInitData != null);
+ return blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, null, drmInitData);
+ }
+
+ /**
+ * Renews an offline license.
+ *
+ * @param offlineLicenseKeySetId The key set id of the license to be renewed.
+ * @return The renewed offline license key set id.
+ * @throws DrmSessionException Thrown when a DRM session error occurs.
+ */
+ public synchronized byte[] renewLicense(byte[] offlineLicenseKeySetId)
+ throws DrmSessionException {
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ return blockingKeyRequest(
+ DefaultDrmSessionManager.MODE_DOWNLOAD, offlineLicenseKeySetId, DUMMY_DRM_INIT_DATA);
+ }
+
+ /**
+ * Releases an offline license.
+ *
+ * @param offlineLicenseKeySetId The key set id of the license to be released.
+ * @throws DrmSessionException Thrown when a DRM session error occurs.
+ */
+ public synchronized void releaseLicense(byte[] offlineLicenseKeySetId)
+ throws DrmSessionException {
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ blockingKeyRequest(
+ DefaultDrmSessionManager.MODE_RELEASE, offlineLicenseKeySetId, DUMMY_DRM_INIT_DATA);
+ }
+
+ /**
+ * Returns the remaining license and playback durations in seconds, for an offline license.
+ *
+ * @param offlineLicenseKeySetId The key set id of the license.
+ * @return The remaining license and playback durations, in seconds.
+ * @throws DrmSessionException Thrown when a DRM session error occurs.
+ */
+ public synchronized Pair<Long, Long> getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId)
+ throws DrmSessionException {
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ drmSessionManager.prepare();
+ DrmSession<T> drmSession =
+ openBlockingKeyRequest(
+ DefaultDrmSessionManager.MODE_QUERY, offlineLicenseKeySetId, DUMMY_DRM_INIT_DATA);
+ DrmSessionException error = drmSession.getError();
+ Pair<Long, Long> licenseDurationRemainingSec =
+ WidevineUtil.getLicenseDurationRemainingSec(drmSession);
+ drmSession.release();
+ drmSessionManager.release();
+ if (error != null) {
+ if (error.getCause() instanceof KeysExpiredException) {
+ return Pair.create(0L, 0L);
+ }
+ throw error;
+ }
+ return Assertions.checkNotNull(licenseDurationRemainingSec);
+ }
+
+ /**
+ * 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<T> drmSession = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId,
+ drmInitData);
+ DrmSessionException error = drmSession.getError();
+ byte[] keySetId = drmSession.getOfflineLicenseKeySetId();
+ drmSession.release();
+ drmSessionManager.release();
+ if (error != null) {
+ throw error;
+ }
+ return Assertions.checkNotNull(keySetId);
+ }
+
+ private DrmSession<T> openBlockingKeyRequest(
+ @Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, DrmInitData drmInitData) {
+ drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId);
+ conditionVariable.close();
+ DrmSession<T> drmSession = drmSessionManager.acquireSession(handlerThread.getLooper(),
+ drmInitData);
+ // Block current thread until key loading is finished
+ conditionVariable.block();
+ return drmSession;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/UnsupportedDrmException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/UnsupportedDrmException.java
new file mode 100644
index 0000000000..4dc9f2b0b2
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/UnsupportedDrmException.java
@@ -0,0 +1,67 @@
+/*
+ * 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 androidx.annotation.IntDef;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Thrown when the requested DRM scheme is not supported.
+ */
+public final class UnsupportedDrmException extends 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 {}
+ /**
+ * The requested DRM scheme is unsupported by the device.
+ */
+ public static final int REASON_UNSUPPORTED_SCHEME = 1;
+ /**
+ * There device advertises support for the requested DRM scheme, but there was an error
+ * instantiating it. The cause can be retrieved using {@link #getCause()}.
+ */
+ public static final int REASON_INSTANTIATION_ERROR = 2;
+
+ /**
+ * Either {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
+ */
+ @Reason public final int reason;
+
+ /**
+ * @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
+ */
+ public UnsupportedDrmException(@Reason int reason) {
+ this.reason = reason;
+ }
+
+ /**
+ * @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
+ * @param cause The cause of this exception.
+ */
+ public UnsupportedDrmException(@Reason int reason, Exception cause) {
+ super(cause);
+ this.reason = reason;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/WidevineUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/WidevineUtil.java
new file mode 100644
index 0000000000..67539bef39
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/WidevineUtil.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.drm;
+
+import android.util.Pair;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.util.Map;
+
+/**
+ * Utility methods for Widevine.
+ */
+public final class WidevineUtil {
+
+ /** Widevine specific key status field name for the remaining license duration, in seconds. */
+ public static final String PROPERTY_LICENSE_DURATION_REMAINING = "LicenseDurationRemaining";
+ /** Widevine specific key status field name for the remaining playback duration, in seconds. */
+ public static final String PROPERTY_PLAYBACK_DURATION_REMAINING = "PlaybackDurationRemaining";
+
+ private WidevineUtil() {}
+
+ /**
+ * Returns license and playback durations remaining in seconds.
+ *
+ * @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 @Nullable Pair<Long, Long> getLicenseDurationRemainingSec(
+ DrmSession<?> drmSession) {
+ Map<String, String> keyStatus = drmSession.queryKeyStatus();
+ if (keyStatus == null) {
+ return null;
+ }
+ return new Pair<>(getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING),
+ getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING));
+ }
+
+ private static long getDurationRemainingSec(Map<String, String> keyStatus, String property) {
+ if (keyStatus != null) {
+ try {
+ String value = keyStatus.get(property);
+ if (value != null) {
+ return Long.parseLong(value);
+ }
+ } catch (NumberFormatException e) {
+ // do nothing.
+ }
+ }
+ return C.TIME_UNSET;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/package-info.java
new file mode 100644
index 0000000000..ec885e2ad7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java
new file mode 100644
index 0000000000..b0b7c7da13
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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}.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.java
new file mode 100644
index 0000000000..4fdf9f3c55
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.java
@@ -0,0 +1,121 @@
+/*
+ * 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.util.Util;
+import java.util.Arrays;
+
+/**
+ * Defines chunks of samples within a media stream.
+ */
+public final class ChunkIndex implements SeekMap {
+
+ /**
+ * The number of chunks.
+ */
+ public final int length;
+
+ /**
+ * The chunk sizes, in bytes.
+ */
+ public final int[] sizes;
+
+ /**
+ * The chunk byte offsets.
+ */
+ public final long[] offsets;
+
+ /**
+ * The chunk durations, in microseconds.
+ */
+ public final long[] durationsUs;
+
+ /**
+ * The start time of each chunk, in microseconds.
+ */
+ public final long[] timesUs;
+
+ private final long durationUs;
+
+ /**
+ * @param sizes The chunk sizes, in bytes.
+ * @param offsets The chunk byte offsets.
+ * @param durationsUs The chunk durations, in microseconds.
+ * @param timesUs The start time of each chunk, in microseconds.
+ */
+ public ChunkIndex(int[] sizes, long[] offsets, long[] durationsUs, long[] timesUs) {
+ this.sizes = sizes;
+ this.offsets = offsets;
+ this.durationsUs = durationsUs;
+ this.timesUs = timesUs;
+ length = sizes.length;
+ if (length > 0) {
+ durationUs = durationsUs[length - 1] + timesUs[length - 1];
+ } else {
+ durationUs = 0;
+ }
+ }
+
+ /**
+ * Obtains the index of the chunk corresponding to a given time.
+ *
+ * @param timeUs The time, in microseconds.
+ * @return The index of the corresponding chunk.
+ */
+ public int getChunkIndex(long timeUs) {
+ return Util.binarySearchFloor(timesUs, timeUs, true, true);
+ }
+
+ // SeekMap implementation.
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMap.java
new file mode 100644
index 0000000000..215aac0e6d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java
new file mode 100644
index 0000000000..93009f2d5c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java
@@ -0,0 +1,308 @@
+/*
+ * 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.upstream.DataSource;
+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;
+import java.util.Arrays;
+
+/**
+ * An {@link ExtractorInput} that wraps a {@link DataSource}.
+ */
+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 int SCRATCH_SPACE_SIZE = 4096;
+
+ private final byte[] scratchSpace;
+ private final DataSource dataSource;
+ private final long streamLength;
+
+ private long position;
+ private byte[] peekBuffer;
+ private int peekBufferPosition;
+ private int peekBufferLength;
+
+ /**
+ * @param dataSource The wrapped {@link DataSource}.
+ * @param position The initial position in the stream.
+ * @param length The length of the stream, or {@link C#LENGTH_UNSET} if it is unknown.
+ */
+ public DefaultExtractorInput(DataSource dataSource, long position, long length) {
+ this.dataSource = dataSource;
+ 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, /* bytesAlreadyRead= */ 0, /* allowEndOfInput= */ true);
+ }
+ commitBytesRead(bytesRead);
+ return bytesRead;
+ }
+
+ @Override
+ public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ int bytesRead = readFromPeekBuffer(target, offset, length);
+ while (bytesRead < length && bytesRead != C.RESULT_END_OF_INPUT) {
+ bytesRead = readFromDataSource(target, offset, length, bytesRead, allowEndOfInput);
+ }
+ commitBytesRead(bytesRead);
+ return bytesRead != C.RESULT_END_OF_INPUT;
+ }
+
+ @Override
+ public void readFully(byte[] target, int offset, int length)
+ throws IOException, InterruptedException {
+ readFully(target, offset, length, false);
+ }
+
+ @Override
+ public int skip(int length) throws IOException, InterruptedException {
+ int bytesSkipped = skipFromPeekBuffer(length);
+ if (bytesSkipped == 0) {
+ bytesSkipped =
+ readFromDataSource(scratchSpace, 0, Math.min(length, scratchSpace.length), 0, true);
+ }
+ commitBytesRead(bytesSkipped);
+ return bytesSkipped;
+ }
+
+ @Override
+ public boolean skipFully(int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ int bytesSkipped = skipFromPeekBuffer(length);
+ while (bytesSkipped < length && bytesSkipped != C.RESULT_END_OF_INPUT) {
+ int minLength = Math.min(length, bytesSkipped + scratchSpace.length);
+ bytesSkipped =
+ readFromDataSource(scratchSpace, -bytesSkipped, minLength, bytesSkipped, allowEndOfInput);
+ }
+ commitBytesRead(bytesSkipped);
+ return bytesSkipped != C.RESULT_END_OF_INPUT;
+ }
+
+ @Override
+ public void skipFully(int length) throws IOException, InterruptedException {
+ 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 {
+ if (!advancePeekPosition(length, allowEndOfInput)) {
+ return false;
+ }
+ System.arraycopy(peekBuffer, peekBufferPosition - length, target, offset, length);
+ return true;
+ }
+
+ @Override
+ public void peekFully(byte[] target, int offset, int length)
+ throws IOException, InterruptedException {
+ peekFully(target, offset, length, false);
+ }
+
+ @Override
+ public boolean advancePeekPosition(int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ ensureSpaceForPeek(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;
+ return true;
+ }
+
+ @Override
+ public void advancePeekPosition(int length) throws IOException, InterruptedException {
+ advancePeekPosition(length, false);
+ }
+
+ @Override
+ public void resetPeekPosition() {
+ peekBufferPosition = 0;
+ }
+
+ @Override
+ public long getPeekPosition() {
+ return position + peekBufferPosition;
+ }
+
+ @Override
+ public long getPosition() {
+ return position;
+ }
+
+ @Override
+ public long getLength() {
+ return streamLength;
+ }
+
+ @Override
+ public <E extends Throwable> void setRetryPosition(long position, E e) throws E {
+ Assertions.checkArgument(position >= 0);
+ this.position = position;
+ throw e;
+ }
+
+ /**
+ * Ensures {@code peekBuffer} is large enough to store at least {@code length} bytes from the
+ * current peek position.
+ */
+ private void ensureSpaceForPeek(int length) {
+ int requiredLength = peekBufferPosition + length;
+ if (requiredLength > peekBuffer.length) {
+ int newPeekCapacity = Util.constrainValue(peekBuffer.length * 2,
+ requiredLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE, requiredLength + PEEK_MAX_FREE_SPACE);
+ peekBuffer = Arrays.copyOf(peekBuffer, newPeekCapacity);
+ }
+ }
+
+ /**
+ * Skips from the peek buffer.
+ *
+ * @param length The maximum number of bytes to skip from the peek buffer.
+ * @return The number of bytes skipped.
+ */
+ private int skipFromPeekBuffer(int length) {
+ int bytesSkipped = Math.min(peekBufferLength, length);
+ updatePeekBuffer(bytesSkipped);
+ return bytesSkipped;
+ }
+
+ /**
+ * 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.
+ * @param length The maximum number of bytes to read from the peek buffer.
+ * @return The number of bytes read.
+ */
+ private int readFromPeekBuffer(byte[] target, int offset, int length) {
+ if (peekBufferLength == 0) {
+ return 0;
+ }
+ int peekBytes = Math.min(peekBufferLength, length);
+ System.arraycopy(peekBuffer, 0, target, offset, peekBytes);
+ updatePeekBuffer(peekBytes);
+ return peekBytes;
+ }
+
+ /**
+ * Updates the peek buffer's length, position and contents after consuming data.
+ *
+ * @param bytesConsumed The number of bytes consumed from the peek buffer.
+ */
+ private void updatePeekBuffer(int bytesConsumed) {
+ peekBufferLength -= bytesConsumed;
+ peekBufferPosition = 0;
+ byte[] newPeekBuffer = peekBuffer;
+ if (peekBufferLength < peekBuffer.length - PEEK_MAX_FREE_SPACE) {
+ newPeekBuffer = new byte[peekBufferLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE];
+ }
+ System.arraycopy(peekBuffer, bytesConsumed, newPeekBuffer, 0, peekBufferLength);
+ peekBuffer = newPeekBuffer;
+ }
+
+ /**
+ * Starts or continues a read from the data source.
+ *
+ * @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 read from the input.
+ * @param bytesAlreadyRead The number of bytes already read from the input.
+ * @param allowEndOfInput True if encountering the end of the input having read no data is
+ * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it
+ * should be considered an error, causing an {@link EOFException} to be thrown.
+ * @return The total number of bytes read so far, or {@link C#RESULT_END_OF_INPUT} if
+ * {@code allowEndOfInput} is true and the input has ended having read no bytes.
+ * @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.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private int readFromDataSource(byte[] target, int offset, int length, int bytesAlreadyRead,
+ boolean allowEndOfInput) throws InterruptedException, IOException {
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+ int bytesRead = dataSource.read(target, offset + bytesAlreadyRead, length - bytesAlreadyRead);
+ if (bytesRead == C.RESULT_END_OF_INPUT) {
+ if (bytesAlreadyRead == 0 && allowEndOfInput) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ throw new EOFException();
+ }
+ return bytesAlreadyRead + bytesRead;
+ }
+
+ /**
+ * Advances the position by the specified number of bytes read.
+ *
+ * @param bytesRead The number of bytes read.
+ */
+ private void commitBytesRead(int bytesRead) {
+ if (bytesRead != C.RESULT_END_OF_INPUT) {
+ position += bytesRead;
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
new file mode 100644
index 0000000000..8425f89860
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
@@ -0,0 +1,269 @@
+/*
+ * 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.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;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
+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:
+ *
+ * <ul>
+ * <li>MP4, including M4A ({@link Mp4Extractor})
+ * <li>fMP4 ({@link FragmentedMp4Extractor})
+ * <li>Matroska and WebM ({@link MatroskaExtractor})
+ * <li>Ogg Vorbis/FLAC ({@link OggExtractor}
+ * <li>MP3 ({@link Mp3Extractor})
+ * <li>AAC ({@link AdtsExtractor})
+ * <li>MPEG TS ({@link TsExtractor})
+ * <li>MPEG PS ({@link PsExtractor})
+ * <li>FLV ({@link FlvExtractor})
+ * <li>WAV ({@link WavExtractor})
+ * <li>AC3 ({@link Ac3Extractor})
+ * <li>AC4 ({@link Ac4Extractor})
+ * <li>AMR ({@link AmrExtractor})
+ * <li>FLAC
+ * <ul>
+ * <li>If available, the FLAC extension extractor is used.
+ * <li>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.
+ * </ul>
+ * </ul>
+ */
+public final class DefaultExtractorsFactory implements ExtractorsFactory {
+
+ private static final Constructor<? extends Extractor> FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR;
+
+ static {
+ Constructor<? extends Extractor> flacExtensionExtractorConstructor = null;
+ try {
+ // 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) {
+ // 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_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.
+ *
+ * @see MatroskaExtractor#MatroskaExtractor(int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setMatroskaExtractorFlags(
+ @MatroskaExtractor.Flags int flags) {
+ this.matroskaFlags = flags;
+ 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.
+ *
+ * @see FragmentedMp4Extractor#FragmentedMp4Extractor(int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setFragmentedMp4ExtractorFlags(
+ @FragmentedMp4Extractor.Flags int flags) {
+ this.fragmentedMp4Flags = flags;
+ return this;
+ }
+
+ /**
+ * Sets flags for {@link Mp3Extractor} instances created by the factory.
+ *
+ * @see Mp3Extractor#Mp3Extractor(int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setMp3ExtractorFlags(@Mp3Extractor.Flags int flags) {
+ mp3Flags = flags;
+ 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.
+ *
+ * @see TsExtractor#TsExtractor(int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setTsExtractorFlags(
+ @DefaultTsPayloadReaderFactory.Flags int flags) {
+ tsFlags = flags;
+ return this;
+ }
+
+ @Override
+ public synchronized Extractor[] createExtractors() {
+ Extractor[] extractors = new Extractor[14];
+ extractors[0] = new MatroskaExtractor(matroskaFlags);
+ extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags);
+ 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(tsMode, tsFlags);
+ extractors[7] = new FlvExtractor();
+ extractors[8] = new OggExtractor();
+ extractors[9] = new PsExtractor();
+ extractors[10] = new WavExtractor();
+ 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[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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyExtractorOutput.java
new file mode 100644
index 0000000000..06c90ae874
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java
new file mode 100644
index 0000000000..6df947731d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java
@@ -0,0 +1,62 @@
+/*
+ * 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 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;
+
+/**
+ * A dummy {@link TrackOutput} implementation.
+ */
+public final class DummyTrackOutput implements TrackOutput {
+
+ @Override
+ public void format(Format format) {
+ // Do nothing.
+ }
+
+ @Override
+ public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ 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;
+ }
+
+ @Override
+ public void sampleData(ParsableByteArray data, int length) {
+ data.skipBytes(length);
+ }
+
+ @Override
+ public void sampleMetadata(
+ long timeUs,
+ @C.BufferFlags int flags,
+ int size,
+ int offset,
+ @Nullable CryptoData cryptoData) {
+ // Do nothing.
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Extractor.java
new file mode 100644
index 0000000000..aeb7028c3f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Extractor.java
@@ -0,0 +1,125 @@
+/*
+ * 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 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.
+ */
+public interface Extractor {
+
+ /**
+ * Returned by {@link #read(ExtractorInput, PositionHolder)} if the {@link ExtractorInput} passed
+ * to the next {@link #read(ExtractorInput, PositionHolder)} is required to provide data
+ * continuing from the position in the stream reached by the returning call.
+ */
+ int RESULT_CONTINUE = 0;
+ /**
+ * Returned by {@link #read(ExtractorInput, PositionHolder)} if the {@link ExtractorInput} passed
+ * to the next {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting
+ * from a specified position in the stream.
+ */
+ int RESULT_SEEK = 1;
+ /**
+ * Returned by {@link #read(ExtractorInput, PositionHolder)} if the end of the
+ * {@link ExtractorInput} was reached. Equal to {@link C#RESULT_END_OF_INPUT}.
+ */
+ 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.
+ * <p>
+ * If {@code true} is returned, the {@code input}'s reading position may have been modified.
+ * Otherwise, only its peek position may have been modified.
+ *
+ * @param input The {@link ExtractorInput} from which data should be peeked/read.
+ * @return Whether this extractor can read the provided input.
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ boolean sniff(ExtractorInput input) throws IOException, InterruptedException;
+
+ /**
+ * Initializes the extractor with an {@link ExtractorOutput}. Called at most once.
+ *
+ * @param output An {@link ExtractorOutput} to receive extracted data.
+ */
+ void init(ExtractorOutput output);
+
+ /**
+ * Extracts data read from a provided {@link ExtractorInput}. Must not be called before {@link
+ * #init(ExtractorOutput)}.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ * @return One of the {@code RESULT_} values defined in this interface.
+ * @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;
+
+ /**
+ * Notifies the extractor that a seek has occurred.
+ * <p>
+ * Following a call to this method, the {@link ExtractorInput} passed to the next invocation of
+ * {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting from {@code
+ * position} in the stream. Valid random access positions are the start of the stream and
+ * positions that can be obtained from any {@link SeekMap} passed to the {@link ExtractorOutput}.
+ *
+ * @param position The byte offset in the stream from which data will be provided.
+ * @param timeUs The seek time in microseconds.
+ */
+ void seek(long position, long timeUs);
+
+ /**
+ * Releases all kept resources.
+ */
+ void release();
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.java
new file mode 100644
index 0000000000..351df1e79e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.java
@@ -0,0 +1,280 @@
+/*
+ * 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 java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Provides data to be consumed by an {@link Extractor}.
+ *
+ * <p>This interface provides two modes of accessing the underlying input. See the subheadings below
+ * for more info about each mode.
+ *
+ * <ul>
+ * <li>The {@code read()/peek()} and {@code skip()} methods provide {@link InputStream}-like
+ * byte-level access operations.
+ * <li>The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user
+ * wants to read an entire block/frame/header of known length.
+ * </ul>
+ *
+ * <h3>{@link InputStream}-like methods</h3>
+ *
+ * <p>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.
+ *
+ * <h3>Block-based methods</h3>
+ *
+ * <p>The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user
+ * wants to read an entire block/frame/header of known length.
+ *
+ * <p>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 <b>not</b> 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).
+ *
+ * <p>The expected behaviour of the block-based methods is therefore:
+ *
+ * <ul>
+ * <li>Already at end-of-input and {@code allowEndOfInput=false}: Throw {@link EOFException}.
+ * <li>Already at end-of-input and {@code allowEndOfInput=true}: Return {@code false}.
+ * <li>Encounter end-of-input during read/skip/peek/advance: Throw {@link EOFException}
+ * (regardless of {@code allowEndOfInput}).
+ * </ul>
+ */
+public interface ExtractorInput {
+
+ /**
+ * Reads up to {@code length} bytes from the input and resets the peek position.
+ * <p>
+ * This method blocks until at least one byte of data can be read, the end of the input is
+ * detected, or an exception is 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 maximum number of bytes to read from the input.
+ * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the input has ended.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ int read(byte[] target, int offset, int length) throws IOException, InterruptedException;
+
+ /**
+ * Like {@link #read(byte[], int, int)}, but reads 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 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. 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.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException;
+
+ /**
+ * 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.
+ * @param length The number of bytes to read from the input.
+ * @throws EOFException If the end of input was encountered.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ void readFully(byte[] target, int offset, int length) throws IOException, InterruptedException;
+
+ /**
+ * Like {@link #read(byte[], int, int)}, except the data is skipped instead of read.
+ *
+ * @param length The maximum number of bytes to skip from the input.
+ * @return The number of bytes skipped, or {@link C#RESULT_END_OF_INPUT} if the input has ended.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ int skip(int length) throws IOException, InterruptedException;
+
+ /**
+ * Like {@link #readFully(byte[], int, int, boolean)}, except the data is skipped instead of read.
+ *
+ * @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. 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.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ boolean skipFully(int length, boolean allowEndOfInput) throws IOException, InterruptedException;
+
+ /**
+ * Like {@link #readFully(byte[], int, int)}, except the data is skipped instead of read.
+ * <p>
+ * Encountering the end of input is always considered an error, and will result in an
+ * {@link EOFException} being thrown.
+ *
+ * @param length The number of bytes to skip from the input.
+ * @throws EOFException If the end of input was encountered.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ void skipFully(int length) throws IOException, InterruptedException;
+
+ /**
+ * Peeks up to {@code length} bytes from the peek position. The current read position is left
+ * unchanged.
+ *
+ * <p>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.
+ *
+ * <p>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. 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.
+ * @throws IOException If an error occurs peeking from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException;
+
+ /**
+ * 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.
+ * @param length The number of bytes to peek from the input.
+ * @throws EOFException If the end of input was encountered.
+ * @throws IOException If an error occurs peeking from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ void peekFully(byte[] target, int offset, int length) throws IOException, InterruptedException;
+
+ /**
+ * 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. 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.
+ * @throws IOException If an error occurs advancing the peek position.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ boolean advancePeekPosition(int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException;
+
+ /**
+ * 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.
+ * @throws IOException If an error occurs peeking from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ void advancePeekPosition(int length) throws IOException, InterruptedException;
+
+ /**
+ * Resets the peek position to equal the current read position.
+ */
+ void resetPeekPosition();
+
+ /**
+ * Returns the current peek position (byte offset) in the stream.
+ *
+ * @return The peek position (byte offset) in the stream.
+ */
+ long getPeekPosition();
+
+ /**
+ * Returns the current read position (byte offset) in the stream.
+ *
+ * @return The read position (byte offset) in the stream.
+ */
+ long getPosition();
+
+ /**
+ * Returns the length of the source stream, or {@link C#LENGTH_UNSET} if it is unknown.
+ *
+ * @return The length of the source stream, or {@link C#LENGTH_UNSET}.
+ */
+ long getLength();
+
+ /**
+ * Called when reading fails and the required retry position is different from the last position.
+ * After setting the retry position it throws the given {@link Throwable}.
+ *
+ * @param <E> Type of {@link Throwable} to be thrown.
+ * @param position The required retry position.
+ * @param e {@link Throwable} to be thrown.
+ * @throws E The given {@link Throwable} object.
+ */
+ <E extends Throwable> void setRetryPosition(long position, E e) throws E;
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.java
new file mode 100644
index 0000000000..8708758265
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/**
+ * Receives stream level data extracted by an {@link Extractor}.
+ */
+public interface ExtractorOutput {
+
+ /**
+ * Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track.
+ * <p>
+ * 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.exoplayer2C}
+ * {@code TRACK_TYPE_*} constants.
+ * @return The {@link TrackOutput} for the given track identifier.
+ */
+ TrackOutput track(int id, int type);
+
+ /**
+ * Called when all tracks have been identified, meaning no new {@code trackId} values will be
+ * passed to {@link #track(int, int)}.
+ */
+ void endTracks();
+
+ /**
+ * Called when a {@link SeekMap} has been extracted from the stream.
+ *
+ * @param seekMap The extracted {@link SeekMap}.
+ */
+ void seekMap(SeekMap seekMap);
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorUtil.java
new file mode 100644
index 0000000000..6951f7e311
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.java
new file mode 100644
index 0000000000..64b803f65e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+/** Factory for arrays of {@link Extractor} instances. */
+public interface ExtractorsFactory {
+
+ /** Returns an array of new {@link Extractor} instances. */
+ Extractor[] createExtractors();
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacFrameReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacFrameReader.java
new file mode 100644
index 0000000000..e8d2b4928b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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 <a
+ * href="https://xiph.org/flac/format.html">FLAC format specification</a>.
+ */
+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}.
+ *
+ * <p>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}.
+ *
+ * <p>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.
+ *
+ * <p>The read position of {@code input} is left unchanged.
+ *
+ * <p>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}.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java
new file mode 100644
index 0000000000..c5413cf459
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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 <a
+ * href="https://xiph.org/flac/format.html">FLAC format specification</a>.
+ */
+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.
+ *
+ * <p>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.
+ *
+ * <p>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<String> 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.
+ *
+ * <p>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.
+ *
+ * <p>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<String> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java
new file mode 100644
index 0000000000..56d54596ac
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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 <a
+ * href="https://xiph.org/flac/format.html#metadata_block_seektable">seek table</a>.
+ */
+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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java
new file mode 100644
index 0000000000..11893d6136
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java
@@ -0,0 +1,131 @@
+/*
+ * 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.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.InternalFrame;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Holder for gapless playback information.
+ */
+public final class GaplessInfoHolder {
+
+ 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})");
+
+ /**
+ * The number of samples to trim from the start of the decoded audio stream, or
+ * {@link Format#NO_VALUE} if not set.
+ */
+ public int encoderDelay;
+
+ /**
+ * The number of samples to trim from the end of the decoded audio stream, or
+ * {@link Format#NO_VALUE} if not set.
+ */
+ public int encoderPadding;
+
+ /**
+ * Creates a new holder for gapless playback information.
+ */
+ public GaplessInfoHolder() {
+ encoderDelay = Format.NO_VALUE;
+ encoderPadding = Format.NO_VALUE;
+ }
+
+ /**
+ * Populates the holder with data from an MP3 Xing header, if valid and non-zero.
+ *
+ * @param value The 24-bit value to decode.
+ * @return Whether the holder was populated.
+ */
+ public boolean setFromXingHeaderValue(int value) {
+ int encoderDelay = value >> 12;
+ int encoderPadding = value & 0x0FFF;
+ if (encoderDelay > 0 || encoderPadding > 0) {
+ this.encoderDelay = encoderDelay;
+ this.encoderPadding = encoderPadding;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Populates the holder with data parsed from ID3 {@link Metadata}.
+ *
+ * @param metadata The metadata from which to parse the gapless information.
+ * @return Whether the holder was populated.
+ */
+ public boolean setFromMetadata(Metadata metadata) {
+ for (int i = 0; i < metadata.length(); i++) {
+ Metadata.Entry entry = metadata.get(i);
+ if (entry instanceof CommentFrame) {
+ CommentFrame commentFrame = (CommentFrame) entry;
+ 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;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 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 data The comment's payload data.
+ * @return Whether the holder was populated.
+ */
+ private boolean setFromComment(String data) {
+ Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data);
+ if (matcher.find()) {
+ try {
+ int encoderDelay = Integer.parseInt(matcher.group(1), 16);
+ int encoderPadding = Integer.parseInt(matcher.group(2), 16);
+ if (encoderDelay > 0 || encoderPadding > 0) {
+ this.encoderDelay = encoderDelay;
+ this.encoderPadding = encoderPadding;
+ return true;
+ }
+ } catch (NumberFormatException e) {
+ // Ignore incorrectly formatted comments.
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether {@link #encoderDelay} and {@link #encoderPadding} have been set.
+ */
+ public boolean hasGaplessInfo() {
+ return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Id3Peeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/Id3Peeker.java
new file mode 100644
index 0000000000..a0a26c76d8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.java
new file mode 100644
index 0000000000..66c3411094
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.java
@@ -0,0 +1,275 @@
+/*
+ * 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 androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+
+/**
+ * An MPEG audio frame header.
+ */
+public final class MpegAudioHeader {
+
+ /**
+ * Theoretical maximum frame size for an MPEG audio stream, which occurs when playing a Layer 2
+ * MPEG 2.5 audio stream at 16 kb/s (with padding). The size is 1152 sample/frame *
+ * 160000 bit/s / (8000 sample/s * 8 bit/byte) + 1 padding byte/frame = 2881 byte/frame.
+ * The next power of two size is 4 KiB.
+ */
+ public static final int MAX_FRAME_SIZE_BYTES = 4096;
+
+ 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 = {
+ 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 (!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;
+ }
+
+ int bitrateIndex = (header >>> 12) & 15;
+ if (bitrateIndex == 0 || bitrateIndex == 0xF) {
+ // Disallow "free" bitrate.
+ return C.LENGTH_UNSET;
+ }
+
+ int samplingRateIndex = (header >>> 10) & 3;
+ if (samplingRateIndex == 3) {
+ return C.LENGTH_UNSET;
+ }
+
+ int samplingRate = SAMPLING_RATE_V1[samplingRateIndex];
+ if (version == 2) {
+ // Version 2
+ samplingRate /= 2;
+ } else if (version == 0) {
+ // Version 2.5
+ samplingRate /= 4;
+ }
+
+ int bitrate;
+ int padding = (header >>> 9) & 1;
+ if (layer == 3) {
+ // Layer I (layer == 3)
+ bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
+ return (12 * bitrate / samplingRate + padding) * 4;
+ } else {
+ // Layer II (layer == 2) or III (layer == 1)
+ if (version == 3) {
+ bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];
+ } else {
+ // Version 2 or 2.5.
+ bitrate = BITRATE_V2[bitrateIndex - 1];
+ }
+ }
+
+ if (version == 3) {
+ // Version 1
+ return 144 * bitrate / samplingRate + padding;
+ } else {
+ // Version 2 or 2.5
+ 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.
+ *
+ * @param headerData Header data to parse.
+ * @param header Header to populate with data from {@code headerData}.
+ * @return True if the header was populated. False otherwise, indicating that {@code headerData}
+ * is not a valid MPEG audio header.
+ */
+ public static boolean populateHeader(int headerData, MpegAudioHeader header) {
+ if (!isMagicPresent(headerData)) {
+ return false;
+ }
+
+ int version = (headerData >>> 19) & 3;
+ if (version == 1) {
+ return false;
+ }
+
+ int layer = (headerData >>> 17) & 3;
+ if (layer == 0) {
+ return false;
+ }
+
+ int bitrateIndex = (headerData >>> 12) & 15;
+ if (bitrateIndex == 0 || bitrateIndex == 0xF) {
+ // Disallow "free" bitrate.
+ return false;
+ }
+
+ int samplingRateIndex = (headerData >>> 10) & 3;
+ if (samplingRateIndex == 3) {
+ return false;
+ }
+
+ int sampleRate = SAMPLING_RATE_V1[samplingRateIndex];
+ if (version == 2) {
+ // Version 2
+ sampleRate /= 2;
+ } else if (version == 0) {
+ // Version 2.5
+ sampleRate /= 4;
+ }
+
+ int padding = (headerData >>> 9) & 1;
+ 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 = (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];
+ frameSize = 144 * bitrate / sampleRate + padding;
+ } else {
+ // Version 2 or 2.5.
+ bitrate = BITRATE_V2[bitrateIndex - 1];
+ 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, 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. */
+ @Nullable public String mimeType;
+ /** Size of the frame associated with this header, in bytes. */
+ public int frameSize;
+ /** Sample rate in samples per second. */
+ public int sampleRate;
+ /** Number of audio channels in the frame. */
+ public int channels;
+ /** Bitrate of the frame in bit/s. */
+ public int bitrate;
+ /** 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) {
+ this.version = version;
+ this.mimeType = mimeType;
+ this.frameSize = frameSize;
+ this.sampleRate = sampleRate;
+ this.channels = channels;
+ this.bitrate = bitrate;
+ this.samplesPerFrame = samplesPerFrame;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/PositionHolder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/PositionHolder.java
new file mode 100644
index 0000000000..feae7f0bc7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/PositionHolder.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+/**
+ * Holds a position in the stream.
+ */
+public final class PositionHolder {
+
+ /**
+ * The held position.
+ */
+ public long position;
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.java
new file mode 100644
index 0000000000..b3ccad214d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.java
@@ -0,0 +1,141 @@
+/*
+ * 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 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. */
+ 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.
+ */
+ 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
+ public boolean isSeekable() {
+ return false;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ 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.
+ *
+ * @return Whether seeking is supported.
+ */
+ boolean isSeekable();
+
+ /**
+ * 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.
+ */
+ long getDurationUs();
+
+ /**
+ * Obtains seek points for the specified seek time in microseconds. The returned {@link
+ * SeekPoints} will contain one or two distinct seek points.
+ *
+ * <p>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.
+ */
+ SeekPoints getSeekPoints(long timeUs);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekPoint.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/SeekPoint.java
new file mode 100644
index 0000000000..1c4db35203
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java
new file mode 100644
index 0000000000..fd33bd6027
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java
@@ -0,0 +1,147 @@
+/*
+ * 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 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.
+ *
+ * @param format The extracted {@link Format}.
+ */
+ void format(Format format);
+
+ /**
+ * Called to write sample data to the output.
+ *
+ * @param input An {@link ExtractorInput} from which to read the sample data.
+ * @param length The maximum length 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 {@link C#RESULT_END_OF_INPUT} being returned. False if it
+ * should be considered an error, causing an {@link EOFException} to be thrown.
+ * @return The number of bytes appended.
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException;
+
+ /**
+ * 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, starting from {@code data.getPosition()}.
+ */
+ void sampleData(ParsableByteArray data, int length);
+
+ /**
+ * Called when metadata associated with a sample has been extracted from the stream.
+ *
+ * <p>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 encryptionData The encryption data required to decrypt the sample. May be null.
+ */
+ void sampleMetadata(
+ long timeUs,
+ @C.BufferFlags int flags,
+ int size,
+ int offset,
+ @Nullable CryptoData encryptionData);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisBitArray.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisBitArray.java
new file mode 100644
index 0000000000..4ea27c0149
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisBitArray.java
@@ -0,0 +1,129 @@
+/*
+ * 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.util.Assertions;
+
+/**
+ * Wraps a byte array, providing methods that allow it to be read as a Vorbis bitstream.
+ *
+ * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-360002">Vorbis bitpacking
+ * specification</a>
+ */
+public final class VorbisBitArray {
+
+ private final byte[] data;
+ private final int byteLimit;
+
+ private int byteOffset;
+ private int bitOffset;
+
+ /**
+ * Creates a new instance that wraps an existing array.
+ *
+ * @param data the array to wrap.
+ */
+ public VorbisBitArray(byte[] data) {
+ this.data = data;
+ byteLimit = data.length;
+ }
+
+ /**
+ * Resets the reading position to zero.
+ */
+ public void reset() {
+ byteOffset = 0;
+ bitOffset = 0;
+ }
+
+ /**
+ * Reads a single bit.
+ *
+ * @return {@code true} if the bit is set, {@code false} otherwise.
+ */
+ public boolean readBit() {
+ boolean returnValue = (((data[byteOffset] & 0xFF) >> bitOffset) & 0x01) == 1;
+ skipBits(1);
+ return returnValue;
+ }
+
+ /**
+ * Reads up to 32 bits.
+ *
+ * @param numBits The number of bits to read.
+ * @return An integer whose bottom {@code numBits} bits hold the read data.
+ */
+ public int readBits(int numBits) {
+ 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;
+ }
+ returnValue &= 0xFFFFFFFF >>> (32 - numBits);
+ skipBits(numBits);
+ return returnValue;
+ }
+
+ /**
+ * Skips {@code numberOfBits} bits.
+ *
+ * @param numBits The number of bits to skip.
+ */
+ public void skipBits(int numBits) {
+ int numBytes = numBits / 8;
+ byteOffset += numBytes;
+ bitOffset += numBits - (numBytes * 8);
+ if (bitOffset > 7) {
+ byteOffset++;
+ bitOffset -= 8;
+ }
+ assertValidOffset();
+ }
+
+ /**
+ * Returns the reading position in bits.
+ */
+ public int getPosition() {
+ return byteOffset * 8 + bitOffset;
+ }
+
+ /**
+ * Sets the reading position in bits.
+ *
+ * @param position The new reading position in bits.
+ */
+ public void setPosition(int position) {
+ byteOffset = position / 8;
+ bitOffset = position - (byteOffset * 8);
+ assertValidOffset();
+ }
+
+ /**
+ * Returns the number of remaining bits.
+ */
+ public int bitsLeft() {
+ return (byteLimit - byteOffset) * 8 - bitOffset;
+ }
+
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisUtil.java
new file mode 100644
index 0000000000..bdd3e13b99
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/VorbisUtil.java
@@ -0,0 +1,522 @@
+/*
+ * 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.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. */
+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";
+
+ /**
+ * Returns ilog(x), which is the index of the highest set bit in {@code x}.
+ *
+ * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-1190009.2.1">
+ * Vorbis spec</a>
+ * @param x the value of which the ilog should be calculated.
+ * @return ilog(x)
+ */
+ public static int iLog(int x) {
+ int val = 0;
+ while (x > 0) {
+ val++;
+ x >>>= 1;
+ }
+ return val;
+ }
+
+ /**
+ * Reads a Vorbis identification header from {@code headerData}.
+ *
+ * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2">Vorbis
+ * spec/Identification header</a>
+ * @param headerData a {@link ParsableByteArray} wrapping the header data.
+ * @return a {@link VorbisUtil.VorbisIdHeader} with meta data.
+ * @throws ParserException thrown if invalid capture pattern is detected.
+ */
+ public static VorbisIdHeader readVorbisIdentificationHeader(ParsableByteArray headerData)
+ throws ParserException {
+
+ verifyVorbisHeaderCapturePattern(0x01, headerData, false);
+
+ long version = headerData.readLittleEndianUnsignedInt();
+ int channels = headerData.readUnsignedByte();
+ long sampleRate = headerData.readLittleEndianUnsignedInt();
+ int bitrateMax = headerData.readLittleEndianInt();
+ int bitrateNominal = headerData.readLittleEndianInt();
+ int bitrateMin = headerData.readLittleEndianInt();
+
+ int blockSize = headerData.readUnsignedByte();
+ int blockSize0 = (int) Math.pow(2, blockSize & 0x0F);
+ 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
+ byte[] data = Arrays.copyOf(headerData.data, headerData.limit());
+
+ return new VorbisIdHeader(version, channels, sampleRate, bitrateMax, bitrateNominal, bitrateMin,
+ blockSize0, blockSize1, framingFlag, data);
+ }
+
+ /**
+ * Reads a Vorbis comment header.
+ *
+ * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3">Vorbis
+ * spec/Comment header</a>
+ * @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);
+ }
+
+ /**
+ * Reads a Vorbis comment header.
+ *
+ * <p>The data provided may not contain the Vorbis metadata common header and the framing bit.
+ *
+ * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3">Vorbis
+ * spec/Comment header</a>
+ * @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();
+ length += 4;
+ String vendor = headerData.readString(len);
+ length += vendor.length();
+
+ long commentListLen = headerData.readLittleEndianUnsignedInt();
+ String[] comments = new String[(int) commentListLen];
+ length += 4;
+ for (int i = 0; i < commentListLen; i++) {
+ len = (int) headerData.readLittleEndianUnsignedInt();
+ length += 4;
+ comments[i] = headerData.readString(len);
+ length += comments[i].length();
+ }
+ if (hasFramingBit && (headerData.readUnsignedByte() & 0x01) == 0) {
+ throw new ParserException("framing bit expected to be set");
+ }
+ length += 1;
+ return new CommentHeader(vendor, comments, length);
+ }
+
+ /**
+ * 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.
+ * @param quiet if {@code true} no exceptions are thrown. Instead {@code false} is returned.
+ * @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 {
+ if (header.bytesLeft() < 7) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("too short header: " + header.bytesLeft());
+ }
+ }
+
+ if (header.readUnsignedByte() != headerType) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("expected header type " + Integer.toHexString(headerType));
+ }
+ }
+
+ if (!(header.readUnsignedByte() == 'v'
+ && header.readUnsignedByte() == 'o'
+ && header.readUnsignedByte() == 'r'
+ && header.readUnsignedByte() == 'b'
+ && header.readUnsignedByte() == 'i'
+ && header.readUnsignedByte() == 's')) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("expected characters 'vorbis'");
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 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 <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-650004.2.4">Vorbis
+ * spec/Setup header</a>
+ * @param headerData a {@link ParsableByteArray} containing setup header data.
+ * @param channels the number of channels.
+ * @return an array of {@link Mode}s.
+ * @throws ParserException thrown if bit stream is invalid.
+ */
+ public static Mode[] readVorbisModes(ParsableByteArray headerData, int channels)
+ throws ParserException {
+
+ verifyVorbisHeaderCapturePattern(0x05, headerData, false);
+
+ int numberOfBooks = headerData.readUnsignedByte() + 1;
+
+ VorbisBitArray bitArray = new VorbisBitArray(headerData.data);
+ bitArray.skipBits(headerData.getPosition() * 8);
+
+ for (int i = 0; i < numberOfBooks; i++) {
+ readBook(bitArray);
+ }
+
+ int timeCount = bitArray.readBits(6) + 1;
+ for (int i = 0; i < timeCount; i++) {
+ if (bitArray.readBits(16) != 0x00) {
+ throw new ParserException("placeholder of time domain transforms not zeroed out");
+ }
+ }
+ readFloors(bitArray);
+ readResidues(bitArray);
+ readMappings(channels, bitArray);
+
+ Mode[] modes = readModes(bitArray);
+ if (!bitArray.readBit()) {
+ throw new ParserException("framing bit after modes not set as expected");
+ }
+ return modes;
+ }
+
+ private static Mode[] readModes(VorbisBitArray bitArray) {
+ int modeCount = bitArray.readBits(6) + 1;
+ Mode[] modes = new Mode[modeCount];
+ for (int i = 0; i < modeCount; i++) {
+ boolean blockFlag = bitArray.readBit();
+ int windowType = bitArray.readBits(16);
+ int transformType = bitArray.readBits(16);
+ int mapping = bitArray.readBits(8);
+ modes[i] = new Mode(blockFlag, windowType, transformType, mapping);
+ }
+ return modes;
+ }
+
+ private static void readMappings(int channels, VorbisBitArray bitArray)
+ throws ParserException {
+ int mappingsCount = bitArray.readBits(6) + 1;
+ for (int i = 0; i < mappingsCount; i++) {
+ int mappingType = bitArray.readBits(16);
+ 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
+ }
+ }
+ }
+
+ private static void readResidues(VorbisBitArray bitArray) throws ParserException {
+ int residueCount = bitArray.readBits(6) + 1;
+ for (int i = 0; i < residueCount; i++) {
+ int residueType = bitArray.readBits(16);
+ if (residueType > 2) {
+ throw new ParserException("residueType greater than 2 is not decodable");
+ } else {
+ bitArray.skipBits(24); // begin
+ bitArray.skipBits(24); // end
+ bitArray.skipBits(24); // partitionSize (add one)
+ int classifications = bitArray.readBits(6) + 1;
+ bitArray.skipBits(8); // classbook
+ int[] cascade = new int[classifications];
+ for (int j = 0; j < classifications; j++) {
+ int highBits = 0;
+ int lowBits = bitArray.readBits(3);
+ if (bitArray.readBit()) {
+ highBits = bitArray.readBits(5);
+ }
+ cascade[j] = highBits * 8 + lowBits;
+ }
+ for (int j = 0; j < classifications; j++) {
+ for (int k = 0; k < 8; k++) {
+ if ((cascade[j] & (0x01 << k)) != 0) {
+ bitArray.skipBits(8); // discard
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static void readFloors(VorbisBitArray bitArray) throws ParserException {
+ int floorCount = bitArray.readBits(6) + 1;
+ for (int i = 0; i < floorCount; i++) {
+ int floorType = bitArray.readBits(16);
+ switch (floorType) {
+ case 0:
+ bitArray.skipBits(8); //order
+ bitArray.skipBits(16); // rate
+ bitArray.skipBits(16); // barkMapSize
+ bitArray.skipBits(6); // amplitudeBits
+ bitArray.skipBits(8); // amplitudeOffset
+ int floorNumberOfBooks = bitArray.readBits(4) + 1;
+ for (int j = 0; j < floorNumberOfBooks; j++) {
+ bitArray.skipBits(8);
+ }
+ break;
+ case 1:
+ int partitions = bitArray.readBits(5);
+ int maximumClass = -1;
+ int[] partitionClassList = new int[partitions];
+ for (int j = 0; j < partitions; j++) {
+ partitionClassList[j] = bitArray.readBits(4);
+ if (partitionClassList[j] > maximumClass) {
+ maximumClass = partitionClassList[j];
+ }
+ }
+ int[] classDimensions = new int[maximumClass + 1];
+ for (int j = 0; j < classDimensions.length; j++) {
+ classDimensions[j] = bitArray.readBits(3) + 1;
+ int classSubclasses = bitArray.readBits(2);
+ if (classSubclasses > 0) {
+ bitArray.skipBits(8); // classMasterbooks
+ }
+ for (int k = 0; k < (1 << classSubclasses); k++) {
+ bitArray.skipBits(8); // subclassBook (subtract 1)
+ }
+ }
+ bitArray.skipBits(2); // multiplier (add one)
+ int rangeBits = bitArray.readBits(4);
+ int count = 0;
+ for (int j = 0, k = 0; j < partitions; j++) {
+ int idx = partitionClassList[j];
+ count += classDimensions[idx];
+ for (; k < count; k++) {
+ bitArray.skipBits(rangeBits); // floorValue
+ }
+ }
+ break;
+ default:
+ throw new ParserException("floor type greater than 1 not decodable: " + floorType);
+ }
+ }
+ }
+
+ private static CodeBook readBook(VorbisBitArray bitArray) throws ParserException {
+ if (bitArray.readBits(24) != 0x564342) {
+ throw new ParserException("expected code book to start with [0x56, 0x43, 0x42] at "
+ + bitArray.getPosition());
+ }
+ int dimensions = bitArray.readBits(16);
+ int entries = bitArray.readBits(24);
+ long[] lengthMap = new long[entries];
+
+ boolean isOrdered = bitArray.readBit();
+ if (!isOrdered) {
+ boolean isSparse = bitArray.readBit();
+ for (int i = 0; i < lengthMap.length; i++) {
+ if (isSparse) {
+ if (bitArray.readBit()) {
+ lengthMap[i] = (long) (bitArray.readBits(5) + 1);
+ } else { // entry unused
+ lengthMap[i] = 0;
+ }
+ } else { // not sparse
+ lengthMap[i] = (long) (bitArray.readBits(5) + 1);
+ }
+ }
+ } else {
+ int length = bitArray.readBits(5) + 1;
+ for (int i = 0; i < lengthMap.length;) {
+ int num = bitArray.readBits(iLog(entries - i));
+ for (int j = 0; j < num && i < lengthMap.length; i++, j++) {
+ lengthMap[i] = length;
+ }
+ length++;
+ }
+ }
+
+ int lookupType = bitArray.readBits(4);
+ if (lookupType > 2) {
+ throw new ParserException("lookup type greater than 2 not decodable: " + lookupType);
+ } else if (lookupType == 1 || lookupType == 2) {
+ bitArray.skipBits(32); // minimumValue
+ bitArray.skipBits(32); // deltaValue
+ int valueBits = bitArray.readBits(4) + 1;
+ bitArray.skipBits(1); // sequenceP
+ long lookupValuesCount;
+ if (lookupType == 1) {
+ if (dimensions != 0) {
+ lookupValuesCount = mapType1QuantValues(entries, dimensions);
+ } else {
+ lookupValuesCount = 0;
+ }
+ } else {
+ lookupValuesCount = (long) entries * dimensions;
+ }
+ // discard (no decoding required yet)
+ bitArray.skipBits((int) (lookupValuesCount * valueBits));
+ }
+ return new CodeBook(dimensions, entries, lengthMap, lookupType, isOrdered);
+ }
+
+ /**
+ * @see <a href="http://svn.xiph.org/trunk/vorbis/lib/sharedbook.c">_book_maptype1_quantvals</a>
+ */
+ private static long mapType1QuantValues(long entries, long dimension) {
+ return (long) Math.floor(Math.pow(entries, 1.d / dimension));
+ }
+
+ private VorbisUtil() {
+ // Prevent instantiation.
+ }
+
+ private static final class CodeBook {
+
+ public final int dimensions;
+ public final int entries;
+ public final long[] lengthMap;
+ public final int lookupType;
+ public final boolean isOrdered;
+
+ public CodeBook(int dimensions, int entries, long[] lengthMap, int lookupType,
+ boolean isOrdered) {
+ this.dimensions = dimensions;
+ this.entries = entries;
+ this.lengthMap = lengthMap;
+ this.lookupType = lookupType;
+ this.isOrdered = isOrdered;
+ }
+
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java
new file mode 100644
index 0000000000..35f539a394
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java
new file mode 100644
index 0000000000..d13b1f394d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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}.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java
new file mode 100644
index 0000000000..fa997001e8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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}.
+ *
+ * <ul>
+ * <li>If the search is successful, the position is set to the start of the found frame.
+ * <li>Otherwise, the position is set to the first unsearched byte.
+ * </ul>
+ *
+ * @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java
new file mode 100644
index 0000000000..54dbaec003
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java
@@ -0,0 +1,130 @@
+/*
+ * 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.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;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Collections;
+
+/**
+ * Parses audio tags from an FLV stream and extracts AAC frames.
+ */
+/* package */ final class AudioTagPayloadReader extends TagPayloadReader {
+
+ private static final int AUDIO_FORMAT_MP3 = 2;
+ private static final int AUDIO_FORMAT_ALAW = 7;
+ private static final int AUDIO_FORMAT_ULAW = 8;
+ private static final int AUDIO_FORMAT_AAC = 10;
+
+ private static final int AAC_PACKET_TYPE_SEQUENCE_HEADER = 0;
+ private static final int AAC_PACKET_TYPE_AAC_RAW = 1;
+
+ private static final int[] AUDIO_SAMPLING_RATE_TABLE = new int[] {5512, 11025, 22050, 44100};
+
+ // State variables
+ private boolean hasParsedAudioDataHeader;
+ private boolean hasOutputFormat;
+ private int audioFormat;
+
+ public AudioTagPayloadReader(TrackOutput output) {
+ super(output);
+ }
+
+ @Override
+ public void seek() {
+ // Do nothing.
+ }
+
+ @Override
+ protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException {
+ if (!hasParsedAudioDataHeader) {
+ int header = data.readUnsignedByte();
+ audioFormat = (header >> 4) & 0x0F;
+ if (audioFormat == AUDIO_FORMAT_MP3) {
+ int sampleRateIndex = (header >> 2) & 0x03;
+ int sampleRate = AUDIO_SAMPLING_RATE_TABLE[sampleRateIndex];
+ Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_MPEG, null,
+ Format.NO_VALUE, Format.NO_VALUE, 1, sampleRate, null, null, 0, null);
+ output.format(format);
+ hasOutputFormat = true;
+ } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) {
+ String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW
+ : 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) {
+ throw new UnsupportedFormatException("Audio format not supported: " + audioFormat);
+ }
+ hasParsedAudioDataHeader = true;
+ } else {
+ // Skip header if it was parsed previously.
+ data.skipBytes(1);
+ }
+ return true;
+ }
+
+ @Override
+ 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) {
+ // Parse the sequence header.
+ byte[] audioSpecificConfig = new byte[data.bytesLeft()];
+ data.readBytes(audioSpecificConfig, 0, audioSpecificConfig.length);
+ Pair<Integer, Integer> audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig(
+ audioSpecificConfig);
+ Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null,
+ Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first,
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java
new file mode 100644
index 0000000000..a7438b190f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java
@@ -0,0 +1,308 @@
+/*
+ * 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.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;
+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.util.ParsableByteArray;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Extracts data from the FLV container format.
+ */
+public final class FlvExtractor implements Extractor {
+
+ /** Factory for {@link FlvExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> 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 {}
+
+ 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 = 0x00464c56;
+
+ private final ParsableByteArray scratch;
+ private final ParsableByteArray headerBuffer;
+ private final ParsableByteArray tagHeaderBuffer;
+ private final ParsableByteArray tagData;
+ private final ScriptTagPayloadReader metadataReader;
+
+ private ExtractorOutput extractorOutput;
+ private @States int state;
+ private boolean outputFirstSample;
+ private long mediaTagTimestampOffsetUs;
+ private int bytesToNextTagHeader;
+ private int tagType;
+ private int tagDataSize;
+ private long tagTimestampUs;
+ private boolean outputSeekMap;
+ private AudioTagPayloadReader audioReader;
+ private VideoTagPayloadReader videoReader;
+
+ public FlvExtractor() {
+ scratch = new ParsableByteArray(4);
+ headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE);
+ tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE);
+ tagData = new ParsableByteArray();
+ metadataReader = new ScriptTagPayloadReader();
+ state = STATE_READING_FLV_HEADER;
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ // Check if file starts with "FLV" tag
+ input.peekFully(scratch.data, 0, 3);
+ scratch.setPosition(0);
+ if (scratch.readUnsignedInt24() != FLV_TAG) {
+ return false;
+ }
+
+ // Checking reserved flags are set to 0
+ input.peekFully(scratch.data, 0, 2);
+ scratch.setPosition(0);
+ if ((scratch.readUnsignedShort() & 0xFA) != 0) {
+ return false;
+ }
+
+ // Read data offset
+ input.peekFully(scratch.data, 0, 4);
+ scratch.setPosition(0);
+ int dataOffset = scratch.readInt();
+
+ input.resetPeekPosition();
+ input.advancePeekPosition(dataOffset);
+
+ // Checking first "previous tag size" is set to 0
+ input.peekFully(scratch.data, 0, 4);
+ scratch.setPosition(0);
+
+ return scratch.readInt() == 0;
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.extractorOutput = output;
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ state = STATE_READING_FLV_HEADER;
+ outputFirstSample = false;
+ bytesToNextTagHeader = 0;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
+ InterruptedException {
+ while (true) {
+ switch (state) {
+ case STATE_READING_FLV_HEADER:
+ if (!readFlvHeader(input)) {
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_SKIPPING_TO_TAG_HEADER:
+ skipToTagHeader(input);
+ break;
+ case STATE_READING_TAG_HEADER:
+ if (!readTagHeader(input)) {
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_READING_TAG_DATA:
+ if (readTagData(input)) {
+ return RESULT_CONTINUE;
+ }
+ break;
+ default:
+ // Never happens.
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ /**
+ * Reads an FLV container header from the provided {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @return True if header was read successfully. False if the end of stream was reached.
+ * @throws IOException If an error occurred reading or parsing data from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private boolean readFlvHeader(ExtractorInput input) throws IOException, InterruptedException {
+ if (!input.readFully(headerBuffer.data, 0, FLV_HEADER_SIZE, true)) {
+ // We've reached the end of the stream.
+ return false;
+ }
+
+ headerBuffer.setPosition(0);
+ headerBuffer.skipBytes(4);
+ int flags = headerBuffer.readUnsignedByte();
+ boolean hasAudio = (flags & 0x04) != 0;
+ boolean hasVideo = (flags & 0x01) != 0;
+ if (hasAudio && audioReader == null) {
+ audioReader = new AudioTagPayloadReader(
+ extractorOutput.track(TAG_TYPE_AUDIO, C.TRACK_TYPE_AUDIO));
+ }
+ if (hasVideo && videoReader == null) {
+ videoReader = new VideoTagPayloadReader(
+ extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO));
+ }
+ extractorOutput.endTracks();
+
+ // 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;
+ state = STATE_SKIPPING_TO_TAG_HEADER;
+ return true;
+ }
+
+ /**
+ * Skips over data to reach the next tag header.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @throws IOException If an error occurred skipping data from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException {
+ input.skipFully(bytesToNextTagHeader);
+ bytesToNextTagHeader = 0;
+ state = STATE_READING_TAG_HEADER;
+ }
+
+ /**
+ * Reads a tag header from the provided {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @return True if tag header was read successfully. Otherwise, false.
+ * @throws IOException If an error occurred reading or parsing data from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private boolean readTagHeader(ExtractorInput input) throws IOException, InterruptedException {
+ if (!input.readFully(tagHeaderBuffer.data, 0, FLV_TAG_HEADER_SIZE, true)) {
+ // We've reached the end of the stream.
+ return false;
+ }
+
+ tagHeaderBuffer.setPosition(0);
+ tagType = tagHeaderBuffer.readUnsignedByte();
+ tagDataSize = tagHeaderBuffer.readUnsignedInt24();
+ tagTimestampUs = tagHeaderBuffer.readUnsignedInt24();
+ tagTimestampUs = ((tagHeaderBuffer.readUnsignedByte() << 24) | tagTimestampUs) * 1000L;
+ tagHeaderBuffer.skipBytes(3); // streamId
+ state = STATE_READING_TAG_DATA;
+ return true;
+ }
+
+ /**
+ * Reads the body of a tag from the provided {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @return True if the data was consumed by a reader. False if it was skipped.
+ * @throws IOException If an error occurred reading or parsing data from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException {
+ boolean wasConsumed = true;
+ boolean wasSampleOutput = false;
+ long timestampUs = getCurrentTimestampUs();
+ if (tagType == TAG_TYPE_AUDIO && audioReader != null) {
+ ensureReadyForMediaOutput();
+ wasSampleOutput = audioReader.consume(prepareTagData(input), timestampUs);
+ } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) {
+ 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.
+ state = STATE_SKIPPING_TO_TAG_HEADER;
+ return wasConsumed;
+ }
+
+ private ParsableByteArray prepareTagData(ExtractorInput input) throws IOException,
+ InterruptedException {
+ if (tagDataSize > tagData.capacity()) {
+ tagData.reset(new byte[Math.max(tagData.capacity() * 2, tagDataSize)], 0);
+ } else {
+ tagData.setPosition(0);
+ }
+ tagData.setLimit(tagDataSize);
+ input.readFully(tagData.data, 0, tagDataSize);
+ return tagData;
+ }
+
+ private void ensureReadyForMediaOutput() {
+ if (!outputSeekMap) {
+ extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ outputSeekMap = true;
+ }
+ }
+
+ private long getCurrentTimestampUs() {
+ return outputFirstSample
+ ? (mediaTagTimestampOffsetUs + tagTimestampUs)
+ : (metadataReader.getDurationUs() == C.TIME_UNSET ? 0 : tagTimestampUs);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java
new file mode 100644
index 0000000000..1494bf1c2e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java
@@ -0,0 +1,227 @@
+/*
+ * 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.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.DummyTrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Parses Script Data tags from an FLV stream and extracts metadata information.
+ */
+/* package */ final class ScriptTagPayloadReader extends TagPayloadReader {
+
+ private static final String NAME_METADATA = "onMetaData";
+ private static final String KEY_DURATION = "duration";
+
+ // AMF object types
+ private static final int AMF_TYPE_NUMBER = 0;
+ private static final int AMF_TYPE_BOOLEAN = 1;
+ private static final int AMF_TYPE_STRING = 2;
+ private static final int AMF_TYPE_OBJECT = 3;
+ private static final int AMF_TYPE_ECMA_ARRAY = 8;
+ private static final int AMF_TYPE_END_MARKER = 9;
+ private static final int AMF_TYPE_STRICT_ARRAY = 10;
+ private static final int AMF_TYPE_DATE = 11;
+
+ private long durationUs;
+
+ public ScriptTagPayloadReader() {
+ super(new DummyTrackOutput());
+ durationUs = C.TIME_UNSET;
+ }
+
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ public void seek() {
+ // Do nothing.
+ }
+
+ @Override
+ protected boolean parseHeader(ParsableByteArray data) {
+ return true;
+ }
+
+ @Override
+ protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
+ int nameType = readAmfType(data);
+ if (nameType != AMF_TYPE_STRING) {
+ // Should never happen.
+ throw new ParserException();
+ }
+ String name = readAmfString(data);
+ if (!NAME_METADATA.equals(name)) {
+ // We're only interested in metadata.
+ return false;
+ }
+ int type = readAmfType(data);
+ if (type != AMF_TYPE_ECMA_ARRAY) {
+ // We're not interested in this metadata.
+ return false;
+ }
+ // Set the duration to the value contained in the metadata, if present.
+ Map<String, Object> metadata = readAmfEcmaArray(data);
+ if (metadata.containsKey(KEY_DURATION)) {
+ double durationSeconds = (double) metadata.get(KEY_DURATION);
+ if (durationSeconds > 0.0) {
+ durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND);
+ }
+ }
+ return false;
+ }
+
+ private static int readAmfType(ParsableByteArray data) {
+ return data.readUnsignedByte();
+ }
+
+ /**
+ * Read a boolean from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static Boolean readAmfBoolean(ParsableByteArray data) {
+ return data.readUnsignedByte() == 1;
+ }
+
+ /**
+ * Read a double number from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static Double readAmfDouble(ParsableByteArray data) {
+ return Double.longBitsToDouble(data.readLong());
+ }
+
+ /**
+ * Read a string from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static String readAmfString(ParsableByteArray data) {
+ int size = data.readUnsignedShort();
+ int position = data.getPosition();
+ data.skipBytes(size);
+ return new String(data.data, position, size);
+ }
+
+ /**
+ * Read an array from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static ArrayList<Object> readAmfStrictArray(ParsableByteArray data) {
+ int count = data.readUnsignedIntToInt();
+ ArrayList<Object> list = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ int type = readAmfType(data);
+ Object value = readAmfData(data, type);
+ if (value != null) {
+ list.add(value);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Read an object from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static HashMap<String, Object> readAmfObject(ParsableByteArray data) {
+ HashMap<String, Object> array = new HashMap<>();
+ while (true) {
+ String key = readAmfString(data);
+ int type = readAmfType(data);
+ if (type == AMF_TYPE_END_MARKER) {
+ break;
+ }
+ Object value = readAmfData(data, type);
+ if (value != null) {
+ array.put(key, value);
+ }
+ }
+ return array;
+ }
+
+ /**
+ * Read an ECMA array from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static HashMap<String, Object> readAmfEcmaArray(ParsableByteArray data) {
+ int count = data.readUnsignedIntToInt();
+ HashMap<String, Object> array = new HashMap<>(count);
+ for (int i = 0; i < count; i++) {
+ String key = readAmfString(data);
+ int type = readAmfType(data);
+ Object value = readAmfData(data, type);
+ if (value != null) {
+ array.put(key, value);
+ }
+ }
+ return array;
+ }
+
+ /**
+ * Read a date from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static Date readAmfDate(ParsableByteArray data) {
+ Date date = new Date((long) readAmfDouble(data).doubleValue());
+ data.skipBytes(2); // Skip reserved bytes.
+ return date;
+ }
+
+ @Nullable
+ private static Object readAmfData(ParsableByteArray data, int type) {
+ switch (type) {
+ case AMF_TYPE_NUMBER:
+ return readAmfDouble(data);
+ case AMF_TYPE_BOOLEAN:
+ return readAmfBoolean(data);
+ case AMF_TYPE_STRING:
+ return readAmfString(data);
+ case AMF_TYPE_OBJECT:
+ return readAmfObject(data);
+ case AMF_TYPE_ECMA_ARRAY:
+ return readAmfEcmaArray(data);
+ case AMF_TYPE_STRICT_ARRAY:
+ return readAmfStrictArray(data);
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java
new file mode 100644
index 0000000000..3f8b51244a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java
@@ -0,0 +1,87 @@
+/*
+ * 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.flv;
+
+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.ParsableByteArray;
+
+/**
+ * Extracts individual samples from FLV tags, preserving original order.
+ */
+/* package */ abstract class TagPayloadReader {
+
+ /**
+ * Thrown when the format is not supported.
+ */
+ public static final class UnsupportedFormatException extends ParserException {
+
+ public UnsupportedFormatException(String msg) {
+ super(msg);
+ }
+
+ }
+
+ protected final TrackOutput output;
+
+ /**
+ * @param output A {@link TrackOutput} to which samples should be written.
+ */
+ protected TagPayloadReader(TrackOutput output) {
+ this.output = output;
+ }
+
+ /**
+ * Notifies the reader that a seek has occurred.
+ * <p>
+ * Following a call to this method, the data passed to the next invocation of
+ * {@link #consume(ParsableByteArray, long)} will not be a continuation of the data that
+ * was previously passed. Hence the reader should reset any internal state.
+ */
+ public abstract void seek();
+
+ /**
+ * Consumes payload data.
+ *
+ * @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 boolean consume(ParsableByteArray data, long timeUs) throws ParserException {
+ return parseHeader(data) && parsePayload(data, timeUs);
+ }
+
+ /**
+ * Parses tag header.
+ *
+ * @param data Buffer where the tag header is stored.
+ * @return Whether the header was parsed successfully.
+ * @throws ParserException If an error occurs parsing the header.
+ */
+ protected abstract boolean parseHeader(ParsableByteArray data) throws ParserException;
+
+ /**
+ * Parses tag payload.
+ *
+ * @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 boolean parsePayload(ParsableByteArray data, long timeUs)
+ throws ParserException;
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java
new file mode 100644
index 0000000000..6ed5206144
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java
@@ -0,0 +1,141 @@
+/*
+ * 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.flv;
+
+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.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.video.AvcConfig;
+
+/**
+ * Parses video tags from an FLV stream and extracts H.264 nal units.
+ */
+/* package */ final class VideoTagPayloadReader extends TagPayloadReader {
+
+ // Video codec.
+ private static final int VIDEO_CODEC_AVC = 7;
+
+ // Frame types.
+ private static final int VIDEO_FRAME_KEYFRAME = 1;
+ private static final int VIDEO_FRAME_VIDEO_INFO = 5;
+
+ // Packet types.
+ private static final int AVC_PACKET_TYPE_SEQUENCE_HEADER = 0;
+ private static final int AVC_PACKET_TYPE_AVC_NALU = 1;
+
+ // Temporary arrays.
+ private final ParsableByteArray nalStartCode;
+ private final ParsableByteArray nalLength;
+ private int nalUnitLengthFieldLength;
+
+ // State variables.
+ private boolean hasOutputFormat;
+ private boolean hasOutputKeyframe;
+ private int frameType;
+
+ /**
+ * @param output A {@link TrackOutput} to which samples should be written.
+ */
+ public VideoTagPayloadReader(TrackOutput output) {
+ super(output);
+ nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
+ nalLength = new ParsableByteArray(4);
+ }
+
+ @Override
+ public void seek() {
+ hasOutputKeyframe = false;
+ }
+
+ @Override
+ protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException {
+ int header = data.readUnsignedByte();
+ int frameType = (header >> 4) & 0x0F;
+ int videoCodec = (header & 0x0F);
+ // Support just H.264 encoded content.
+ if (videoCodec != VIDEO_CODEC_AVC) {
+ throw new UnsupportedFormatException("Video format not supported: " + videoCodec);
+ }
+ this.frameType = frameType;
+ return (frameType != VIDEO_FRAME_VIDEO_INFO);
+ }
+
+ @Override
+ protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
+ int packetType = data.readUnsignedByte();
+ 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) {
+ ParsableByteArray videoSequence = new ParsableByteArray(new byte[data.bytesLeft()]);
+ data.readBytes(videoSequence.data, 0, data.bytesLeft());
+ AvcConfig avcConfig = AvcConfig.parse(videoSequence);
+ nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength;
+ // Construct and output the format.
+ Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null,
+ Format.NO_VALUE, Format.NO_VALUE, avcConfig.width, avcConfig.height, Format.NO_VALUE,
+ 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.
+ byte[] nalLengthData = nalLength.data;
+ nalLengthData[0] = 0;
+ nalLengthData[1] = 0;
+ nalLengthData[2] = 0;
+ int nalUnitLengthFieldLengthDiff = 4 - nalUnitLengthFieldLength;
+ // NAL units are length delimited, but the decoder requires start code delimited units.
+ // Loop until we've written the sample to the track output, replacing length delimiters with
+ // start codes as we encounter them.
+ int bytesWritten = 0;
+ int bytesToWrite;
+ while (data.bytesLeft() > 0) {
+ // Read the NAL length so that we know where we find the next one.
+ data.readBytes(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);
+ nalLength.setPosition(0);
+ bytesToWrite = nalLength.readUnsignedIntToInt();
+
+ // Write a start code for the current NAL unit.
+ nalStartCode.setPosition(0);
+ output.sampleData(nalStartCode, 4);
+ bytesWritten += 4;
+
+ // Write the payload of the NAL unit.
+ output.sampleData(data, bytesToWrite);
+ bytesWritten += bytesToWrite;
+ }
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java
new file mode 100644
index 0000000000..b4e160fa74
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java
@@ -0,0 +1,260 @@
+/*
+ * 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.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.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;
+
+ private static final int MAX_ID_BYTES = 4;
+ private static final int MAX_LENGTH_BYTES = 8;
+
+ private static final int MAX_INTEGER_ELEMENT_SIZE_BYTES = 8;
+ private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4;
+ private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8;
+
+ private final byte[] scratch;
+ private final ArrayDeque<MasterElement> masterElementsStack;
+ private final VarintReader varintReader;
+
+ 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(EbmlProcessor processor) {
+ this.processor = processor;
+ }
+
+ @Override
+ public void reset() {
+ elementState = ELEMENT_STATE_READ_ID;
+ masterElementsStack.clear();
+ varintReader.reset();
+ }
+
+ @Override
+ public boolean read(ExtractorInput input) throws IOException, InterruptedException {
+ Assertions.checkNotNull(processor);
+ while (true) {
+ if (!masterElementsStack.isEmpty()
+ && input.getPosition() >= masterElementsStack.peek().elementEndPosition) {
+ processor.endMasterElement(masterElementsStack.pop().elementId);
+ return true;
+ }
+
+ if (elementState == ELEMENT_STATE_READ_ID) {
+ long result = varintReader.readUnsignedVarint(input, true, false, MAX_ID_BYTES);
+ if (result == C.RESULT_MAX_LENGTH_EXCEEDED) {
+ result = maybeResyncToNextLevel1Element(input);
+ }
+ if (result == C.RESULT_END_OF_INPUT) {
+ return false;
+ }
+ // Element IDs are at most 4 bytes, so we can cast to integers.
+ elementId = (int) result;
+ elementState = ELEMENT_STATE_READ_CONTENT_SIZE;
+ }
+
+ if (elementState == ELEMENT_STATE_READ_CONTENT_SIZE) {
+ elementContentSize = varintReader.readUnsignedVarint(input, false, true, MAX_LENGTH_BYTES);
+ elementState = ELEMENT_STATE_READ_CONTENT;
+ }
+
+ @EbmlProcessor.ElementType int type = processor.getElementType(elementId);
+ switch (type) {
+ case EbmlProcessor.ELEMENT_TYPE_MASTER:
+ long elementContentPosition = input.getPosition();
+ long elementEndPosition = elementContentPosition + elementContentSize;
+ masterElementsStack.push(new MasterElement(elementId, elementEndPosition));
+ processor.startMasterElement(elementId, elementContentPosition, elementContentSize);
+ elementState = ELEMENT_STATE_READ_ID;
+ return true;
+ case EbmlProcessor.ELEMENT_TYPE_UNSIGNED_INT:
+ if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
+ throw new ParserException("Invalid integer size: " + elementContentSize);
+ }
+ processor.integerElement(elementId, readInteger(input, (int) elementContentSize));
+ elementState = ELEMENT_STATE_READ_ID;
+ return true;
+ 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);
+ }
+ processor.floatElement(elementId, readFloat(input, (int) elementContentSize));
+ elementState = ELEMENT_STATE_READ_ID;
+ return true;
+ case EbmlProcessor.ELEMENT_TYPE_STRING:
+ if (elementContentSize > Integer.MAX_VALUE) {
+ throw new ParserException("String element size: " + elementContentSize);
+ }
+ processor.stringElement(elementId, readString(input, (int) elementContentSize));
+ elementState = ELEMENT_STATE_READ_ID;
+ return true;
+ case EbmlProcessor.ELEMENT_TYPE_BINARY:
+ processor.binaryElement(elementId, (int) elementContentSize, input);
+ elementState = ELEMENT_STATE_READ_ID;
+ return true;
+ case EbmlProcessor.ELEMENT_TYPE_UNKNOWN:
+ input.skipFully((int) elementContentSize);
+ elementState = ELEMENT_STATE_READ_ID;
+ break;
+ default:
+ throw new ParserException("Invalid element type " + type);
+ }
+ }
+ }
+
+ /**
+ * Does a byte by byte search to try and find the next level 1 element. This method is called if
+ * some invalid data is encountered in the parser.
+ *
+ * @param input The {@link ExtractorInput} from which data has to be read.
+ * @return id of the next level 1 element that has been found.
+ * @throws EOFException If the end of input was encountered when searching for the next level 1
+ * element.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private long maybeResyncToNextLevel1Element(ExtractorInput input) throws IOException,
+ InterruptedException {
+ input.resetPeekPosition();
+ while (true) {
+ input.peekFully(scratch, 0, MAX_ID_BYTES);
+ int varintLength = VarintReader.parseUnsignedVarintLength(scratch[0]);
+ if (varintLength != C.LENGTH_UNSET && varintLength <= MAX_ID_BYTES) {
+ int potentialId = (int) VarintReader.assembleVarint(scratch, varintLength, false);
+ if (processor.isLevel1Element(potentialId)) {
+ input.skipFully(varintLength);
+ return potentialId;
+ }
+ }
+ input.skipFully(1);
+ }
+ }
+
+ /**
+ * Reads and returns an integer of length {@code byteLength} from the {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @param byteLength The length of the integer being read.
+ * @return The read integer value.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private long readInteger(ExtractorInput input, int byteLength)
+ throws IOException, InterruptedException {
+ input.readFully(scratch, 0, byteLength);
+ long value = 0;
+ for (int i = 0; i < byteLength; i++) {
+ value = (value << 8) | (scratch[i] & 0xFF);
+ }
+ return value;
+ }
+
+ /**
+ * Reads and returns a float of length {@code byteLength} from the {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @param byteLength The length of the float being read.
+ * @return The read float value.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private double readFloat(ExtractorInput input, int byteLength)
+ throws IOException, InterruptedException {
+ long integerValue = readInteger(input, byteLength);
+ double floatValue;
+ if (byteLength == VALID_FLOAT32_ELEMENT_SIZE_BYTES) {
+ floatValue = Float.intBitsToFloat((int) integerValue);
+ } else {
+ floatValue = Double.longBitsToDouble(integerValue);
+ }
+ return floatValue;
+ }
+
+ /**
+ * 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 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.
+ */
+ private String readString(ExtractorInput input, int byteLength)
+ throws IOException, InterruptedException {
+ if (byteLength == 0) {
+ return "";
+ }
+ byte[] stringBytes = new byte[byteLength];
+ input.readFully(stringBytes, 0, byteLength);
+ // 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 EbmlProcessor#endMasterElement(int)} can be called.
+ */
+ private static final class MasterElement {
+
+ private final int elementId;
+ private final long elementEndPosition;
+
+ private MasterElement(int elementId, long elementEndPosition) {
+ this.elementId = elementId;
+ this.elementEndPosition = elementEndPosition;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java
new file mode 100644
index 0000000000..188ced0554
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlProcessor.java
@@ -0,0 +1,150 @@
+/*
+ * 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.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 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.
+ *
+ * <p>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 {@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);
+
+ /**
+ * Checks if the given id is that of a level 1 element.
+ *
+ * @param id The element ID.
+ * @return Whether the given id is that of a level 1 element.
+ */
+ boolean isLevel1Element(int id);
+
+ /**
+ * Called when the start of a master element is encountered.
+ * <p>
+ * Following events should be considered as taking place within this element until a matching call
+ * to {@link #endMasterElement(int)} is made.
+ * <p>
+ * Note that it is possible for another master element of the same element ID to be nested within
+ * itself.
+ *
+ * @param id The element ID.
+ * @param contentPosition The position of the start of the element's content in the stream.
+ * @param contentSize The size of the element's content in bytes.
+ * @throws ParserException If a parsing error occurs.
+ */
+ void startMasterElement(int id, long contentPosition, long contentSize) throws ParserException;
+
+ /**
+ * Called when the end of a master element is encountered.
+ *
+ * @param id The element ID.
+ * @throws ParserException If a parsing error occurs.
+ */
+ void endMasterElement(int id) throws ParserException;
+
+ /**
+ * Called when an integer element is encountered.
+ *
+ * @param id The element ID.
+ * @param value The integer value that the element contains.
+ * @throws ParserException If a parsing error occurs.
+ */
+ void integerElement(int id, long value) throws ParserException;
+
+ /**
+ * Called when a float element is encountered.
+ *
+ * @param id The element ID.
+ * @param value The float value that the element contains
+ * @throws ParserException If a parsing error occurs.
+ */
+ void floatElement(int id, double value) throws ParserException;
+
+ /**
+ * Called when a string element is encountered.
+ *
+ * @param id The element ID.
+ * @param value The string value that the element contains.
+ * @throws ParserException If a parsing error occurs.
+ */
+ void stringElement(int id, String value) throws ParserException;
+
+ /**
+ * Called when a binary element is encountered.
+ * <p>
+ * The element header (containing the element ID and content size) will already have been read.
+ * Implementations are required to consume the whole remainder of the element, which is
+ * {@code contentSize} bytes in length, before returning. Implementations are permitted to fail
+ * (by throwing an exception) having partially consumed the data, however if they do this, they
+ * must consume the remainder of the content when called again.
+ *
+ * @param id The element ID.
+ * @param contentsSize The element's content size.
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @throws ParserException If a parsing error occurs.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ void binaryElement(int id, int contentsSize, ExtractorInput input)
+ throws IOException, InterruptedException;
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java
new file mode 100644
index 0000000000..1416a9087e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.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.extractor.mkv;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import java.io.IOException;
+
+/**
+ * Event-driven EBML reader that delivers events to an {@link EbmlProcessor}.
+ *
+ * <p>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 <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
+ */
+/* package */ interface EbmlReader {
+
+ /**
+ * Initializes the extractor with an {@link EbmlProcessor}.
+ *
+ * @param processor An {@link EbmlProcessor} to process events.
+ */
+ void init(EbmlProcessor processor);
+
+ /**
+ * Resets the state of the reader.
+ * <p>
+ * Subsequent calls to {@link #read(ExtractorInput)} will start reading a new EBML structure
+ * from scratch.
+ */
+ void reset();
+
+ /**
+ * Reads from an {@link ExtractorInput}, invoking an event callback if possible.
+ *
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @return True if data can continue to be read. False if the end of the input was encountered.
+ * @throws ParserException If parsing fails.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ boolean read(ExtractorInput input) throws IOException, InterruptedException;
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
new file mode 100644
index 0000000000..d9587cd27e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
@@ -0,0 +1,2331 @@
+/*
+ * 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.mkv;
+
+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;
+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.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.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;
+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.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;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+
+/** 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()};
+
+ /**
+ * 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})
+ public @interface Flags {}
+ /**
+ * Flag to disable seeking for cues.
+ * <p>
+ * Normally (i.e. when this flag is not set) the extractor will seek to the cues element if its
+ * position is specified in the seek head and if it's after the first cluster. Setting this flag
+ * disables seeking to the cues element. If the cues element is after the first cluster then the
+ * media is treated as being unseekable.
+ */
+ 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;
+ private static final int BLOCK_STATE_HEADER = 1;
+ private static final int BLOCK_STATE_DATA = 2;
+
+ private static final String DOC_TYPE_MATROSKA = "matroska";
+ 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";
+ private static final String CODEC_ID_MPEG4_AP = "V_MPEG4/ISO/AP";
+ private static final String CODEC_ID_H264 = "V_MPEG4/ISO/AVC";
+ private static final String CODEC_ID_H265 = "V_MPEGH/ISO/HEVC";
+ private static final String CODEC_ID_FOURCC = "V_MS/VFW/FOURCC";
+ private static final String CODEC_ID_THEORA = "V_THEORA";
+ private static final String CODEC_ID_VORBIS = "A_VORBIS";
+ private static final String CODEC_ID_OPUS = "A_OPUS";
+ private static final String CODEC_ID_AAC = "A_AAC";
+ private static final String CODEC_ID_MP2 = "A_MPEG/L2";
+ private static final String CODEC_ID_MP3 = "A_MPEG/L3";
+ private static final String CODEC_ID_AC3 = "A_AC3";
+ private static final String CODEC_ID_E_AC3 = "A_EAC3";
+ private static final String CODEC_ID_TRUEHD = "A_TRUEHD";
+ private static final String CODEC_ID_DTS = "A_DTS";
+ private static final String CODEC_ID_DTS_EXPRESS = "A_DTS/EXPRESS";
+ private static final String CODEC_ID_DTS_LOSSLESS = "A_DTS/LOSSLESS";
+ private static final String CODEC_ID_FLAC = "A_FLAC";
+ 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";
+
+ private static final int VORBIS_MAX_INPUT_SIZE = 8192;
+ private static final int OPUS_MAX_INPUT_SIZE = 5760;
+ private static final int ENCRYPTION_IV_SIZE = 8;
+ private static final int TRACK_TYPE_AUDIO = 2;
+
+ private static final int ID_EBML = 0x1A45DFA3;
+ private static final int ID_EBML_READ_VERSION = 0x42F7;
+ private static final int ID_DOC_TYPE = 0x4282;
+ private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
+ private static final int ID_SEGMENT = 0x18538067;
+ private static final int ID_SEGMENT_INFO = 0x1549A966;
+ private static final int ID_SEEK_HEAD = 0x114D9B74;
+ private static final int ID_SEEK = 0x4DBB;
+ private static final int ID_SEEK_ID = 0x53AB;
+ private static final int ID_SEEK_POSITION = 0x53AC;
+ private static final int ID_INFO = 0x1549A966;
+ private static final int ID_TIMECODE_SCALE = 0x2AD7B1;
+ private static final int ID_DURATION = 0x4489;
+ private static final int ID_CLUSTER = 0x1F43B675;
+ private static final int ID_TIME_CODE = 0xE7;
+ private static final int ID_SIMPLE_BLOCK = 0xA3;
+ 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;
+ private static final int ID_TRACK_NUMBER = 0xD7;
+ private static final int ID_TRACK_TYPE = 0x83;
+ 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;
+ private static final int ID_SEEK_PRE_ROLL = 0x56BB;
+ private static final int ID_VIDEO = 0xE0;
+ private static final int ID_PIXEL_WIDTH = 0xB0;
+ private static final int ID_PIXEL_HEIGHT = 0xBA;
+ private static final int ID_DISPLAY_WIDTH = 0x54B0;
+ private static final int ID_DISPLAY_HEIGHT = 0x54BA;
+ private static final int ID_DISPLAY_UNIT = 0x54B2;
+ private static final int ID_AUDIO = 0xE1;
+ private static final int ID_CHANNELS = 0x9F;
+ private static final int ID_AUDIO_BIT_DEPTH = 0x6264;
+ private static final int ID_SAMPLING_FREQUENCY = 0xB5;
+ private static final int ID_CONTENT_ENCODINGS = 0x6D80;
+ private static final int ID_CONTENT_ENCODING = 0x6240;
+ private static final int ID_CONTENT_ENCODING_ORDER = 0x5031;
+ private static final int ID_CONTENT_ENCODING_SCOPE = 0x5032;
+ private static final int ID_CONTENT_COMPRESSION = 0x5034;
+ private static final int ID_CONTENT_COMPRESSION_ALGORITHM = 0x4254;
+ private static final int ID_CONTENT_COMPRESSION_SETTINGS = 0x4255;
+ private static final int ID_CONTENT_ENCRYPTION = 0x5035;
+ private static final int ID_CONTENT_ENCRYPTION_ALGORITHM = 0x47E1;
+ private static final int ID_CONTENT_ENCRYPTION_KEY_ID = 0x47E2;
+ private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS = 0x47E7;
+ private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE = 0x47E8;
+ private static final int ID_CUES = 0x1C53BB6B;
+ private static final int ID_CUE_POINT = 0xBB;
+ private static final int ID_CUE_TIME = 0xB3;
+ private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
+ 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;
+ private static final int ID_COLOUR_TRANSFER = 0x55BA;
+ private static final int ID_COLOUR_PRIMARIES = 0x55BB;
+ private static final int ID_MAX_CLL = 0x55BC;
+ private static final int ID_MAX_FALL = 0x55BD;
+ private static final int ID_MASTERING_METADATA = 0x55D0;
+ private static final int ID_PRIMARY_R_CHROMATICITY_X = 0x55D1;
+ private static final int ID_PRIMARY_R_CHROMATICITY_Y = 0x55D2;
+ private static final int ID_PRIMARY_G_CHROMATICITY_X = 0x55D3;
+ private static final int ID_PRIMARY_G_CHROMATICITY_Y = 0x55D4;
+ private static final int ID_PRIMARY_B_CHROMATICITY_X = 0x55D5;
+ private static final int ID_PRIMARY_B_CHROMATICITY_Y = 0x55D6;
+ private static final int ID_WHITE_POINT_CHROMATICITY_X = 0x55D7;
+ private static final int ID_WHITE_POINT_CHROMATICITY_Y = 0x55D8;
+ 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.
+ *
+ * <p>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.
+ *
+ * <p>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
+ };
+ /**
+ * The byte offset of the end timecode in {@link #SUBRIP_PREFIX}.
+ */
+ private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19;
+ /**
+ * 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 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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ */
+ private static final int WAVE_FORMAT_SIZE = 18;
+ /**
+ * Format tag indicating a WAVEFORMATEXTENSIBLE structure.
+ */
+ private static final int WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
+ /**
+ * Format tag for PCM.
+ */
+ private static final int WAVE_FORMAT_PCM = 1;
+ /**
+ * Sub format for PCM.
+ */
+ private static final UUID WAVE_SUBFORMAT_PCM = new UUID(0x0100000000001000L, 0x800000AA00389B71L);
+
+ private final EbmlReader reader;
+ private final VarintReader varintReader;
+ private final SparseArray<Track> tracks;
+ private final boolean seekForCuesEnabled;
+
+ // Temporary arrays.
+ private final ParsableByteArray nalStartCode;
+ private final ParsableByteArray nalLength;
+ private final ParsableByteArray scratch;
+ private final ParsableByteArray vorbisNumPageSamples;
+ private final ParsableByteArray seekEntryIdBytes;
+ private final ParsableByteArray sampleStrippedBytes;
+ private final ParsableByteArray subtitleSample;
+ private final ParsableByteArray encryptionInitializationVector;
+ private final ParsableByteArray encryptionSubsampleData;
+ private final ParsableByteArray blockAdditionalData;
+ private ByteBuffer encryptionSubsampleDataBuffer;
+
+ private long segmentContentSize;
+ private long segmentContentPosition = C.POSITION_UNSET;
+ private long timecodeScale = C.TIME_UNSET;
+ private long durationTimecode = C.TIME_UNSET;
+ private long durationUs = C.TIME_UNSET;
+
+ // The track corresponding to the current TrackEntry element, or null.
+ private Track currentTrack;
+
+ // Whether a seek map has been sent to the output.
+ private boolean sentSeekMap;
+
+ // Master seek entry related elements.
+ private int seekEntryId;
+ private long seekEntryPosition;
+
+ // Cue related elements.
+ private boolean seekForCues;
+ private long cuesContentPosition = C.POSITION_UNSET;
+ private long seekPositionAfterBuildingCues = C.POSITION_UNSET;
+ private long clusterTimecodeUs = C.TIME_UNSET;
+ private LongArray cueTimesUs;
+ 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 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 writing state.
+ private int sampleBytesRead;
+ private int sampleBytesWritten;
+ private int sampleCurrentNalBytesRemaining;
+ private boolean sampleEncodingHandled;
+ private boolean sampleSignalByteRead;
+ private boolean samplePartitionCountRead;
+ private int samplePartitionCount;
+ private byte sampleSignalByte;
+ private boolean sampleInitializationVectorRead;
+
+ // Extractor outputs.
+ private ExtractorOutput extractorOutput;
+
+ public MatroskaExtractor() {
+ this(0);
+ }
+
+ public MatroskaExtractor(@Flags int flags) {
+ this(new DefaultEbmlReader(), flags);
+ }
+
+ /* package */ MatroskaExtractor(EbmlReader reader, @Flags int flags) {
+ this.reader = reader;
+ this.reader.init(new InnerEbmlProcessor());
+ seekForCuesEnabled = (flags & FLAG_DISABLE_SEEK_FOR_CUES) == 0;
+ varintReader = new VarintReader();
+ tracks = new SparseArray<>();
+ scratch = new ParsableByteArray(4);
+ vorbisNumPageSamples = new ParsableByteArray(ByteBuffer.allocate(4).putInt(-1).array());
+ seekEntryIdBytes = new ParsableByteArray(4);
+ nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
+ nalLength = new ParsableByteArray(4);
+ sampleStrippedBytes = new ParsableByteArray();
+ subtitleSample = new ParsableByteArray();
+ encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE);
+ encryptionSubsampleData = new ParsableByteArray();
+ blockAdditionalData = new ParsableByteArray();
+ }
+
+ @Override
+ public final boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ return new Sniffer().sniff(input);
+ }
+
+ @Override
+ 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();
+ resetWriteSampleData();
+ for (int i = 0; i < tracks.size(); i++) {
+ tracks.valueAt(i).reset();
+ }
+ }
+
+ @Override
+ public final void release() {
+ // Do nothing
+ }
+
+ @Override
+ public final int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ haveOutputSample = false;
+ boolean continueReading = true;
+ while (continueReading && !haveOutputSample) {
+ continueReading = reader.read(input);
+ if (continueReading && maybeSeekForCues(seekPosition, input.getPosition())) {
+ return Extractor.RESULT_SEEK;
+ }
+ }
+ if (!continueReading) {
+ for (int i = 0; i < tracks.size(); i++) {
+ tracks.valueAt(i).outputPendingSampleMetadata();
+ }
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ /**
+ * 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:
+ case ID_SEEK_HEAD:
+ case ID_SEEK:
+ case ID_INFO:
+ case ID_CLUSTER:
+ case ID_TRACKS:
+ case ID_TRACK_ENTRY:
+ case ID_AUDIO:
+ case ID_VIDEO:
+ case ID_CONTENT_ENCODINGS:
+ case ID_CONTENT_ENCODING:
+ case ID_CONTENT_COMPRESSION:
+ case ID_CONTENT_ENCRYPTION:
+ case ID_CONTENT_ENCRYPTION_AES_SETTINGS:
+ case ID_CUES:
+ 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 EbmlProcessor.ELEMENT_TYPE_MASTER;
+ case ID_EBML_READ_VERSION:
+ case ID_DOC_TYPE_READ_VERSION:
+ case ID_SEEK_POSITION:
+ case ID_TIMECODE_SCALE:
+ case ID_TIME_CODE:
+ case ID_BLOCK_DURATION:
+ case ID_PIXEL_WIDTH:
+ case ID_PIXEL_HEIGHT:
+ case ID_DISPLAY_WIDTH:
+ case ID_DISPLAY_HEIGHT:
+ case ID_DISPLAY_UNIT:
+ case ID_TRACK_NUMBER:
+ case ID_TRACK_TYPE:
+ 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:
+ case ID_AUDIO_BIT_DEPTH:
+ case ID_CONTENT_ENCODING_ORDER:
+ case ID_CONTENT_ENCODING_SCOPE:
+ case ID_CONTENT_COMPRESSION_ALGORITHM:
+ case ID_CONTENT_ENCRYPTION_ALGORITHM:
+ case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:
+ case ID_CUE_TIME:
+ case ID_CUE_CLUSTER_POSITION:
+ case ID_REFERENCE_BLOCK:
+ case ID_STEREO_MODE:
+ case ID_COLOUR_RANGE:
+ case ID_COLOUR_TRANSFER:
+ case ID_COLOUR_PRIMARIES:
+ case ID_MAX_CLL:
+ case ID_MAX_FALL:
+ 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 EbmlProcessor.ELEMENT_TYPE_STRING;
+ case ID_SEEK_ID:
+ case ID_CONTENT_COMPRESSION_SETTINGS:
+ case ID_CONTENT_ENCRYPTION_KEY_ID:
+ case ID_SIMPLE_BLOCK:
+ case ID_BLOCK:
+ case ID_CODEC_PRIVATE:
+ case ID_PROJECTION_PRIVATE:
+ case ID_BLOCK_ADDITIONAL:
+ return EbmlProcessor.ELEMENT_TYPE_BINARY;
+ case ID_DURATION:
+ case ID_SAMPLING_FREQUENCY:
+ case ID_PRIMARY_R_CHROMATICITY_X:
+ case ID_PRIMARY_R_CHROMATICITY_Y:
+ case ID_PRIMARY_G_CHROMATICITY_X:
+ case ID_PRIMARY_G_CHROMATICITY_Y:
+ case ID_PRIMARY_B_CHROMATICITY_X:
+ case ID_PRIMARY_B_CHROMATICITY_Y:
+ case ID_WHITE_POINT_CHROMATICITY_X:
+ case ID_WHITE_POINT_CHROMATICITY_Y:
+ case ID_LUMNINANCE_MAX:
+ case ID_LUMNINANCE_MIN:
+ case ID_PROJECTION_POSE_YAW:
+ case ID_PROJECTION_POSE_PITCH:
+ case ID_PROJECTION_POSE_ROLL:
+ return EbmlProcessor.ELEMENT_TYPE_FLOAT;
+ default:
+ return EbmlProcessor.ELEMENT_TYPE_UNKNOWN;
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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:
+ if (segmentContentPosition != C.POSITION_UNSET
+ && segmentContentPosition != contentPosition) {
+ throw new ParserException("Multiple Segment elements not supported");
+ }
+ segmentContentPosition = contentPosition;
+ segmentContentSize = contentSize;
+ break;
+ case ID_SEEK:
+ seekEntryId = UNSET_ENTRY_ID;
+ seekEntryPosition = C.POSITION_UNSET;
+ break;
+ case ID_CUES:
+ cueTimesUs = new LongArray();
+ cueClusterPositions = new LongArray();
+ break;
+ case ID_CUE_POINT:
+ seenClusterPositionForCurrentCuePoint = false;
+ break;
+ case ID_CLUSTER:
+ if (!sentSeekMap) {
+ // We need to build cues before parsing the cluster.
+ if (seekForCuesEnabled && cuesContentPosition != C.POSITION_UNSET) {
+ // We know where the Cues element is located. Seek to request it.
+ seekForCues = true;
+ } else {
+ // We don't know where the Cues element is located. It's most likely omitted. Allow
+ // playback, but disable seeking.
+ extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
+ sentSeekMap = true;
+ }
+ }
+ break;
+ case ID_BLOCK_GROUP:
+ blockHasReferenceBlock = false;
+ break;
+ case ID_CONTENT_ENCODING:
+ // TODO: check and fail if more than one content encoding is present.
+ break;
+ case ID_CONTENT_ENCRYPTION:
+ currentTrack.hasContentEncryption = true;
+ break;
+ case ID_TRACK_ENTRY:
+ currentTrack = new Track();
+ break;
+ case ID_MASTERING_METADATA:
+ currentTrack.hasColorInfo = true;
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * 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) {
+ // timecodeScale was omitted. Use the default value.
+ timecodeScale = 1000000;
+ }
+ if (durationTimecode != C.TIME_UNSET) {
+ durationUs = scaleTimecodeToUs(durationTimecode);
+ }
+ break;
+ case ID_SEEK:
+ if (seekEntryId == UNSET_ENTRY_ID || seekEntryPosition == C.POSITION_UNSET) {
+ throw new ParserException("Mandatory element SeekID or SeekPosition not found");
+ }
+ if (seekEntryId == ID_CUES) {
+ cuesContentPosition = seekEntryPosition;
+ }
+ break;
+ case ID_CUES:
+ if (!sentSeekMap) {
+ extractorOutput.seekMap(buildSeekMap());
+ sentSeekMap = true;
+ } else {
+ // We have already built the cues. Ignore.
+ }
+ break;
+ case ID_BLOCK_GROUP:
+ if (blockState != BLOCK_STATE_DATA) {
+ // We've skipped this block (due to incompatible track number).
+ return;
+ }
+ // 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);
+ }
+ blockState = BLOCK_STATE_START;
+ break;
+ case ID_CONTENT_ENCODING:
+ if (currentTrack.hasContentEncryption) {
+ 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.cryptoData.encryptionKey));
+ }
+ break;
+ case ID_CONTENT_ENCODINGS:
+ if (currentTrack.hasContentEncryption && currentTrack.sampleStrippedBytes != null) {
+ throw new ParserException("Combining encryption and compression is not supported");
+ }
+ break;
+ case ID_TRACK_ENTRY:
+ if (isCodecSupported(currentTrack.codecId)) {
+ currentTrack.initializeOutput(extractorOutput, currentTrack.number);
+ tracks.put(currentTrack.number, currentTrack);
+ }
+ currentTrack = null;
+ break;
+ case ID_TRACKS:
+ if (tracks.size() == 0) {
+ throw new ParserException("No valid tracks were found");
+ }
+ extractorOutput.endTracks();
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * 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.
+ if (value != 1) {
+ throw new ParserException("EBMLReadVersion " + value + " not supported");
+ }
+ break;
+ case ID_DOC_TYPE_READ_VERSION:
+ // Validate that DocTypeReadVersion is supported. This extractor only supports up to v2.
+ if (value < 1 || value > 2) {
+ throw new ParserException("DocTypeReadVersion " + value + " not supported");
+ }
+ break;
+ case ID_SEEK_POSITION:
+ // Seek Position is the relative offset beginning from the Segment. So to get absolute
+ // offset from the beginning of the file, we need to add segmentContentPosition to it.
+ seekEntryPosition = value + segmentContentPosition;
+ break;
+ case ID_TIMECODE_SCALE:
+ timecodeScale = value;
+ break;
+ case ID_PIXEL_WIDTH:
+ currentTrack.width = (int) value;
+ break;
+ case ID_PIXEL_HEIGHT:
+ currentTrack.height = (int) value;
+ break;
+ case ID_DISPLAY_WIDTH:
+ currentTrack.displayWidth = (int) value;
+ break;
+ case ID_DISPLAY_HEIGHT:
+ currentTrack.displayHeight = (int) value;
+ break;
+ case ID_DISPLAY_UNIT:
+ currentTrack.displayUnit = (int) value;
+ break;
+ case ID_TRACK_NUMBER:
+ currentTrack.number = (int) value;
+ break;
+ case ID_FLAG_DEFAULT:
+ currentTrack.flagDefault = value == 1;
+ break;
+ case ID_FLAG_FORCED:
+ currentTrack.flagForced = value == 1;
+ break;
+ case ID_TRACK_TYPE:
+ currentTrack.type = (int) value;
+ break;
+ 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;
+ case ID_SEEK_PRE_ROLL:
+ currentTrack.seekPreRollNs = value;
+ break;
+ case ID_CHANNELS:
+ currentTrack.channelCount = (int) value;
+ break;
+ case ID_AUDIO_BIT_DEPTH:
+ currentTrack.audioBitDepth = (int) value;
+ break;
+ case ID_REFERENCE_BLOCK:
+ blockHasReferenceBlock = true;
+ break;
+ case ID_CONTENT_ENCODING_ORDER:
+ // This extractor only supports one ContentEncoding element and hence the order has to be 0.
+ if (value != 0) {
+ throw new ParserException("ContentEncodingOrder " + value + " not supported");
+ }
+ break;
+ case ID_CONTENT_ENCODING_SCOPE:
+ // This extractor only supports the scope of all frames.
+ if (value != 1) {
+ throw new ParserException("ContentEncodingScope " + value + " not supported");
+ }
+ break;
+ case ID_CONTENT_COMPRESSION_ALGORITHM:
+ // This extractor only supports header stripping.
+ if (value != 3) {
+ throw new ParserException("ContentCompAlgo " + value + " not supported");
+ }
+ break;
+ case ID_CONTENT_ENCRYPTION_ALGORITHM:
+ // Only the value 5 (AES) is allowed according to the WebM specification.
+ if (value != 5) {
+ throw new ParserException("ContentEncAlgo " + value + " not supported");
+ }
+ break;
+ case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:
+ // Only the value 1 is allowed according to the WebM specification.
+ if (value != 1) {
+ throw new ParserException("AESSettingsCipherMode " + value + " not supported");
+ }
+ break;
+ case ID_CUE_TIME:
+ cueTimesUs.add(scaleTimecodeToUs(value));
+ break;
+ case ID_CUE_CLUSTER_POSITION:
+ if (!seenClusterPositionForCurrentCuePoint) {
+ // If there's more than one video/audio track, then there could be more than one
+ // CueTrackPositions within a single CuePoint. In such a case, ignore all but the first
+ // one (since the cluster position will be quite close for all the tracks).
+ cueClusterPositions.add(value);
+ seenClusterPositionForCurrentCuePoint = true;
+ }
+ break;
+ case ID_TIME_CODE:
+ clusterTimecodeUs = scaleTimecodeToUs(value);
+ break;
+ case ID_BLOCK_DURATION:
+ blockDurationUs = scaleTimecodeToUs(value);
+ break;
+ case ID_STEREO_MODE:
+ int layout = (int) value;
+ switch (layout) {
+ case 0:
+ currentTrack.stereoMode = C.STEREO_MODE_MONO;
+ break;
+ case 1:
+ currentTrack.stereoMode = C.STEREO_MODE_LEFT_RIGHT;
+ break;
+ case 3:
+ currentTrack.stereoMode = C.STEREO_MODE_TOP_BOTTOM;
+ break;
+ case 15:
+ currentTrack.stereoMode = C.STEREO_MODE_STEREO_MESH;
+ break;
+ default:
+ break;
+ }
+ break;
+ case ID_COLOUR_PRIMARIES:
+ currentTrack.hasColorInfo = true;
+ switch ((int) value) {
+ case 1:
+ currentTrack.colorSpace = C.COLOR_SPACE_BT709;
+ break;
+ case 4: // BT.470M.
+ case 5: // BT.470BG.
+ case 6: // SMPTE 170M.
+ case 7: // SMPTE 240M.
+ currentTrack.colorSpace = C.COLOR_SPACE_BT601;
+ break;
+ case 9:
+ currentTrack.colorSpace = C.COLOR_SPACE_BT2020;
+ break;
+ default:
+ break;
+ }
+ break;
+ case ID_COLOUR_TRANSFER:
+ switch ((int) value) {
+ case 1: // BT.709.
+ case 6: // SMPTE 170M.
+ case 7: // SMPTE 240M.
+ currentTrack.colorTransfer = C.COLOR_TRANSFER_SDR;
+ break;
+ case 16:
+ currentTrack.colorTransfer = C.COLOR_TRANSFER_ST2084;
+ break;
+ case 18:
+ currentTrack.colorTransfer = C.COLOR_TRANSFER_HLG;
+ break;
+ default:
+ break;
+ }
+ break;
+ case ID_COLOUR_RANGE:
+ switch((int) value) {
+ case 1: // Broadcast range.
+ currentTrack.colorRange = C.COLOR_RANGE_LIMITED;
+ break;
+ case 2:
+ currentTrack.colorRange = C.COLOR_RANGE_FULL;
+ break;
+ default:
+ break;
+ }
+ break;
+ case ID_MAX_CLL:
+ currentTrack.maxContentLuminance = (int) value;
+ break;
+ 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;
+ }
+ }
+
+ /**
+ * 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;
+ break;
+ case ID_SAMPLING_FREQUENCY:
+ currentTrack.sampleRate = (int) value;
+ break;
+ case ID_PRIMARY_R_CHROMATICITY_X:
+ currentTrack.primaryRChromaticityX = (float) value;
+ break;
+ case ID_PRIMARY_R_CHROMATICITY_Y:
+ currentTrack.primaryRChromaticityY = (float) value;
+ break;
+ case ID_PRIMARY_G_CHROMATICITY_X:
+ currentTrack.primaryGChromaticityX = (float) value;
+ break;
+ case ID_PRIMARY_G_CHROMATICITY_Y:
+ currentTrack.primaryGChromaticityY = (float) value;
+ break;
+ case ID_PRIMARY_B_CHROMATICITY_X:
+ currentTrack.primaryBChromaticityX = (float) value;
+ break;
+ case ID_PRIMARY_B_CHROMATICITY_Y:
+ currentTrack.primaryBChromaticityY = (float) value;
+ break;
+ case ID_WHITE_POINT_CHROMATICITY_X:
+ currentTrack.whitePointChromaticityX = (float) value;
+ break;
+ case ID_WHITE_POINT_CHROMATICITY_Y:
+ currentTrack.whitePointChromaticityY = (float) value;
+ break;
+ case ID_LUMNINANCE_MAX:
+ currentTrack.maxMasteringLuminance = (float) value;
+ break;
+ 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;
+ }
+ }
+
+ /**
+ * 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.
+ if (!DOC_TYPE_WEBM.equals(value) && !DOC_TYPE_MATROSKA.equals(value)) {
+ throw new ParserException("DocType " + value + " not supported");
+ }
+ break;
+ case ID_NAME:
+ currentTrack.name = value;
+ break;
+ case ID_CODEC_ID:
+ currentTrack.codecId = value;
+ break;
+ case ID_LANGUAGE:
+ currentTrack.language = value;
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * 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:
+ Arrays.fill(seekEntryIdBytes.data, (byte) 0);
+ input.readFully(seekEntryIdBytes.data, 4 - contentSize, contentSize);
+ seekEntryIdBytes.setPosition(0);
+ seekEntryId = (int) seekEntryIdBytes.readUnsignedInt();
+ break;
+ case ID_CODEC_PRIVATE:
+ currentTrack.codecPrivate = new byte[contentSize];
+ input.readFully(currentTrack.codecPrivate, 0, contentSize);
+ break;
+ case ID_PROJECTION_PRIVATE:
+ currentTrack.projectionData = new byte[contentSize];
+ input.readFully(currentTrack.projectionData, 0, contentSize);
+ break;
+ case ID_CONTENT_COMPRESSION_SETTINGS:
+ // This extractor only supports header stripping, so the payload is the stripped bytes.
+ currentTrack.sampleStrippedBytes = new byte[contentSize];
+ input.readFully(currentTrack.sampleStrippedBytes, 0, contentSize);
+ break;
+ case ID_CONTENT_ENCRYPTION_KEY_ID:
+ 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:
+ // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
+ // and http://matroska.org/technical/specs/index.html#block_structure
+ // for info about how data is organized in SimpleBlock and Block elements respectively. They
+ // differ only in the way flags are specified.
+
+ if (blockState == BLOCK_STATE_START) {
+ blockTrackNumber = (int) varintReader.readUnsignedVarint(input, false, true, 8);
+ blockTrackNumberLength = varintReader.getLastLength();
+ blockDurationUs = C.TIME_UNSET;
+ blockState = BLOCK_STATE_HEADER;
+ scratch.reset();
+ }
+
+ Track track = tracks.get(blockTrackNumber);
+
+ // Ignore the block if we don't know about the track to which it belongs.
+ if (track == null) {
+ input.skipFully(contentSize - blockTrackNumberLength);
+ blockState = BLOCK_STATE_START;
+ return;
+ }
+
+ if (blockState == BLOCK_STATE_HEADER) {
+ // Read the relative timecode (2 bytes) and flags (1 byte).
+ readScratch(input, 3);
+ int lacing = (scratch.data[2] & 0x06) >> 1;
+ if (lacing == LACING_NONE) {
+ blockSampleCount = 1;
+ blockSampleSizes = ensureArrayCapacity(blockSampleSizes, 1);
+ blockSampleSizes[0] = contentSize - blockTrackNumberLength - 3;
+ } else {
+ // Read the sample count (1 byte).
+ readScratch(input, 4);
+ blockSampleCount = (scratch.data[3] & 0xFF) + 1;
+ blockSampleSizes = ensureArrayCapacity(blockSampleSizes, blockSampleCount);
+ if (lacing == LACING_FIXED_SIZE) {
+ int 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 < blockSampleCount - 1; sampleIndex++) {
+ blockSampleSizes[sampleIndex] = 0;
+ int byteValue;
+ do {
+ readScratch(input, ++headerSize);
+ byteValue = scratch.data[headerSize - 1] & 0xFF;
+ blockSampleSizes[sampleIndex] += byteValue;
+ } while (byteValue == 0xFF);
+ totalSamplesSize += blockSampleSizes[sampleIndex];
+ }
+ blockSampleSizes[blockSampleCount - 1] =
+ contentSize - blockTrackNumberLength - headerSize - totalSamplesSize;
+ } else if (lacing == LACING_EBML) {
+ int totalSamplesSize = 0;
+ int headerSize = 4;
+ 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");
+ }
+ long readValue = 0;
+ for (int i = 0; i < 8; i++) {
+ int lengthMask = 1 << (7 - i);
+ if ((scratch.data[headerSize - 1] & lengthMask) != 0) {
+ int readPosition = headerSize - 1;
+ headerSize += i;
+ readScratch(input, headerSize);
+ readValue = (scratch.data[readPosition++] & 0xFF) & ~lengthMask;
+ while (readPosition < headerSize) {
+ readValue <<= 8;
+ readValue |= (scratch.data[readPosition++] & 0xFF);
+ }
+ // The first read value is the first size. Later values are signed offsets.
+ if (sampleIndex > 0) {
+ readValue -= (1L << (6 + i * 7)) - 1;
+ }
+ break;
+ }
+ }
+ if (readValue < Integer.MIN_VALUE || readValue > Integer.MAX_VALUE) {
+ throw new ParserException("EBML lacing sample size out of range.");
+ }
+ int intReadValue = (int) readValue;
+ blockSampleSizes[sampleIndex] =
+ sampleIndex == 0
+ ? intReadValue
+ : blockSampleSizes[sampleIndex - 1] + intReadValue;
+ totalSamplesSize += blockSampleSizes[sampleIndex];
+ }
+ blockSampleSizes[blockSampleCount - 1] =
+ contentSize - blockTrackNumberLength - headerSize - totalSamplesSize;
+ } else {
+ // Lacing is always in the range 0--3.
+ throw new ParserException("Unexpected lacing value: " + lacing);
+ }
+ }
+
+ int timecode = (scratch.data[0] << 8) | (scratch.data[1] & 0xFF);
+ blockTimeUs = clusterTimecodeUs + scaleTimecodeToUs(timecode);
+ boolean isInvisible = (scratch.data[2] & 0x08) == 0x08;
+ boolean isKeyframe = track.type == TRACK_TYPE_AUDIO
+ || (id == ID_SIMPLE_BLOCK && (scratch.data[2] & 0x80) == 0x80);
+ blockFlags = (isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0)
+ | (isInvisible ? C.BUFFER_FLAG_DECODE_ONLY : 0);
+ blockState = BLOCK_STATE_DATA;
+ blockSampleIndex = 0;
+ }
+
+ if (id == ID_SIMPLE_BLOCK) {
+ // 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 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);
+ }
+ }
+
+ 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);
+ }
+ }
+
+ 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;
+ }
+
+ /**
+ * Ensures {@link #scratch} contains at least {@code requiredLength} bytes of data, reading from
+ * the extractor input if necessary.
+ */
+ private void readScratch(ExtractorInput input, int requiredLength)
+ throws IOException, InterruptedException {
+ if (scratch.limit() >= requiredLength) {
+ return;
+ }
+ if (scratch.capacity() < requiredLength) {
+ scratch.reset(Arrays.copyOf(scratch.data, Math.max(scratch.data.length * 2, requiredLength)),
+ scratch.limit());
+ }
+ input.readFully(scratch.data, scratch.limit(), requiredLength - scratch.limit());
+ scratch.setLimit(requiredLength);
+ }
+
+ /**
+ * 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)) {
+ 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;
+ if (!sampleEncodingHandled) {
+ if (track.hasContentEncryption) {
+ // If the sample is encrypted, read its encryption signal byte and set the IV size.
+ // Clear the encrypted flag.
+ blockFlags &= ~C.BUFFER_FLAG_ENCRYPTED;
+ if (!sampleSignalByteRead) {
+ input.readFully(scratch.data, 0, 1);
+ sampleBytesRead++;
+ if ((scratch.data[0] & 0x80) == 0x80) {
+ throw new ParserException("Extension bit is set in signal byte");
+ }
+ sampleSignalByte = scratch.data[0];
+ sampleSignalByteRead = true;
+ }
+ boolean isEncrypted = (sampleSignalByte & 0x01) == 0x01;
+ if (isEncrypted) {
+ boolean hasSubsampleEncryption = (sampleSignalByte & 0x02) == 0x02;
+ blockFlags |= C.BUFFER_FLAG_ENCRYPTED;
+ if (!sampleInitializationVectorRead) {
+ input.readFully(encryptionInitializationVector.data, 0, ENCRYPTION_IV_SIZE);
+ sampleBytesRead += ENCRYPTION_IV_SIZE;
+ sampleInitializationVectorRead = true;
+ // Write the signal byte, containing the IV size and the subsample encryption flag.
+ scratch.data[0] = (byte) (ENCRYPTION_IV_SIZE | (hasSubsampleEncryption ? 0x80 : 0x00));
+ scratch.setPosition(0);
+ output.sampleData(scratch, 1);
+ sampleBytesWritten++;
+ // Write the IV.
+ encryptionInitializationVector.setPosition(0);
+ output.sampleData(encryptionInitializationVector, ENCRYPTION_IV_SIZE);
+ sampleBytesWritten += ENCRYPTION_IV_SIZE;
+ }
+ if (hasSubsampleEncryption) {
+ if (!samplePartitionCountRead) {
+ input.readFully(scratch.data, 0, 1);
+ sampleBytesRead++;
+ scratch.setPosition(0);
+ samplePartitionCount = scratch.readUnsignedByte();
+ samplePartitionCountRead = true;
+ }
+ int samplePartitionDataSize = samplePartitionCount * 4;
+ scratch.reset(samplePartitionDataSize);
+ input.readFully(scratch.data, 0, samplePartitionDataSize);
+ sampleBytesRead += samplePartitionDataSize;
+ short subsampleCount = (short) (1 + (samplePartitionCount / 2));
+ int subsampleDataSize = 2 + 6 * subsampleCount;
+ if (encryptionSubsampleDataBuffer == null
+ || encryptionSubsampleDataBuffer.capacity() < subsampleDataSize) {
+ encryptionSubsampleDataBuffer = ByteBuffer.allocate(subsampleDataSize);
+ }
+ encryptionSubsampleDataBuffer.position(0);
+ encryptionSubsampleDataBuffer.putShort(subsampleCount);
+ // Loop through the partition offsets and write out the data in the way ExoPlayer
+ // wants it (ISO 23001-7 Part 7):
+ // 2 bytes - sub sample count.
+ // for each sub sample:
+ // 2 bytes - clear data size.
+ // 4 bytes - encrypted data size.
+ int partitionOffset = 0;
+ for (int i = 0; i < samplePartitionCount; i++) {
+ int previousPartitionOffset = partitionOffset;
+ partitionOffset = scratch.readUnsignedIntToInt();
+ if ((i % 2) == 0) {
+ encryptionSubsampleDataBuffer.putShort(
+ (short) (partitionOffset - previousPartitionOffset));
+ } else {
+ encryptionSubsampleDataBuffer.putInt(partitionOffset - previousPartitionOffset);
+ }
+ }
+ int finalPartitionSize = size - sampleBytesRead - partitionOffset;
+ if ((samplePartitionCount % 2) == 1) {
+ encryptionSubsampleDataBuffer.putInt(finalPartitionSize);
+ } else {
+ encryptionSubsampleDataBuffer.putShort((short) finalPartitionSize);
+ encryptionSubsampleDataBuffer.putInt(0);
+ }
+ encryptionSubsampleData.reset(encryptionSubsampleDataBuffer.array(), subsampleDataSize);
+ output.sampleData(encryptionSubsampleData, subsampleDataSize);
+ sampleBytesWritten += subsampleDataSize;
+ }
+ }
+ } else if (track.sampleStrippedBytes != null) {
+ // 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();
+
+ if (CODEC_ID_H264.equals(track.codecId) || CODEC_ID_H265.equals(track.codecId)) {
+ // 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.
+ byte[] nalLengthData = nalLength.data;
+ nalLengthData[0] = 0;
+ nalLengthData[1] = 0;
+ nalLengthData[2] = 0;
+ int nalUnitLengthFieldLength = track.nalUnitLengthFieldLength;
+ int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength;
+ // NAL units are length delimited, but the decoder requires start code delimited units.
+ // Loop until we've written the sample to the track output, replacing length delimiters with
+ // start codes as we encounter them.
+ while (sampleBytesRead < size) {
+ if (sampleCurrentNalBytesRemaining == 0) {
+ // Read the NAL length so that we know where we find the next one.
+ writeToTarget(
+ input, nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);
+ sampleBytesRead += nalUnitLengthFieldLength;
+ nalLength.setPosition(0);
+ sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();
+ // Write a start code for the current NAL unit.
+ nalStartCode.setPosition(0);
+ output.sampleData(nalStartCode, 4);
+ sampleBytesWritten += 4;
+ } else {
+ // Write the payload of the NAL unit.
+ 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) {
+ int bytesWritten = writeToOutput(input, output, size - sampleBytesRead);
+ sampleBytesRead += bytesWritten;
+ sampleBytesWritten += bytesWritten;
+ }
+ }
+
+ if (CODEC_ID_VORBIS.equals(track.codecId)) {
+ // Vorbis decoder in android MediaCodec [1] expects the last 4 bytes of the sample to be the
+ // number of samples in the current page. This definition holds good only for Ogg and
+ // irrelevant for Matroska. So we always set this to -1 (the decoder will ignore this value if
+ // we set it to -1). The android platform media extractor [2] does the same.
+ // [1] https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/codecs/vorbis/dec/SoftVorbis.cpp#314
+ // [2] https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/NuMediaExtractor.cpp#474
+ vorbisNumPageSamples.setPosition(0);
+ output.sampleData(vorbisNumPageSamples, 4);
+ sampleBytesWritten += 4;
+ }
+
+ return finishWriteSampleData();
+ }
+
+ /**
+ * 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;
+ }
+
+ /** 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 {
+ System.arraycopy(samplePrefix, 0, subtitleSample.data, 0, samplePrefix.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}.
+ *
+ * <p>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 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);
+ }
+ }
+
+ /**
+ * 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 writeToOutput(ExtractorInput input, TrackOutput output, int length)
+ throws IOException, InterruptedException {
+ int bytesWritten;
+ int strippedBytesLeft = sampleStrippedBytes.bytesLeft();
+ if (strippedBytesLeft > 0) {
+ bytesWritten = Math.min(length, strippedBytesLeft);
+ output.sampleData(sampleStrippedBytes, bytesWritten);
+ } else {
+ bytesWritten = output.sampleData(input, length, false);
+ }
+ return bytesWritten;
+ }
+
+ /**
+ * Builds a {@link SeekMap} from the recently gathered Cues information.
+ *
+ * @return The built {@link SeekMap}. The returned {@link SeekMap} may be unseekable if cues
+ * information was missing or incomplete.
+ */
+ private SeekMap buildSeekMap() {
+ if (segmentContentPosition == C.POSITION_UNSET || durationUs == C.TIME_UNSET
+ || cueTimesUs == null || cueTimesUs.size() == 0
+ || cueClusterPositions == null || cueClusterPositions.size() != cueTimesUs.size()) {
+ // Cues information is missing or incomplete.
+ cueTimesUs = null;
+ cueClusterPositions = null;
+ return new SeekMap.Unseekable(durationUs);
+ }
+ int cuePointsSize = cueTimesUs.size();
+ int[] sizes = new int[cuePointsSize];
+ long[] offsets = new long[cuePointsSize];
+ long[] durationsUs = new long[cuePointsSize];
+ long[] timesUs = new long[cuePointsSize];
+ for (int i = 0; i < cuePointsSize; i++) {
+ timesUs[i] = cueTimesUs.get(i);
+ offsets[i] = segmentContentPosition + cueClusterPositions.get(i);
+ }
+ for (int i = 0; i < cuePointsSize - 1; i++) {
+ sizes[i] = (int) (offsets[i + 1] - offsets[i]);
+ durationsUs[i] = timesUs[i + 1] - timesUs[i];
+ }
+ 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);
+ }
+
+ /**
+ * Updates the position of the holder to Cues element's position if the extractor configuration
+ * permits use of master seek entry. After building Cues sets the holder's position back to where
+ * it was before.
+ *
+ * @param seekPosition The holder whose position will be updated.
+ * @param currentPosition Current position of the input.
+ * @return Whether the seek position was updated.
+ */
+ private boolean maybeSeekForCues(PositionHolder seekPosition, long currentPosition) {
+ if (seekForCues) {
+ seekPositionAfterBuildingCues = currentPosition;
+ seekPosition.position = cuesContentPosition;
+ seekForCues = false;
+ return true;
+ }
+ // After parsing Cues, seek back to original position if available. We will not do this unless
+ // we seeked to get to the Cues in the first place.
+ if (sentSeekMap && seekPositionAfterBuildingCues != C.POSITION_UNSET) {
+ seekPosition.position = seekPositionAfterBuildingCues;
+ seekPositionAfterBuildingCues = C.POSITION_UNSET;
+ return true;
+ }
+ return false;
+ }
+
+ private long scaleTimecodeToUs(long unscaledTimecode) throws ParserException {
+ if (timecodeScale == C.TIME_UNSET) {
+ throw new ParserException("Can't scale timecode prior to timecodeScale being set.");
+ }
+ return Util.scaleLargeTimestamp(unscaledTimecode, timecodeScale, 1000);
+ }
+
+ 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)
+ || CODEC_ID_MPEG4_AP.equals(codecId)
+ || CODEC_ID_H264.equals(codecId)
+ || CODEC_ID_H265.equals(codecId)
+ || CODEC_ID_FOURCC.equals(codecId)
+ || CODEC_ID_THEORA.equals(codecId)
+ || CODEC_ID_OPUS.equals(codecId)
+ || CODEC_ID_VORBIS.equals(codecId)
+ || CODEC_ID_AAC.equals(codecId)
+ || CODEC_ID_MP2.equals(codecId)
+ || CODEC_ID_MP3.equals(codecId)
+ || CODEC_ID_AC3.equals(codecId)
+ || CODEC_ID_E_AC3.equals(codecId)
+ || CODEC_ID_TRUEHD.equals(codecId)
+ || CODEC_ID_DTS.equals(codecId)
+ || CODEC_ID_DTS_EXPRESS.equals(codecId)
+ || CODEC_ID_DTS_LOSSLESS.equals(codecId)
+ || CODEC_ID_FLAC.equals(codecId)
+ || 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);
+ }
+
+ /**
+ * Returns an array that can store (at least) {@code length} elements, which will be either a new
+ * array or {@code array} if it's not null and large enough.
+ */
+ private static int[] ensureArrayCapacity(int[] array, int length) {
+ if (array == null) {
+ return new int[length];
+ } else if (array.length >= length) {
+ return array;
+ } else {
+ // Double the size to avoid allocating constantly if the required length increases gradually.
+ return new int[Math.max(array.length * 2, length)];
+ }
+ }
+
+ /** 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);
+ }
+
+ @Override
+ public boolean isLevel1Element(int id) {
+ return MatroskaExtractor.this.isLevel1Element(id);
+ }
+
+ @Override
+ public void startMasterElement(int id, long contentPosition, long contentSize)
+ throws ParserException {
+ MatroskaExtractor.this.startMasterElement(id, contentPosition, contentSize);
+ }
+
+ @Override
+ public void endMasterElement(int id) throws ParserException {
+ MatroskaExtractor.this.endMasterElement(id);
+ }
+
+ @Override
+ public void integerElement(int id, long value) throws ParserException {
+ MatroskaExtractor.this.integerElement(id, value);
+ }
+
+ @Override
+ public void floatElement(int id, double value) throws ParserException {
+ MatroskaExtractor.this.floatElement(id, value);
+ }
+
+ @Override
+ public void stringElement(int id, String value) throws ParserException {
+ MatroskaExtractor.this.stringElement(id, value);
+ }
+
+ @Override
+ public void binaryElement(int id, int contentsSize, ExtractorInput input)
+ 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 {
+
+ private static final int DISPLAY_UNIT_PIXELS = 0;
+ private static final int MAX_CHROMATICITY = 50000; // Defined in CTA-861.3.
+ /**
+ * Default max content light level (CLL) that should be encoded into hdrStaticInfo.
+ */
+ private static final int DEFAULT_MAX_CLL = 1000; // nits.
+
+ /**
+ * Default frame-average light level (FALL) that should be encoded into hdrStaticInfo.
+ */
+ 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 TrackOutput.CryptoData cryptoData;
+ public byte[] codecPrivate;
+ public DrmInitData drmInitData;
+
+ // Video elements.
+ public int width = Format.NO_VALUE;
+ public int height = Format.NO_VALUE;
+ 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;
+ public boolean hasColorInfo = false;
+ @C.ColorSpace
+ public int colorSpace = Format.NO_VALUE;
+ @C.ColorTransfer
+ public int colorTransfer = Format.NO_VALUE;
+ @C.ColorRange
+ public int colorRange = Format.NO_VALUE;
+ public int maxContentLuminance = DEFAULT_MAX_CLL;
+ public int maxFrameAverageLuminance = DEFAULT_MAX_FALL;
+ public float primaryRChromaticityX = Format.NO_VALUE;
+ public float primaryRChromaticityY = Format.NO_VALUE;
+ public float primaryGChromaticityX = Format.NO_VALUE;
+ public float primaryGChromaticityY = Format.NO_VALUE;
+ public float primaryBChromaticityX = Format.NO_VALUE;
+ public float primaryBChromaticityY = Format.NO_VALUE;
+ public float whitePointChromaticityX = Format.NO_VALUE;
+ public float whitePointChromaticityY = Format.NO_VALUE;
+ public float maxMasteringLuminance = Format.NO_VALUE;
+ public float minMasteringLuminance = Format.NO_VALUE;
+
+ // Audio elements. Initially set to their default values.
+ public int channelCount = 1;
+ public int audioBitDepth = Format.NO_VALUE;
+ public int sampleRate = 8000;
+ public long codecDelayNs = 0;
+ public long seekPreRollNs = 0;
+ @Nullable public TrueHdSampleRechunker trueHdSampleRechunker;
+
+ // Text elements.
+ public boolean flagForced;
+ public boolean flagDefault = true;
+ private String language = "eng";
+
+ // Set when the output is initialized. nalUnitLengthFieldLength is only set for H264/H265.
+ public TrackOutput output;
+ public int nalUnitLengthFieldLength;
+
+ /** Initializes the track with an output. */
+ public void initializeOutput(ExtractorOutput output, int trackId) throws ParserException {
+ String mimeType;
+ int maxInputSize = Format.NO_VALUE;
+ @C.PcmEncoding int pcmEncoding = Format.NO_VALUE;
+ List<byte[]> initializationData = null;
+ switch (codecId) {
+ case CODEC_ID_VP8:
+ mimeType = MimeTypes.VIDEO_VP8;
+ break;
+ 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;
+ case CODEC_ID_MPEG4_SP:
+ case CODEC_ID_MPEG4_ASP:
+ case CODEC_ID_MPEG4_AP:
+ mimeType = MimeTypes.VIDEO_MP4V;
+ initializationData =
+ codecPrivate == null ? null : Collections.singletonList(codecPrivate);
+ break;
+ case CODEC_ID_H264:
+ mimeType = MimeTypes.VIDEO_H264;
+ AvcConfig avcConfig = AvcConfig.parse(new ParsableByteArray(codecPrivate));
+ initializationData = avcConfig.initializationData;
+ nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength;
+ break;
+ case CODEC_ID_H265:
+ mimeType = MimeTypes.VIDEO_H265;
+ HevcConfig hevcConfig = HevcConfig.parse(new ParsableByteArray(codecPrivate));
+ initializationData = hevcConfig.initializationData;
+ nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength;
+ break;
+ case CODEC_ID_FOURCC:
+ Pair<String, List<byte[]>> 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
+ // should be set to for this case.
+ mimeType = MimeTypes.VIDEO_UNKNOWN;
+ break;
+ case CODEC_ID_VORBIS:
+ mimeType = MimeTypes.AUDIO_VORBIS;
+ maxInputSize = VORBIS_MAX_INPUT_SIZE;
+ initializationData = parseVorbisCodecPrivate(codecPrivate);
+ break;
+ case CODEC_ID_OPUS:
+ mimeType = MimeTypes.AUDIO_OPUS;
+ maxInputSize = OPUS_MAX_INPUT_SIZE;
+ initializationData = new ArrayList<>(3);
+ initializationData.add(codecPrivate);
+ initializationData.add(
+ ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(codecDelayNs).array());
+ initializationData.add(
+ ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(seekPreRollNs).array());
+ break;
+ case CODEC_ID_AAC:
+ mimeType = MimeTypes.AUDIO_AAC;
+ initializationData = Collections.singletonList(codecPrivate);
+ break;
+ case CODEC_ID_MP2:
+ mimeType = MimeTypes.AUDIO_MPEG_L2;
+ maxInputSize = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;
+ break;
+ case CODEC_ID_MP3:
+ mimeType = MimeTypes.AUDIO_MPEG;
+ maxInputSize = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;
+ break;
+ case CODEC_ID_AC3:
+ mimeType = MimeTypes.AUDIO_AC3;
+ break;
+ case CODEC_ID_E_AC3:
+ mimeType = MimeTypes.AUDIO_E_AC3;
+ break;
+ case CODEC_ID_TRUEHD:
+ mimeType = MimeTypes.AUDIO_TRUEHD;
+ trueHdSampleRechunker = new TrueHdSampleRechunker();
+ break;
+ case CODEC_ID_DTS:
+ case CODEC_ID_DTS_EXPRESS:
+ mimeType = MimeTypes.AUDIO_DTS;
+ break;
+ case CODEC_ID_DTS_LOSSLESS:
+ mimeType = MimeTypes.AUDIO_DTS_HD;
+ break;
+ case CODEC_ID_FLAC:
+ mimeType = MimeTypes.AUDIO_FLAC;
+ initializationData = Collections.singletonList(codecPrivate);
+ break;
+ case CODEC_ID_ACM:
+ mimeType = MimeTypes.AUDIO_RAW;
+ 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) {
+ 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);
+ break;
+ case CODEC_ID_PGS:
+ mimeType = MimeTypes.APPLICATION_PGS;
+ break;
+ case CODEC_ID_DVBSUB:
+ mimeType = MimeTypes.APPLICATION_DVBSUBS;
+ // Init data: composition_page (2), ancillary_page (2)
+ initializationData = Collections.singletonList(new byte[] {codecPrivate[0],
+ codecPrivate[1], codecPrivate[2], codecPrivate[3]});
+ break;
+ default:
+ throw new ParserException("Unrecognized codec identifier.");
+ }
+
+ int type;
+ Format format;
+ @C.SelectionFlags int selectionFlags = 0;
+ selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0;
+ selectionFlags |= flagForced ? C.SELECTION_FLAG_FORCED : 0;
+ // TODO: Consider reading the name elements of the tracks and, if present, incorporating them
+ // into the trackId passed when creating the formats.
+ if (MimeTypes.isAudio(mimeType)) {
+ type = C.TRACK_TYPE_AUDIO;
+ format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
+ Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding,
+ initializationData, drmInitData, selectionFlags, language);
+ } else if (MimeTypes.isVideo(mimeType)) {
+ type = C.TRACK_TYPE_VIDEO;
+ if (displayUnit == Track.DISPLAY_UNIT_PIXELS) {
+ displayWidth = displayWidth == Format.NO_VALUE ? width : displayWidth;
+ displayHeight = displayHeight == Format.NO_VALUE ? height : displayHeight;
+ }
+ float pixelWidthHeightRatio = Format.NO_VALUE;
+ if (displayWidth != Format.NO_VALUE && displayHeight != Format.NO_VALUE) {
+ pixelWidthHeightRatio = ((float) (height * displayWidth)) / (width * displayHeight);
+ }
+ ColorInfo colorInfo = null;
+ if (hasColorInfo) {
+ byte[] hdrStaticInfo = getHdrStaticInfo();
+ colorInfo = new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo);
+ }
+ 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, 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,
+ selectionFlags,
+ initializationData,
+ language,
+ drmInitData);
+ } else {
+ throw new ParserException("Unexpected MIME type.");
+ }
+
+ this.output = output.track(number, type);
+ this.output.format(format);
+ }
+
+ /** 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
+ || primaryGChromaticityX == Format.NO_VALUE || primaryGChromaticityY == Format.NO_VALUE
+ || primaryBChromaticityX == Format.NO_VALUE || primaryBChromaticityY == Format.NO_VALUE
+ || whitePointChromaticityX == Format.NO_VALUE
+ || whitePointChromaticityY == Format.NO_VALUE || maxMasteringLuminance == Format.NO_VALUE
+ || minMasteringLuminance == Format.NO_VALUE) {
+ return null;
+ }
+
+ byte[] hdrStaticInfoData = new byte[25];
+ 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));
+ hdrStaticInfo.putShort((short) ((primaryGChromaticityX * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryGChromaticityY * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryBChromaticityX * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryBChromaticityY * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((whitePointChromaticityX * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((whitePointChromaticityY * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) (maxMasteringLuminance + 0.5f));
+ hdrStaticInfo.putShort((short) (minMasteringLuminance + 0.5f));
+ hdrStaticInfo.putShort((short) maxContentLuminance);
+ hdrStaticInfo.putShort((short) maxFrameAverageLuminance);
+ return hdrStaticInfoData;
+ }
+
+ /**
+ * Builds initialization data for a {@link Format} from FourCC codec private data.
+ *
+ * @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 Pair<String, List<byte[]>> 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_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");
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ 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);
+ }
+
+ /**
+ * Builds initialization data for a {@link Format} from Vorbis codec private data.
+ *
+ * @return The initialization data for the {@link Format}.
+ * @throws ParserException If the initialization data could not be built.
+ */
+ private static List<byte[]> parseVorbisCodecPrivate(byte[] codecPrivate)
+ throws ParserException {
+ try {
+ if (codecPrivate[0] != 0x02) {
+ throw new ParserException("Error parsing vorbis codec private");
+ }
+ int offset = 1;
+ int vorbisInfoLength = 0;
+ while (codecPrivate[offset] == (byte) 0xFF) {
+ vorbisInfoLength += 0xFF;
+ offset++;
+ }
+ vorbisInfoLength += codecPrivate[offset++];
+
+ int vorbisSkipLength = 0;
+ while (codecPrivate[offset] == (byte) 0xFF) {
+ vorbisSkipLength += 0xFF;
+ offset++;
+ }
+ vorbisSkipLength += codecPrivate[offset++];
+
+ if (codecPrivate[offset] != 0x01) {
+ throw new ParserException("Error parsing vorbis codec private");
+ }
+ byte[] vorbisInfo = new byte[vorbisInfoLength];
+ System.arraycopy(codecPrivate, offset, vorbisInfo, 0, vorbisInfoLength);
+ offset += vorbisInfoLength;
+ if (codecPrivate[offset] != 0x03) {
+ throw new ParserException("Error parsing vorbis codec private");
+ }
+ offset += vorbisSkipLength;
+ if (codecPrivate[offset] != 0x05) {
+ throw new ParserException("Error parsing vorbis codec private");
+ }
+ byte[] vorbisBooks = new byte[codecPrivate.length - offset];
+ System.arraycopy(codecPrivate, offset, vorbisBooks, 0, codecPrivate.length - offset);
+ List<byte[]> initializationData = new ArrayList<>(2);
+ initializationData.add(vorbisInfo);
+ initializationData.add(vorbisBooks);
+ return initializationData;
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new ParserException("Error parsing vorbis codec private");
+ }
+ }
+
+ /**
+ * Parses an MS/ACM codec private, returning whether it indicates PCM audio.
+ *
+ * @return Whether the codec private indicates PCM audio.
+ * @throws ParserException If a parsing error occurs.
+ */
+ private static boolean parseMsAcmCodecPrivate(ParsableByteArray buffer) throws ParserException {
+ try {
+ int formatTag = buffer.readLittleEndianUnsignedShort();
+ if (formatTag == WAVE_FORMAT_PCM) {
+ return true;
+ } else if (formatTag == WAVE_FORMAT_EXTENSIBLE) {
+ buffer.setPosition(WAVE_FORMAT_SIZE + 6); // unionSamples(2), channelMask(4)
+ return buffer.readLong() == WAVE_SUBFORMAT_PCM.getMostSignificantBits()
+ && buffer.readLong() == WAVE_SUBFORMAT_PCM.getLeastSignificantBits();
+ } else {
+ return false;
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new ParserException("Error parsing MS/ACM codec private");
+ }
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.java
new file mode 100644
index 0000000000..f84cd084a3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.java
@@ -0,0 +1,114 @@
+/*
+ * 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.mkv;
+
+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 java.io.IOException;
+
+/**
+ * Utility class that peeks from the input stream in order to determine whether it appears to be
+ * compatible input for this extractor.
+ */
+/* package */ final class Sniffer {
+
+ /**
+ * The number of bytes to search for a valid header in {@link #sniff(ExtractorInput)}.
+ */
+ private static final int SEARCH_LENGTH = 1024;
+ private static final int ID_EBML = 0x1A45DFA3;
+
+ private final ParsableByteArray scratch;
+ private int peekLength;
+
+ public Sniffer() {
+ scratch = new ParsableByteArray(8);
+ }
+
+ /**
+ * @see com.google.android.exoplayer2.extractor.Extractor#sniff(ExtractorInput)
+ */
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ long inputLength = input.getLength();
+ int bytesToSearch = (int) (inputLength == C.LENGTH_UNSET || inputLength > SEARCH_LENGTH
+ ? SEARCH_LENGTH : inputLength);
+ // Find four bytes equal to ID_EBML near the start of the input.
+ input.peekFully(scratch.data, 0, 4);
+ long tag = scratch.readUnsignedInt();
+ peekLength = 4;
+ while (tag != ID_EBML) {
+ if (++peekLength == bytesToSearch) {
+ return false;
+ }
+ input.peekFully(scratch.data, 0, 1);
+ tag = (tag << 8) & 0xFFFFFF00;
+ tag |= scratch.data[0] & 0xFF;
+ }
+
+ // Read the size of the EBML header and make sure it is within the stream.
+ long headerSize = readUint(input);
+ long headerStart = peekLength;
+ if (headerSize == Long.MIN_VALUE
+ || (inputLength != C.LENGTH_UNSET && headerStart + headerSize >= inputLength)) {
+ return false;
+ }
+
+ // Read the payload elements in the EBML header.
+ while (peekLength < headerStart + headerSize) {
+ long id = readUint(input);
+ if (id == Long.MIN_VALUE) {
+ return false;
+ }
+ long size = readUint(input);
+ if (size < 0 || size > Integer.MAX_VALUE) {
+ return false;
+ }
+ if (size != 0) {
+ int sizeInt = (int) size;
+ input.advancePeekPosition(sizeInt);
+ peekLength += sizeInt;
+ }
+ }
+ return peekLength == headerStart + headerSize;
+ }
+
+ /**
+ * Peeks a variable-length unsigned EBML integer from the input.
+ */
+ private long readUint(ExtractorInput input) throws IOException, InterruptedException {
+ input.peekFully(scratch.data, 0, 1);
+ int value = scratch.data[0] & 0xFF;
+ if (value == 0) {
+ return Long.MIN_VALUE;
+ }
+ int mask = 0x80;
+ int length = 0;
+ while ((value & mask) == 0) {
+ mask >>= 1;
+ length++;
+ }
+ value &= ~mask;
+ input.peekFully(scratch.data, 1, length);
+ for (int i = 0; i < length; i++) {
+ value <<= 8;
+ value += scratch.data[i + 1] & 0xFF;
+ }
+ peekLength += length + 1;
+ return value;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/VarintReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/VarintReader.java
new file mode 100644
index 0000000000..8a8d572ea5
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mkv/VarintReader.java
@@ -0,0 +1,155 @@
+/*
+ * 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.mkv;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Reads EBML variable-length integers (varints) from an {@link ExtractorInput}.
+ */
+/* package */ final class VarintReader {
+
+ private static final int STATE_BEGIN_READING = 0;
+ private static final int STATE_READ_CONTENTS = 1;
+
+ /**
+ * The first byte of a variable-length integer (varint) will have one of these bit masks
+ * indicating the total length in bytes.
+ *
+ * <p>{@code 0x80} is a one-byte integer, {@code 0x40} is two bytes, and so on up to eight bytes.
+ */
+ private static final long[] VARINT_LENGTH_MASKS = new long[] {
+ 0x80L, 0x40L, 0x20L, 0x10L, 0x08L, 0x04L, 0x02L, 0x01L
+ };
+
+ private final byte[] scratch;
+
+ private int state;
+ private int length;
+
+ public VarintReader() {
+ scratch = new byte[8];
+ }
+
+ /**
+ * Resets the reader to start reading a new variable-length integer.
+ */
+ public void reset() {
+ state = STATE_BEGIN_READING;
+ length = 0;
+ }
+
+ /**
+ * Reads an EBML variable-length integer (varint) from an {@link ExtractorInput} such that
+ * reading can be resumed later if an error occurs having read only some of it.
+ * <p>
+ * If an value is successfully read, then the reader will automatically reset itself ready to
+ * read another value.
+ * <p>
+ * If an {@link IOException} or {@link InterruptedException} is throw, the read can be resumed
+ * later by calling this method again, passing an {@link ExtractorInput} providing data starting
+ * where the previous one left off.
+ *
+ * @param input The {@link ExtractorInput} from which the integer should be read.
+ * @param allowEndOfInput True if encountering the end of the input having read no data is
+ * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it
+ * should be considered an error, causing an {@link EOFException} to be thrown.
+ * @param removeLengthMask Removes the variable-length integer length mask from the value.
+ * @param maximumAllowedLength Maximum allowed length of the variable integer to be read.
+ * @return The read value, or {@link C#RESULT_END_OF_INPUT} if {@code allowEndOfStream} is true
+ * and the end of the input was encountered, or {@link C#RESULT_MAX_LENGTH_EXCEEDED} if the
+ * length of the varint exceeded maximumAllowedLength.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ public long readUnsignedVarint(ExtractorInput input, boolean allowEndOfInput,
+ boolean removeLengthMask, int maximumAllowedLength) throws IOException, InterruptedException {
+ if (state == STATE_BEGIN_READING) {
+ // Read the first byte to establish the length.
+ if (!input.readFully(scratch, 0, 1, allowEndOfInput)) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ int firstByte = scratch[0] & 0xFF;
+ length = parseUnsignedVarintLength(firstByte);
+ if (length == C.LENGTH_UNSET) {
+ throw new IllegalStateException("No valid varint length mask found");
+ }
+ state = STATE_READ_CONTENTS;
+ }
+
+ if (length > maximumAllowedLength) {
+ state = STATE_BEGIN_READING;
+ return C.RESULT_MAX_LENGTH_EXCEEDED;
+ }
+
+ if (length != 1) {
+ // Read the remaining bytes.
+ input.readFully(scratch, 1, length - 1);
+ }
+
+ state = STATE_BEGIN_READING;
+ return assembleVarint(scratch, length, removeLengthMask);
+ }
+
+ /**
+ * Returns the number of bytes occupied by the most recently parsed varint.
+ */
+ public int getLastLength() {
+ return length;
+ }
+
+ /**
+ * Parses and the length of the varint given the first byte.
+ *
+ * @param firstByte First byte of the varint.
+ * @return Length of the varint beginning with the given byte if it was valid,
+ * {@link C#LENGTH_UNSET} otherwise.
+ */
+ public static int parseUnsignedVarintLength(int firstByte) {
+ int varIntLength = C.LENGTH_UNSET;
+ for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {
+ if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {
+ varIntLength = i + 1;
+ break;
+ }
+ }
+ return varIntLength;
+ }
+
+ /**
+ * Assemble a varint from the given byte array.
+ *
+ * @param varintBytes Bytes that make up the varint.
+ * @param varintLength Length of the varint to assemble.
+ * @param removeLengthMask Removes the variable-length integer length mask from the value.
+ * @return Parsed and assembled varint.
+ */
+ public static long assembleVarint(byte[] varintBytes, int varintLength,
+ boolean removeLengthMask) {
+ long varint = varintBytes[0] & 0xFFL;
+ if (removeLengthMask) {
+ varint &= ~VARINT_LENGTH_MASKS[varintLength - 1];
+ }
+ for (int i = 1; i < varintLength; i++) {
+ varint = (varint << 8) | (varintBytes[i] & 0xFFL);
+ }
+ return varint;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java
new file mode 100644
index 0000000000..1a442110e3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java
@@ -0,0 +1,46 @@
+/*
+ * 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.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 extends ConstantBitrateSeekMap implements Seeker {
+
+ /**
+ * @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 getTimeUsAtPosition(position);
+ }
+
+ @Override
+ public long getDataEndPosition() {
+ return C.POSITION_UNSET;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java
new file mode 100644
index 0000000000..662ded4ec3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<Long, Long> 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<Long, Long> 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<Long, Long> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
new file mode 100644
index 0000000000..2829a1e519
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
@@ -0,0 +1,482 @@
+/*
+ * 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.mp3;
+
+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.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.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.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 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 the MP3 container format.
+ */
+public final class Mp3Extractor implements Extractor {
+
+ /** Factory for {@link Mp3Extractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp3Extractor()};
+
+ /**
+ * 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})
+ 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;
+ /**
+ * 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 = 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.
+ */
+ private static final int MAX_SYNC_BYTES = 128 * 1024;
+ /**
+ * The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up.
+ */
+ private static final int MAX_SNIFF_BYTES = 16 * 1024;
+ /**
+ * Maximum length of data read into {@link #scratch}.
+ */
+ private static final int SCRATCH_LENGTH = 10;
+
+ /**
+ * Mask that includes the audio header values that must match between frames.
+ */
+ 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;
+ private TrackOutput trackOutput;
+
+ private int synchronizedHeaderData;
+
+ private Metadata metadata;
+ @Nullable private Seeker seeker;
+ private boolean disableSeeking;
+ private long basisTimeUs;
+ private long samplesRead;
+ private long firstSamplePosition;
+ private int sampleBytesRemaining;
+
+ public Mp3Extractor() {
+ this(0);
+ }
+
+ /**
+ * @param flags Flags that control the extractor's behavior.
+ */
+ public Mp3Extractor(@Flags int flags) {
+ this(flags, C.TIME_UNSET);
+ }
+
+ /**
+ * @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.
+ */
+ public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) {
+ this.flags = flags;
+ this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
+ scratch = new ParsableByteArray(SCRATCH_LENGTH);
+ 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);
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ extractorOutput = output;
+ trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO);
+ extractorOutput.endTracks();
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ synchronizedHeaderData = 0;
+ basisTimeUs = C.TIME_UNSET;
+ samplesRead = 0;
+ sampleBytesRemaining = 0;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ if (synchronizedHeaderData == 0) {
+ try {
+ synchronize(input, false);
+ } catch (EOFException e) {
+ return RESULT_END_OF_INPUT;
+ }
+ }
+ if (seeker == null) {
+ // 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(
+ /* 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.
+ *
+ * <p>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 (peekEndOfStreamOrHeader(extractorInput)) {
+ return RESULT_END_OF_INPUT;
+ }
+ scratch.setPosition(0);
+ int sampleHeaderData = scratch.readInt();
+ 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);
+ synchronizedHeaderData = 0;
+ return RESULT_CONTINUE;
+ }
+ MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader);
+ if (basisTimeUs == C.TIME_UNSET) {
+ basisTimeUs = seeker.getTimeUs(extractorInput.getPosition());
+ if (forcedFirstSampleTimestampUs != C.TIME_UNSET) {
+ long embeddedFirstSampleTimestampUs = seeker.getTimeUs(0);
+ basisTimeUs += forcedFirstSampleTimestampUs - embeddedFirstSampleTimestampUs;
+ }
+ }
+ sampleBytesRemaining = synchronizedHeader.frameSize;
+ }
+ int bytesAppended = trackOutput.sampleData(extractorInput, sampleBytesRemaining, true);
+ if (bytesAppended == C.RESULT_END_OF_INPUT) {
+ return RESULT_END_OF_INPUT;
+ }
+ sampleBytesRemaining -= bytesAppended;
+ if (sampleBytesRemaining > 0) {
+ return RESULT_CONTINUE;
+ }
+ long timeUs = basisTimeUs + (samplesRead * C.MICROS_PER_SECOND / synchronizedHeader.sampleRate);
+ trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, synchronizedHeader.frameSize, 0,
+ null);
+ samplesRead += synchronizedHeader.samplesPerFrame;
+ sampleBytesRemaining = 0;
+ return RESULT_CONTINUE;
+ }
+
+ private boolean synchronize(ExtractorInput input, boolean sniffing)
+ throws IOException, InterruptedException {
+ int validFrameCount = 0;
+ int candidateSynchronizedHeaderData = 0;
+ int peekedId3Bytes = 0;
+ int searchedBytes = 0;
+ int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES;
+ input.resetPeekPosition();
+ if (input.getPosition() == 0) {
+ // 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 (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
+ && !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) {
+ if (!sniffing) {
+ throw new ParserException("Searched too many bytes.");
+ }
+ return false;
+ }
+ validFrameCount = 0;
+ candidateSynchronizedHeaderData = 0;
+ if (sniffing) {
+ input.resetPeekPosition();
+ input.advancePeekPosition(peekedId3Bytes + searchedBytes);
+ } else {
+ input.skipFully(1);
+ }
+ } else {
+ // The header matches the candidate header and/or is valid.
+ validFrameCount++;
+ if (validFrameCount == 1) {
+ MpegAudioHeader.populateHeader(headerData, synchronizedHeader);
+ candidateSynchronizedHeaderData = headerData;
+ } else if (validFrameCount == 4) {
+ break;
+ }
+ input.advancePeekPosition(frameSize - 4);
+ }
+ }
+ // Prepare to read the synchronized frame.
+ if (sniffing) {
+ input.skipFully(peekedId3Bytes + searchedBytes);
+ } else {
+ input.resetPeekPosition();
+ }
+ synchronizedHeaderData = candidateSynchronizedHeaderData;
+ return true;
+ }
+
+ /**
+ * 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 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;
+ }
+ }
+ try {
+ return !extractorInput.peekFully(
+ scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true);
+ } catch (EOFException e) {
+ return true;
+ }
+ }
+
+ /**
+ * 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.
+ */
+ private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException, InterruptedException {
+ ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize);
+ input.peekFully(frame.data, 0, synchronizedHeader.frameSize);
+ int xingBase = (synchronizedHeader.version & 1) != 0
+ ? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1
+ : (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5
+ 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();
+ input.advancePeekPosition(xingBase + 141);
+ input.peekFully(scratch.data, 0, 3);
+ scratch.setPosition(0);
+ gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24());
+ }
+ 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);
+ }
+ } 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();
+ }
+ return seeker;
+ }
+
+ /**
+ * Peeks the next frame and returns a {@link ConstantBitrateSeeker} based on its bitrate.
+ */
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Seeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/Seeker.java
new file mode 100644
index 0000000000..da0306cc60
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java
new file mode 100644
index 0000000000..8bb142f496
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java
@@ -0,0 +1,136 @@
+/*
+ * 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.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 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.
+ * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required
+ * information is not present.
+ */
+ public static @Nullable VbriSeeker create(
+ long inputLength, long position, MpegAudioHeader mpegAudioHeader, ParsableByteArray frame) {
+ frame.skipBytes(10);
+ int numFrames = frame.readInt();
+ if (numFrames <= 0) {
+ return null;
+ }
+ int sampleRate = mpegAudioHeader.sampleRate;
+ long durationUs = Util.scaleLargeTimestamp(numFrames,
+ C.MICROS_PER_SECOND * (sampleRate >= 32000 ? 1152 : 576), sampleRate);
+ int entryCount = frame.readUnsignedShort();
+ int scale = frame.readUnsignedShort();
+ int entrySize = frame.readUnsignedShort();
+ frame.skipBytes(2);
+
+ long minPosition = position + mpegAudioHeader.frameSize;
+ // Read table of contents entries.
+ 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:
+ segmentSize = frame.readUnsignedByte();
+ break;
+ case 2:
+ segmentSize = frame.readUnsignedShort();
+ break;
+ case 3:
+ segmentSize = frame.readUnsignedInt24();
+ break;
+ case 4:
+ segmentSize = frame.readUnsignedIntToInt();
+ break;
+ default:
+ return null;
+ }
+ position += segmentSize * scale;
+ }
+ 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, long dataEndPosition) {
+ this.timesUs = timesUs;
+ this.positions = positions;
+ this.durationUs = durationUs;
+ this.dataEndPosition = dataEndPosition;
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ 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
+ public long getTimeUs(long position) {
+ return timesUs[Util.binarySearchFloor(positions, position, true, true)];
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ public long getDataEndPosition() {
+ return dataEndPosition;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java
new file mode 100644
index 0000000000..61568aac93
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java
@@ -0,0 +1,188 @@
+/*
+ * 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.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 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.
+ * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required
+ * information is not present.
+ */
+ public static @Nullable XingSeeker create(
+ long inputLength, long position, MpegAudioHeader mpegAudioHeader, ParsableByteArray frame) {
+ int samplesPerFrame = mpegAudioHeader.samplesPerFrame;
+ int sampleRate = mpegAudioHeader.sampleRate;
+
+ int flags = frame.readInt();
+ int frameCount;
+ if ((flags & 0x01) != 0x01 || (frameCount = frame.readUnsignedIntToInt()) == 0) {
+ // If the frame count is missing/invalid, the header can't be used to determine the duration.
+ return null;
+ }
+ long durationUs = Util.scaleLargeTimestamp(frameCount, samplesPerFrame * C.MICROS_PER_SECOND,
+ sampleRate);
+ if ((flags & 0x06) != 0x06) {
+ // If the size in bytes or table of contents is missing, the stream is not seekable.
+ return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs);
+ }
+
+ 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();
+
+ 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 dataStartPosition;
+ private final int xingFrameSize;
+ private final long durationUs;
+ /** Data size, including the XING frame. */
+ private final long dataSize;
+
+ 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 dataStartPosition,
+ int xingFrameSize,
+ long durationUs,
+ long dataSize,
+ @Nullable long[] tableOfContents) {
+ this.dataStartPosition = dataStartPosition;
+ this.xingFrameSize = xingFrameSize;
+ this.durationUs = durationUs;
+ this.tableOfContents = tableOfContents;
+ this.dataSize = dataSize;
+ dataEndPosition = dataSize == C.LENGTH_UNSET ? C.POSITION_UNSET : dataStartPosition + dataSize;
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return tableOfContents != null;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ if (!isSeekable()) {
+ return new SeekPoints(new SeekPoint(0, dataStartPosition + xingFrameSize));
+ }
+ 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 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 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) {
+ long positionOffset = position - dataStartPosition;
+ if (!isSeekable() || positionOffset <= xingFrameSize) {
+ return 0L;
+ }
+ 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
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ public long getDataEndPosition() {
+ return dataEndPosition;
+ }
+
+ /**
+ * 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 getTimeUsForTableIndex(int tableIndex) {
+ return (durationUs * tableIndex) / 100;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java
new file mode 100644
index 0000000000..56f0eab1cd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java
@@ -0,0 +1,558 @@
+/*
+ * 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.mp4;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@SuppressWarnings("ConstantField")
+/* package */ abstract class Atom {
+
+ /**
+ * Size of an atom header, in bytes.
+ */
+ public static final int HEADER_SIZE = 8;
+
+ /**
+ * Size of a full atom header, in bytes.
+ */
+ public static final int FULL_HEADER_SIZE = 12;
+
+ /**
+ * Size of a long atom header, in bytes.
+ */
+ public static final int LONG_HEADER_SIZE = 16;
+
+ /**
+ * Value for the size field in an atom that defines its size in the largesize field.
+ */
+ public static final int DEFINES_LARGE_SIZE = 1;
+
+ /**
+ * 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;
+
+ public Atom(int type) {
+ this.type = type;
+ }
+
+ @Override
+ public String toString() {
+ return getAtomTypeString(type);
+ }
+
+ /**
+ * An MP4 atom that is a leaf.
+ */
+ /* package */ static final class LeafAtom extends Atom {
+
+ /**
+ * The atom data.
+ */
+ public final ParsableByteArray data;
+
+ /**
+ * @param type The type of the atom.
+ * @param data The atom data.
+ */
+ public LeafAtom(int type, ParsableByteArray data) {
+ super(type);
+ this.data = data;
+ }
+
+ }
+
+ /**
+ * An MP4 atom that has child atoms.
+ */
+ /* package */ static final class ContainerAtom extends Atom {
+
+ public final long endPosition;
+ public final List<LeafAtom> leafChildren;
+ public final List<ContainerAtom> containerChildren;
+
+ /**
+ * @param type The type of the atom.
+ * @param endPosition The position of the first byte after the end of the atom.
+ */
+ public ContainerAtom(int type, long endPosition) {
+ super(type);
+ this.endPosition = endPosition;
+ leafChildren = new ArrayList<>();
+ containerChildren = new ArrayList<>();
+ }
+
+ /**
+ * Adds a child leaf to this container.
+ *
+ * @param atom The child to add.
+ */
+ public void add(LeafAtom atom) {
+ leafChildren.add(atom);
+ }
+
+ /**
+ * Adds a child container to this container.
+ *
+ * @param atom The child to add.
+ */
+ public void add(ContainerAtom atom) {
+ containerChildren.add(atom);
+ }
+
+ /**
+ * Returns the child leaf of the given type.
+ *
+ * <p>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++) {
+ LeafAtom atom = leafChildren.get(i);
+ if (atom.type == type) {
+ return atom;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the child container of the given type.
+ *
+ * <p>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++) {
+ ContainerAtom atom = containerChildren.get(i);
+ if (atom.type == type) {
+ return atom;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the total number of leaf/container children of this atom with the given type.
+ *
+ * @param type The type of child atoms to count.
+ * @return The total number of leaf/container children of this atom with the given type.
+ */
+ public int getChildAtomOfTypeCount(int type) {
+ int count = 0;
+ int size = leafChildren.size();
+ for (int i = 0; i < size; i++) {
+ LeafAtom atom = leafChildren.get(i);
+ if (atom.type == type) {
+ count++;
+ }
+ }
+ size = containerChildren.size();
+ for (int i = 0; i < size; i++) {
+ ContainerAtom atom = containerChildren.get(i);
+ if (atom.type == type) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public String toString() {
+ return getAtomTypeString(type)
+ + " leaves: " + Arrays.toString(leafChildren.toArray())
+ + " containers: " + Arrays.toString(containerChildren.toArray());
+ }
+
+ }
+
+ /**
+ * Parses the version number out of the additional integer component of a full atom.
+ */
+ public static int parseFullAtomVersion(int fullAtomInt) {
+ return 0x000000FF & (fullAtomInt >> 24);
+ }
+
+ /**
+ * Parses the atom flags out of the additional integer component of a full atom.
+ */
+ public static int parseFullAtomFlags(int fullAtomInt) {
+ return 0x00FFFFFF & fullAtomInt;
+ }
+
+ /**
+ * Converts a numeric atom type to the corresponding four character string.
+ *
+ * @param type The numeric atom type.
+ * @return The corresponding four character string.
+ */
+ public static String getAtomTypeString(int type) {
+ return "" + (char) ((type >> 24) & 0xFF)
+ + (char) ((type >> 16) & 0xFF)
+ + (char) ((type >> 8) & 0xFF)
+ + (char) (type & 0xFF);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
new file mode 100644
index 0000000000..93ee2d6810
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
@@ -0,0 +1,1607 @@
+/*
+ * 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.mp4;
+
+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. */
+@SuppressWarnings({"ConstantField"})
+/* package */ final class AtomParsers {
+
+ private static final String TAG = "AtomParsers";
+
+ @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).
+ *
+ * @param trak Atom to decode.
+ * @param mvhd Movie header atom, used to get the timescale.
+ * @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 ignoreEditLists, boolean isQuickTime)
+ throws ParserException {
+ Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
+ int trackType = getTrackTypeForHdlr(parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data));
+ if (trackType == C.TRACK_TYPE_UNKNOWN) {
+ return null;
+ }
+
+ TkhdData tkhdData = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data);
+ if (duration == C.TIME_UNSET) {
+ duration = tkhdData.duration;
+ }
+ long movieTimescale = parseMvhd(mvhd.data);
+ long durationUs;
+ if (duration == C.TIME_UNSET) {
+ durationUs = C.TIME_UNSET;
+ } else {
+ durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, movieTimescale);
+ }
+ Atom.ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf)
+ .getContainerAtomOfType(Atom.TYPE_stbl);
+
+ Pair<Long, String> 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);
+ long[] editListDurations = null;
+ long[] editListMediaTimes = null;
+ if (!ignoreEditLists) {
+ Pair<long[], long[]> 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, editListDurations, editListMediaTimes);
+ }
+
+ /**
+ * Parses an stbl atom (defined in 14496-12).
+ *
+ * @param track Track to which this sample table corresponds.
+ * @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 Thrown if the stbl atom can't be parsed.
+ */
+ 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) {
+ sampleSizeBox = new StszSampleSizeBox(stszAtom);
+ } else {
+ Atom.LeafAtom stz2Atom = stblAtom.getLeafAtomOfType(Atom.TYPE_stz2);
+ if (stz2Atom == null) {
+ throw new ParserException("Track has no sample table size information");
+ }
+ sampleSizeBox = new Stz2SampleSizeBox(stz2Atom);
+ }
+
+ int sampleCount = sampleSizeBox.getSampleCount();
+ if (sampleCount == 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.
+ boolean chunkOffsetsAreLongs = false;
+ Atom.LeafAtom chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stco);
+ if (chunkOffsetsAtom == null) {
+ chunkOffsetsAreLongs = true;
+ chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_co64);
+ }
+ ParsableByteArray chunkOffsets = chunkOffsetsAtom.data;
+ // Entries are (chunk number, number of samples per chunk, sample description index).
+ ParsableByteArray stsc = stblAtom.getLeafAtomOfType(Atom.TYPE_stsc).data;
+ // Entries are (number of samples, timestamp delta between those samples).
+ ParsableByteArray stts = stblAtom.getLeafAtomOfType(Atom.TYPE_stts).data;
+ // Entries are the indices of samples that are synchronization samples.
+ Atom.LeafAtom stssAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stss);
+ ParsableByteArray stss = stssAtom != null ? stssAtom.data : null;
+ // Entries are (number of samples, timestamp offset).
+ Atom.LeafAtom cttsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_ctts);
+ ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null;
+
+ // Prepare to read chunk information.
+ ChunkIterator chunkIterator = new ChunkIterator(stsc, chunkOffsets, chunkOffsetsAreLongs);
+
+ // Prepare to read sample timestamps.
+ stts.setPosition(Atom.FULL_HEADER_SIZE);
+ int remainingTimestampDeltaChanges = stts.readUnsignedIntToInt() - 1;
+ int remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
+ int timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();
+
+ // Prepare to read sample timestamp offsets, if ctts is present.
+ int remainingSamplesAtTimestampOffset = 0;
+ int remainingTimestampOffsetChanges = 0;
+ int timestampOffset = 0;
+ if (ctts != null) {
+ ctts.setPosition(Atom.FULL_HEADER_SIZE);
+ remainingTimestampOffsetChanges = ctts.readUnsignedIntToInt();
+ }
+
+ int nextSynchronizationSampleIndex = C.INDEX_UNSET;
+ int remainingSynchronizationSamples = 0;
+ if (stss != null) {
+ stss.setPosition(Atom.FULL_HEADER_SIZE);
+ remainingSynchronizationSamples = stss.readUnsignedIntToInt();
+ if (remainingSynchronizationSamples > 0) {
+ nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
+ } else {
+ // Ignore empty stss boxes, which causes all samples to be treated as sync samples.
+ stss = null;
+ }
+ }
+
+ // 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;
+ int maximumSize = 0;
+ long[] timestamps;
+ int[] flags;
+ long timestampTimeUnits = 0;
+ long duration;
+
+ if (!isFixedSampleSizeRawAudio) {
+ offsets = new long[sampleCount];
+ sizes = new int[sampleCount];
+ timestamps = new long[sampleCount];
+ flags = new int[sampleCount];
+ long offset = 0;
+ int remainingSamplesInChunk = 0;
+
+ for (int i = 0; i < sampleCount; i++) {
+ // Advance to the next chunk if necessary.
+ 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) {
+ while (remainingSamplesAtTimestampOffset == 0 && remainingTimestampOffsetChanges > 0) {
+ remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt();
+ // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers
+ // in version 0 ctts boxes, however some streams violate the spec and use signed
+ // integers instead. It's safe to always decode sample offsets 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 offsets are always small).
+ timestampOffset = ctts.readInt();
+ remainingTimestampOffsetChanges--;
+ }
+ remainingSamplesAtTimestampOffset--;
+ }
+
+ offsets[i] = offset;
+ sizes[i] = sampleSizeBox.readNextSampleSize();
+ if (sizes[i] > maximumSize) {
+ maximumSize = sizes[i];
+ }
+ timestamps[i] = timestampTimeUnits + timestampOffset;
+
+ // All samples are synchronization samples if the stss is not present.
+ flags[i] = stss == null ? C.BUFFER_FLAG_KEY_FRAME : 0;
+ if (i == nextSynchronizationSampleIndex) {
+ flags[i] = C.BUFFER_FLAG_KEY_FRAME;
+ remainingSynchronizationSamples--;
+ if (remainingSynchronizationSamples > 0) {
+ nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
+ }
+ }
+
+ // Add on the duration of this sample.
+ timestampTimeUnits += timestampDeltaInTimeUnits;
+ remainingSamplesAtTimestampDelta--;
+ if (remainingSamplesAtTimestampDelta == 0 && remainingTimestampDeltaChanges > 0) {
+ remainingSamplesAtTimestampDelta = 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--;
+ }
+ duration = timestampTimeUnits + timestampOffset;
+
+ // If the stbl's child boxes are not consistent the container is malformed, but the stream may
+ // still be playable.
+ 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];
+ int[] chunkSampleCounts = new int[chunkIterator.length];
+ while (chunkIterator.moveNext()) {
+ chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset;
+ chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples;
+ }
+ int fixedSampleSize =
+ Util.getPcmFrameSize(track.format.pcmEncoding, track.format.channelCount);
+ FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk(
+ fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits);
+ offsets = rechunkedResults.offsets;
+ sizes = rechunkedResults.sizes;
+ 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) {
+ Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
+ 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 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
+ && timestamps.length >= 2) {
+ long editStartTime = track.editListMediaTimes[0];
+ long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0],
+ track.timescale, track.movieTimescale);
+ 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,
+ track.format.sampleRate, track.timescale);
+ if ((encoderDelay != 0 || encoderPadding != 0) && encoderDelay <= Integer.MAX_VALUE
+ && encoderPadding <= Integer.MAX_VALUE) {
+ gaplessInfoHolder.encoderDelay = (int) encoderDelay;
+ gaplessInfoHolder.encoderPadding = (int) encoderPadding;
+ Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
+ long editedDurationUs =
+ Util.scaleLargeTimestamp(
+ track.editListDurations[0], C.MICROS_PER_SECOND, track.movieTimescale);
+ return new TrackSampleTable(
+ track, offsets, sizes, maximumSize, timestamps, flags, editedDurationUs);
+ }
+ }
+ }
+
+ if (track.editListDurations.length == 1 && track.editListDurations[0] == 0) {
+ // 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] - editStartTime, C.MICROS_PER_SECOND, track.timescale);
+ }
+ 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.
+ boolean omitClippedSample = track.type == C.TRACK_TYPE_AUDIO;
+
+ // Count the number of samples after applying edits.
+ 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 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;
+
+ // Calculate edited sample timestamps and update the corresponding metadata arrays.
+ long[] editedOffsets = copyMetadata ? new long[editedSampleCount] : offsets;
+ int[] editedSizes = copyMetadata ? new int[editedSampleCount] : sizes;
+ int editedMaximumSize = copyMetadata ? 0 : maximumSize;
+ int[] editedFlags = copyMetadata ? new int[editedSampleCount] : flags;
+ long[] editedTimestamps = new long[editedSampleCount];
+ long pts = 0;
+ int sampleIndex = 0;
+ for (int i = 0; i < track.editListDurations.length; i++) {
+ 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);
+ }
+ 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];
+ }
+ long editedDurationUs =
+ Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
+ return new TrackSampleTable(
+ track,
+ editedOffsets,
+ editedSizes,
+ editedMaximumSize,
+ editedTimestamps,
+ editedFlags,
+ editedDurationUs);
+ }
+
+ /**
+ * Parses a udta atom.
+ *
+ * @param udtaAtom The udta (user data) atom to decode.
+ * @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
+ // decode one.
+ return null;
+ }
+ ParsableByteArray udtaData = udtaAtom.data;
+ udtaData.setPosition(Atom.HEADER_SIZE);
+ while (udtaData.bytesLeft() >= Atom.HEADER_SIZE) {
+ int atomPosition = udtaData.getPosition();
+ int atomSize = udtaData.readInt();
+ int atomType = udtaData.readInt();
+ if (atomType == Atom.TYPE_meta) {
+ udtaData.setPosition(atomPosition);
+ return parseUdtaMeta(udtaData, atomPosition + atomSize);
+ }
+ udtaData.setPosition(atomPosition + atomSize);
+ }
+ return null;
+ }
+
+ /**
+ * 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<Metadata.Entry> 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();
+ int atomSize = meta.readInt();
+ int atomType = meta.readInt();
+ if (atomType == Atom.TYPE_ilst) {
+ meta.setPosition(atomPosition);
+ return parseIlst(meta, atomPosition + atomSize);
+ }
+ meta.setPosition(atomPosition + atomSize);
+ }
+ return null;
+ }
+
+ @Nullable
+ private static Metadata parseIlst(ParsableByteArray ilst, int limit) {
+ ilst.skipBytes(Atom.HEADER_SIZE);
+ ArrayList<Metadata.Entry> entries = new ArrayList<>();
+ while (ilst.getPosition() < limit) {
+ Metadata.Entry entry = MetadataUtil.parseIlstElement(ilst);
+ if (entry != null) {
+ entries.add(entry);
+ }
+ }
+ return entries.isEmpty() ? null : new Metadata(entries);
+ }
+
+ /**
+ * Parses a mvhd atom (defined in 14496-12), returning the timescale for the movie.
+ *
+ * @param mvhd Contents of the mvhd atom to be parsed.
+ * @return Timescale for the movie.
+ */
+ private static long parseMvhd(ParsableByteArray mvhd) {
+ mvhd.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = mvhd.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ mvhd.skipBytes(version == 0 ? 8 : 16);
+ return mvhd.readUnsignedInt();
+ }
+
+ /**
+ * Parses a tkhd atom (defined in 14496-12).
+ *
+ * @return An object containing the parsed data.
+ */
+ private static TkhdData parseTkhd(ParsableByteArray tkhd) {
+ tkhd.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = tkhd.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+
+ tkhd.skipBytes(version == 0 ? 8 : 16);
+ int trackId = tkhd.readInt();
+
+ tkhd.skipBytes(4);
+ boolean durationUnknown = true;
+ int durationPosition = tkhd.getPosition();
+ int durationByteCount = version == 0 ? 4 : 8;
+ for (int i = 0; i < durationByteCount; i++) {
+ if (tkhd.data[durationPosition + i] != -1) {
+ durationUnknown = false;
+ break;
+ }
+ }
+ long duration;
+ if (durationUnknown) {
+ tkhd.skipBytes(durationByteCount);
+ duration = C.TIME_UNSET;
+ } else {
+ duration = version == 0 ? tkhd.readUnsignedInt() : tkhd.readUnsignedLongToLong();
+ if (duration == 0) {
+ // 0 duration normally indicates that the file is fully fragmented (i.e. all of the media
+ // samples are in fragments). Treat as unknown.
+ duration = C.TIME_UNSET;
+ }
+ }
+
+ tkhd.skipBytes(16);
+ int a00 = tkhd.readInt();
+ int a01 = tkhd.readInt();
+ tkhd.skipBytes(4);
+ int a10 = tkhd.readInt();
+ int a11 = tkhd.readInt();
+
+ int rotationDegrees;
+ int fixedOne = 65536;
+ if (a00 == 0 && a01 == fixedOne && a10 == -fixedOne && a11 == 0) {
+ rotationDegrees = 90;
+ } else if (a00 == 0 && a01 == -fixedOne && a10 == fixedOne && a11 == 0) {
+ rotationDegrees = 270;
+ } else if (a00 == -fixedOne && a01 == 0 && a10 == 0 && a11 == -fixedOne) {
+ rotationDegrees = 180;
+ } else {
+ // Only 0, 90, 180 and 270 are supported. Treat anything else as 0.
+ rotationDegrees = 0;
+ }
+
+ return new TkhdData(trackId, duration, rotationDegrees);
+ }
+
+ /**
+ * Parses an hdlr atom.
+ *
+ * @param hdlr The hdlr atom to decode.
+ * @return The handler value.
+ */
+ private static int parseHdlr(ParsableByteArray hdlr) {
+ hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4);
+ 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 (hdlr == TYPE_vide) {
+ return C.TRACK_TYPE_VIDEO;
+ } else if (hdlr == TYPE_text || hdlr == TYPE_sbtl || hdlr == TYPE_subt || hdlr == TYPE_clcp) {
+ return C.TRACK_TYPE_TEXT;
+ } else if (hdlr == TYPE_meta) {
+ return C.TRACK_TYPE_METADATA;
+ } else {
+ return C.TRACK_TYPE_UNKNOWN;
+ }
+ }
+
+ /**
+ * Parses an mdhd atom (defined in 14496-12).
+ *
+ * @param mdhd The mdhd atom to decode.
+ * @return A pair consisting of the media timescale defined as the number of time units that pass
+ * in one second, and the language code.
+ */
+ private static Pair<Long, String> parseMdhd(ParsableByteArray mdhd) {
+ mdhd.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = mdhd.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ mdhd.skipBytes(version == 0 ? 8 : 16);
+ 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);
+ return Pair.create(timescale, language);
+ }
+
+ /**
+ * Parses a stsd atom (defined in 14496-12).
+ *
+ * @param stsd The stsd atom to decode.
+ * @param trackId The track's identifier in its container.
+ * @param rotationDegrees The rotation of the track in degrees.
+ * @param language The language of the track.
+ * @param drmInitData {@link DrmInitData} to be included in the format.
+ * @param isQuickTime True for QuickTime media. False otherwise.
+ * @return An object containing the parsed data.
+ */
+ private static StsdData parseStsd(ParsableByteArray stsd, int trackId, int rotationDegrees,
+ String language, DrmInitData drmInitData, boolean isQuickTime) throws ParserException {
+ stsd.setPosition(Atom.FULL_HEADER_SIZE);
+ int numberOfEntries = stsd.readInt();
+ StsdData out = new StsdData(numberOfEntries);
+ for (int i = 0; i < numberOfEntries; i++) {
+ int childStartPosition = stsd.getPosition();
+ 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
+ || 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_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, out);
+ } else if (childAtomType == Atom.TYPE_camm) {
+ out.format = Format.createSampleFormat(Integer.toString(trackId),
+ MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, null);
+ }
+ stsd.setPosition(childStartPosition + childAtomSize);
+ }
+ return out;
+ }
+
+ private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position,
+ int atomSize, int trackId, String language, StsdData out) throws ParserException {
+ parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
+
+ // Default values.
+ List<byte[]> initializationData = null;
+ long subsampleOffsetUs = Format.OFFSET_SAMPLE_RELATIVE;
+
+ String mimeType;
+ if (atomType == Atom.TYPE_TTML) {
+ mimeType = MimeTypes.APPLICATION_TTML;
+ } else if (atomType == Atom.TYPE_tx3g) {
+ mimeType = MimeTypes.APPLICATION_TX3G;
+ int sampleDescriptionLength = atomSize - Atom.HEADER_SIZE - 8;
+ byte[] sampleDescriptionData = new byte[sampleDescriptionLength];
+ parent.readBytes(sampleDescriptionData, 0, sampleDescriptionLength);
+ initializationData = Collections.singletonList(sampleDescriptionData);
+ } else if (atomType == Atom.TYPE_wvtt) {
+ mimeType = MimeTypes.APPLICATION_MP4VTT;
+ } else if (atomType == Atom.TYPE_stpp) {
+ mimeType = MimeTypes.APPLICATION_TTML;
+ subsampleOffsetUs = 0; // Subsample timing is absolute.
+ } else if (atomType == Atom.TYPE_c608) {
+ // Defined by the QuickTime File Format specification.
+ mimeType = MimeTypes.APPLICATION_MP4CEA608;
+ out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT;
+ } else {
+ // Never happens.
+ throw new IllegalStateException();
+ }
+
+ 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,
+ int size, int trackId, int rotationDegrees, DrmInitData drmInitData, StsdData out,
+ int entryIndex) throws ParserException {
+ parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
+
+ parent.skipBytes(16);
+ int width = parent.readUnsignedShort();
+ int height = parent.readUnsignedShort();
+ boolean pixelWidthHeightRatioFromPasp = false;
+ float pixelWidthHeightRatio = 1;
+ parent.skipBytes(50);
+
+ int childPosition = parent.getPosition();
+ if (atomType == Atom.TYPE_encv) {
+ Pair<Integer, TrackEncryptionBox> 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<byte[]> initializationData = null;
+ String mimeType = null;
+ String codecs = null;
+ byte[] projectionData = null;
+ @C.StereoMode
+ int stereoMode = Format.NO_VALUE;
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childStartPosition = parent.getPosition();
+ int childAtomSize = parent.readInt();
+ if (childAtomSize == 0 && parent.getPosition() - position == size) {
+ // Handle optional terminating four zero bytes in MOV files.
+ break;
+ }
+ Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_avcC) {
+ Assertions.checkState(mimeType == null);
+ mimeType = MimeTypes.VIDEO_H264;
+ parent.setPosition(childStartPosition + Atom.HEADER_SIZE);
+ AvcConfig avcConfig = AvcConfig.parse(parent);
+ initializationData = avcConfig.initializationData;
+ out.nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength;
+ if (!pixelWidthHeightRatioFromPasp) {
+ pixelWidthHeightRatio = avcConfig.pixelWidthAspectRatio;
+ }
+ } else if (childAtomType == Atom.TYPE_hvcC) {
+ Assertions.checkState(mimeType == null);
+ mimeType = MimeTypes.VIDEO_H265;
+ parent.setPosition(childStartPosition + Atom.HEADER_SIZE);
+ 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;
+ } else if (childAtomType == Atom.TYPE_esds) {
+ Assertions.checkState(mimeType == null);
+ Pair<String, byte[]> mimeTypeAndInitializationData =
+ parseEsdsFromParent(parent, childStartPosition);
+ mimeType = mimeTypeAndInitializationData.first;
+ initializationData = Collections.singletonList(mimeTypeAndInitializationData.second);
+ } else if (childAtomType == Atom.TYPE_pasp) {
+ pixelWidthHeightRatio = parsePaspFromParent(parent, childStartPosition);
+ pixelWidthHeightRatioFromPasp = true;
+ } else if (childAtomType == Atom.TYPE_sv3d) {
+ projectionData = parseProjFromParent(parent, childStartPosition, childAtomSize);
+ } else if (childAtomType == Atom.TYPE_st3d) {
+ int version = parent.readUnsignedByte();
+ parent.skipBytes(3); // Flags.
+ if (version == 0) {
+ int layout = parent.readUnsignedByte();
+ switch (layout) {
+ case 0:
+ stereoMode = C.STEREO_MODE_MONO;
+ break;
+ case 1:
+ stereoMode = C.STEREO_MODE_TOP_BOTTOM;
+ break;
+ case 2:
+ stereoMode = C.STEREO_MODE_LEFT_RIGHT;
+ break;
+ case 3:
+ stereoMode = C.STEREO_MODE_STEREO_MESH;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ childPosition += childAtomSize;
+ }
+
+ // If the media type was not recognized, ignore the track.
+ if (mimeType == null) {
+ return;
+ }
+
+ 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);
+ }
+
+ /**
+ * Parses the edts atom (defined in 14496-12 subsection 8.6.5).
+ *
+ * @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.
+ */
+ private static Pair<long[], long[]> parseEdts(Atom.ContainerAtom edtsAtom) {
+ Atom.LeafAtom elst;
+ if (edtsAtom == null || (elst = edtsAtom.getLeafAtomOfType(Atom.TYPE_elst)) == null) {
+ return Pair.create(null, null);
+ }
+ ParsableByteArray elstData = elst.data;
+ elstData.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = elstData.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ int entryCount = elstData.readUnsignedIntToInt();
+ long[] editListDurations = new long[entryCount];
+ long[] editListMediaTimes = new long[entryCount];
+ for (int i = 0; i < entryCount; i++) {
+ editListDurations[i] =
+ version == 1 ? elstData.readUnsignedLongToLong() : elstData.readUnsignedInt();
+ editListMediaTimes[i] = version == 1 ? elstData.readLong() : elstData.readInt();
+ int mediaRateInteger = elstData.readShort();
+ if (mediaRateInteger != 1) {
+ // The extractor does not handle dwell edits (mediaRateInteger == 0).
+ throw new IllegalArgumentException("Unsupported media rate.");
+ }
+ elstData.skipBytes(2);
+ }
+ return Pair.create(editListDurations, editListMediaTimes);
+ }
+
+ private static float parsePaspFromParent(ParsableByteArray parent, int position) {
+ parent.setPosition(position + Atom.HEADER_SIZE);
+ int hSpacing = parent.readUnsignedIntToInt();
+ int vSpacing = parent.readUnsignedIntToInt();
+ return (float) hSpacing / vSpacing;
+ }
+
+ private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position,
+ int size, int trackId, String language, boolean isQuickTime, DrmInitData drmInitData,
+ StsdData out, int entryIndex) throws ParserException {
+ parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
+
+ int quickTimeSoundDescriptionVersion = 0;
+ if (isQuickTime) {
+ quickTimeSoundDescriptionVersion = parent.readUnsignedShort();
+ parent.skipBytes(6);
+ } else {
+ parent.skipBytes(8);
+ }
+
+ int channelCount;
+ int sampleRate;
+ @C.PcmEncoding int pcmEncoding = Format.NO_VALUE;
+
+ if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) {
+ channelCount = parent.readUnsignedShort();
+ parent.skipBytes(6); // sampleSize, compressionId, packetSize.
+ sampleRate = parent.readUnsignedFixedPoint1616();
+
+ if (quickTimeSoundDescriptionVersion == 1) {
+ parent.skipBytes(16);
+ }
+ } else if (quickTimeSoundDescriptionVersion == 2) {
+ parent.skipBytes(16); // always[3,16,Minus2,0,65536], sizeOfStructOnly
+
+ sampleRate = (int) Math.round(parent.readDouble());
+ channelCount = parent.readUnsignedIntToInt();
+
+ // Skip always7F000000, sampleSize, formatSpecificFlags, constBytesPerAudioPacket,
+ // constLPCMFramesPerAudioPacket.
+ parent.skipBytes(20);
+ } else {
+ // Unsupported version.
+ return;
+ }
+
+ int childPosition = parent.getPosition();
+ if (atomType == Atom.TYPE_enca) {
+ Pair<Integer, TrackEncryptionBox> 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;
+ if (atomType == Atom.TYPE_ac_3) {
+ 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) {
+ mimeType = MimeTypes.AUDIO_DTS_HD;
+ } else if (atomType == Atom.TYPE_dtse) {
+ mimeType = MimeTypes.AUDIO_DTS_EXPRESS;
+ } else if (atomType == Atom.TYPE_samr) {
+ mimeType = MimeTypes.AUDIO_AMR_NB;
+ } else if (atomType == Atom.TYPE_sawb) {
+ 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;
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childAtomSize = parent.readInt();
+ Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_esds || (isQuickTime && childAtomType == Atom.TYPE_wave)) {
+ int esdsAtomPosition = childAtomType == Atom.TYPE_esds ? childPosition
+ : findEsdsPosition(parent, childPosition, childAtomSize);
+ if (esdsAtomPosition != C.POSITION_UNSET) {
+ Pair<String, byte[]> mimeTypeAndInitializationData =
+ parseEsdsFromParent(parent, esdsAtomPosition);
+ mimeType = mimeTypeAndInitializationData.first;
+ initializationData = mimeTypeAndInitializationData.second;
+ if (MimeTypes.AUDIO_AAC.equals(mimeType)) {
+ // Update sampleRate and channelCount from the AudioSpecificConfig initialization data,
+ // which is more reliable. See [Internal: b/10903778].
+ Pair<Integer, Integer> audioSpecificConfig =
+ CodecSpecificDataUtil.parseAacAudioSpecificConfig(initializationData);
+ sampleRate = audioSpecificConfig.first;
+ channelCount = audioSpecificConfig.second;
+ }
+ }
+ } else if (childAtomType == Atom.TYPE_dac3) {
+ parent.setPosition(Atom.HEADER_SIZE + childPosition);
+ out.format = Ac3Util.parseAc3AnnexFFormat(parent, Integer.toString(trackId), language,
+ drmInitData);
+ } else if (childAtomType == Atom.TYPE_dec3) {
+ 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) {
+ 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<Integer, Integer> audioSpecificConfig =
+ CodecSpecificDataUtil.parseAlacAudioSpecificConfig(initializationData);
+ sampleRate = audioSpecificConfig.first;
+ channelCount = audioSpecificConfig.second;
+ }
+ childPosition += childAtomSize;
+ }
+
+ if (out.format == null && mimeType != null) {
+ out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
+ Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding,
+ initializationData == null ? null : Collections.singletonList(initializationData),
+ drmInitData, 0, language);
+ }
+ }
+
+ /**
+ * Returns the position of the esds box within a parent, or {@link C#POSITION_UNSET} if no esds
+ * box is found
+ */
+ private static int findEsdsPosition(ParsableByteArray parent, int position, int size) {
+ int childAtomPosition = parent.getPosition();
+ while (childAtomPosition - position < size) {
+ parent.setPosition(childAtomPosition);
+ int childAtomSize = parent.readInt();
+ Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
+ int childType = parent.readInt();
+ if (childType == Atom.TYPE_esds) {
+ return childAtomPosition;
+ }
+ childAtomPosition += childAtomSize;
+ }
+ return C.POSITION_UNSET;
+ }
+
+ /**
+ * Returns codec-specific initialization data contained in an esds box.
+ */
+ private static Pair<String, byte[]> parseEsdsFromParent(ParsableByteArray parent, int position) {
+ parent.setPosition(position + Atom.HEADER_SIZE + 4);
+ // Start of the ES_Descriptor (defined in 14496-1)
+ parent.skipBytes(1); // ES_Descriptor tag
+ parseExpandableClassSize(parent);
+ parent.skipBytes(2); // ES_ID
+
+ int flags = parent.readUnsignedByte();
+ if ((flags & 0x80 /* streamDependenceFlag */) != 0) {
+ parent.skipBytes(2);
+ }
+ if ((flags & 0x40 /* URL_Flag */) != 0) {
+ parent.skipBytes(parent.readUnsignedShort());
+ }
+ if ((flags & 0x20 /* OCRstreamFlag */) != 0) {
+ parent.skipBytes(2);
+ }
+
+ // Start of the DecoderConfigDescriptor (defined in 14496-1)
+ parent.skipBytes(1); // DecoderConfigDescriptor tag
+ parseExpandableClassSize(parent);
+
+ // Set the MIME type based on the object type indication (14496-1 table 5).
+ int objectTypeIndication = parent.readUnsignedByte();
+ 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 DecoderSpecificInfo.
+ parent.skipBytes(1); // DecoderSpecificInfo tag
+ int initializationDataSize = parseExpandableClassSize(parent);
+ byte[] initializationData = new byte[initializationDataSize];
+ parent.readBytes(initializationData, 0, initializationDataSize);
+ return Pair.create(mimeType, initializationData);
+ }
+
+ /**
+ * 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 Pair<Integer, TrackEncryptionBox> parseSampleEntryEncryptionData(
+ ParsableByteArray parent, int position, int size) {
+ int childPosition = parent.getPosition();
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childAtomSize = parent.readInt();
+ Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_sinf) {
+ Pair<Integer, TrackEncryptionBox> result = parseCommonEncryptionSinfFromParent(parent,
+ childPosition, childAtomSize);
+ if (result != null) {
+ return result;
+ }
+ }
+ childPosition += childAtomSize;
+ }
+ return null;
+ }
+
+ /* package */ static Pair<Integer, TrackEncryptionBox> parseCommonEncryptionSinfFromParent(
+ ParsableByteArray parent, int position, int size) {
+ int childPosition = position + Atom.HEADER_SIZE;
+ int schemeInformationBoxPosition = C.POSITION_UNSET;
+ int schemeInformationBoxSize = 0;
+ String schemeType = null;
+ Integer dataFormat = null;
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childAtomSize = parent.readInt();
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_frma) {
+ dataFormat = parent.readInt();
+ } else if (childAtomType == Atom.TYPE_schm) {
+ parent.skipBytes(4);
+ // 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) {
+ schemeInformationBoxPosition = childPosition;
+ schemeInformationBoxSize = childAtomSize;
+ }
+ childPosition += childAtomSize;
+ }
+
+ 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(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, 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) {
+ 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);
+ 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;
+ }
+ return null;
+ }
+
+ /**
+ * 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;
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childAtomSize = parent.readInt();
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_proj) {
+ return Arrays.copyOfRange(parent.data, childPosition, childPosition + childAtomSize);
+ }
+ childPosition += childAtomSize;
+ }
+ return null;
+ }
+
+ /**
+ * Parses the size of an expandable class, as specified by ISO 14496-1 subsection 8.3.3.
+ */
+ private static int parseExpandableClassSize(ParsableByteArray data) {
+ int currentByte = data.readUnsignedByte();
+ int size = currentByte & 0x7F;
+ while ((currentByte & 0x80) == 0x80) {
+ currentByte = data.readUnsignedByte();
+ size = (size << 7) | (currentByte & 0x7F);
+ }
+ 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.
+ }
+
+ private static final class ChunkIterator {
+
+ public final int length;
+
+ public int index;
+ public int numSamples;
+ public long offset;
+
+ private final boolean chunkOffsetsAreLongs;
+ private final ParsableByteArray chunkOffsets;
+ private final ParsableByteArray stsc;
+
+ private int nextSamplesPerChunkChangeIndex;
+ private int remainingSamplesPerChunkChanges;
+
+ public ChunkIterator(ParsableByteArray stsc, ParsableByteArray chunkOffsets,
+ boolean chunkOffsetsAreLongs) {
+ this.stsc = stsc;
+ this.chunkOffsets = chunkOffsets;
+ this.chunkOffsetsAreLongs = chunkOffsetsAreLongs;
+ chunkOffsets.setPosition(Atom.FULL_HEADER_SIZE);
+ length = chunkOffsets.readUnsignedIntToInt();
+ stsc.setPosition(Atom.FULL_HEADER_SIZE);
+ remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt();
+ Assertions.checkState(stsc.readInt() == 1, "first_chunk must be 1");
+ index = -1;
+ }
+
+ public boolean moveNext() {
+ if (++index == length) {
+ return false;
+ }
+ offset = chunkOffsetsAreLongs ? chunkOffsets.readUnsignedLongToLong()
+ : chunkOffsets.readUnsignedInt();
+ if (index == nextSamplesPerChunkChangeIndex) {
+ numSamples = stsc.readUnsignedIntToInt();
+ stsc.skipBytes(4); // Skip sample_description_index
+ nextSamplesPerChunkChangeIndex = --remainingSamplesPerChunkChanges > 0
+ ? (stsc.readUnsignedIntToInt() - 1) : C.INDEX_UNSET;
+ }
+ return true;
+ }
+
+ }
+
+ /**
+ * Holds data parsed from a tkhd atom.
+ */
+ private static final class TkhdData {
+
+ private final int id;
+ private final long duration;
+ private final int rotationDegrees;
+
+ public TkhdData(int id, long duration, int rotationDegrees) {
+ this.id = id;
+ this.duration = duration;
+ this.rotationDegrees = rotationDegrees;
+ }
+
+ }
+
+ /**
+ * Holds data parsed from an stsd atom and its children.
+ */
+ private static final class StsdData {
+
+ public static final int STSD_HEADER_SIZE = 8;
+
+ public final TrackEncryptionBox[] trackEncryptionBoxes;
+
+ public Format format;
+ public int nalUnitLengthFieldLength;
+ @Track.Transformation
+ public int requiredSampleTransformation;
+
+ public StsdData(int numberOfEntries) {
+ trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries];
+ requiredSampleTransformation = Track.TRANSFORMATION_NONE;
+ }
+
+ }
+
+ /**
+ * A box containing sample sizes (e.g. stsz, stz2).
+ */
+ private interface SampleSizeBox {
+
+ /**
+ * Returns the number of samples.
+ */
+ int getSampleCount();
+
+ /**
+ * Returns the size for the next sample.
+ */
+ int readNextSampleSize();
+
+ /**
+ * Returns whether samples have a fixed size.
+ */
+ boolean isFixedSampleSize();
+
+ }
+
+ /**
+ * An stsz sample size box.
+ */
+ /* package */ static final class StszSampleSizeBox implements SampleSizeBox {
+
+ private final int fixedSampleSize;
+ private final int sampleCount;
+ private final ParsableByteArray data;
+
+ public StszSampleSizeBox(Atom.LeafAtom stszAtom) {
+ data = stszAtom.data;
+ data.setPosition(Atom.FULL_HEADER_SIZE);
+ fixedSampleSize = data.readUnsignedIntToInt();
+ sampleCount = data.readUnsignedIntToInt();
+ }
+
+ @Override
+ public int getSampleCount() {
+ return sampleCount;
+ }
+
+ @Override
+ public int readNextSampleSize() {
+ return fixedSampleSize == 0 ? data.readUnsignedIntToInt() : fixedSampleSize;
+ }
+
+ @Override
+ public boolean isFixedSampleSize() {
+ return fixedSampleSize != 0;
+ }
+
+ }
+
+ /**
+ * An stz2 sample size box.
+ */
+ /* package */ static final class Stz2SampleSizeBox implements SampleSizeBox {
+
+ private final ParsableByteArray data;
+ private final int sampleCount;
+ private final int fieldSize; // Can be 4, 8, or 16.
+
+ // Used only if fieldSize == 4.
+ private int sampleIndex;
+ private int currentByte;
+
+ public Stz2SampleSizeBox(Atom.LeafAtom stz2Atom) {
+ data = stz2Atom.data;
+ data.setPosition(Atom.FULL_HEADER_SIZE);
+ fieldSize = data.readUnsignedIntToInt() & 0x000000FF;
+ sampleCount = data.readUnsignedIntToInt();
+ }
+
+ @Override
+ public int getSampleCount() {
+ return sampleCount;
+ }
+
+ @Override
+ public int readNextSampleSize() {
+ if (fieldSize == 8) {
+ return data.readUnsignedByte();
+ } else if (fieldSize == 16) {
+ return data.readUnsignedShort();
+ } else {
+ // fieldSize == 4.
+ if ((sampleIndex++ % 2) == 0) {
+ // Read the next byte into our cached byte when we are reading the upper bits.
+ currentByte = data.readUnsignedByte();
+ // Read the upper bits from the byte and shift them to the lower 4 bits.
+ return (currentByte & 0xF0) >> 4;
+ } else {
+ // Mask out the upper 4 bits of the last byte we read.
+ return currentByte & 0x0F;
+ }
+ }
+ }
+
+ @Override
+ public boolean isFixedSampleSize() {
+ return false;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java
new file mode 100644
index 0000000000..0942673435
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java
@@ -0,0 +1,32 @@
+/*
+ * 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.mp4;
+
+/* package */ final class DefaultSampleValues {
+
+ public final int sampleDescriptionIndex;
+ public final int duration;
+ public final int size;
+ public final int flags;
+
+ public DefaultSampleValues(int sampleDescriptionIndex, int duration, int size, int flags) {
+ this.sampleDescriptionIndex = sampleDescriptionIndex;
+ this.duration = duration;
+ this.size = size;
+ this.flags = flags;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java
new file mode 100644
index 0000000000..78d30ba582
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java
@@ -0,0 +1,114 @@
+/*
+ * 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.mp4;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+/**
+ * Rechunks fixed sample size media in which every sample is a key frame (e.g. uncompressed audio).
+ */
+/* package */ final class FixedSampleSizeRechunker {
+
+ /**
+ * The result of a rechunking operation.
+ */
+ public static final class Results {
+
+ public final long[] offsets;
+ public final int[] sizes;
+ 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,
+ long duration) {
+ this.offsets = offsets;
+ this.sizes = sizes;
+ this.maximumSize = maximumSize;
+ this.timestamps = timestamps;
+ this.flags = flags;
+ this.duration = duration;
+ }
+
+ }
+
+ /**
+ * Maximum number of bytes for each buffer in rechunked output.
+ */
+ private static final int MAX_SAMPLE_SIZE = 8 * 1024;
+
+ /**
+ * Rechunk the given fixed sample size input to produce a new sequence of samples.
+ *
+ * @param fixedSampleSize Size in bytes of each sample.
+ * @param chunkOffsets Chunk offsets in the MP4 stream to rechunk.
+ * @param chunkSampleCounts Sample counts for each of the MP4 stream's chunks.
+ * @param timestampDeltaInTimeUnits Timestamp delta between each sample in time units.
+ */
+ public static Results rechunk(int fixedSampleSize, long[] chunkOffsets, int[] chunkSampleCounts,
+ long timestampDeltaInTimeUnits) {
+ int maxSampleCount = MAX_SAMPLE_SIZE / fixedSampleSize;
+
+ // Count the number of new, rechunked buffers.
+ int rechunkedSampleCount = 0;
+ for (int chunkSampleCount : chunkSampleCounts) {
+ rechunkedSampleCount += Util.ceilDivide(chunkSampleCount, maxSampleCount);
+ }
+
+ long[] offsets = new long[rechunkedSampleCount];
+ int[] sizes = new int[rechunkedSampleCount];
+ int maximumSize = 0;
+ long[] timestamps = new long[rechunkedSampleCount];
+ int[] flags = new int[rechunkedSampleCount];
+
+ int originalSampleIndex = 0;
+ int newSampleIndex = 0;
+ for (int chunkIndex = 0; chunkIndex < chunkSampleCounts.length; chunkIndex++) {
+ int chunkSamplesRemaining = chunkSampleCounts[chunkIndex];
+ long sampleOffset = chunkOffsets[chunkIndex];
+
+ while (chunkSamplesRemaining > 0) {
+ int bufferSampleCount = Math.min(maxSampleCount, chunkSamplesRemaining);
+
+ offsets[newSampleIndex] = sampleOffset;
+ sizes[newSampleIndex] = fixedSampleSize * bufferSampleCount;
+ maximumSize = Math.max(maximumSize, sizes[newSampleIndex]);
+ timestamps[newSampleIndex] = (timestampDeltaInTimeUnits * originalSampleIndex);
+ flags[newSampleIndex] = C.BUFFER_FLAG_KEY_FRAME;
+
+ sampleOffset += sizes[newSampleIndex];
+ originalSampleIndex += bufferSampleCount;
+
+ chunkSamplesRemaining -= bufferSampleCount;
+ newSampleIndex++;
+ }
+ }
+ long duration = timestampDeltaInTimeUnits * originalSampleIndex;
+
+ return new Results(offsets, sizes, maximumSize, timestamps, flags, duration);
+ }
+
+ private FixedSampleSizeRechunker() {
+ // Prevent instantiation.
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
new file mode 100644
index 0000000000..291a9ade27
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
@@ -0,0 +1,1660 @@
+/*
+ * 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.mp4;
+
+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;
+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.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.Collections;
+import java.util.List;
+import java.util.UUID;
+
+/** 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()};
+
+ /**
+ * 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_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.
+ * The workaround overrides the sync frame flags in the stream, forcing them to false except for
+ * the first sample in each segment.
+ * <p>
+ * 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 = 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 = 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 = 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";
+
+ @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;
+ private static final int STATE_READING_ATOM_PAYLOAD = 1;
+ private static final int STATE_READING_ENCRYPTION_DATA = 2;
+ private static final int STATE_READING_SAMPLE_START = 3;
+ private static final int STATE_READING_SAMPLE_CONTINUE = 4;
+
+ // Workarounds.
+ @Flags private final int flags;
+ @Nullable private final Track sideloadedTrack;
+
+ // Sideloaded data.
+ private final List<Format> closedCaptionFormats;
+
+ // Track-linked data bundle, accessible as a whole through trackID.
+ private final SparseArray<TrackBundle> trackBundles;
+
+ // Temporary arrays.
+ private final ParsableByteArray nalStartCode;
+ private final ParsableByteArray nalPrefix;
+ private final ParsableByteArray nalBuffer;
+ private final byte[] scratchBytes;
+ private final ParsableByteArray scratch;
+
+ // Adjusts sample timestamps.
+ @Nullable private final TimestampAdjuster timestampAdjuster;
+
+ private final EventMessageEncoder eventMessageEncoder;
+
+ // Parser state.
+ private final ParsableByteArray atomHeader;
+ private final ArrayDeque<ContainerAtom> containerAtoms;
+ private final ArrayDeque<MetadataSampleInfo> pendingMetadataSampleInfos;
+ @Nullable private final TrackOutput additionalEmsgTrackOutput;
+
+ private int parserState;
+ private int atomType;
+ private long atomSize;
+ private int atomHeaderBytesRead;
+ private ParsableByteArray atomData;
+ private long endOfMdatPosition;
+ private int pendingMetadataSampleBytes;
+ private long pendingSeekTimeUs;
+
+ private long durationUs;
+ private long segmentIndexEarliestPresentationTimeUs;
+ private TrackBundle currentTrackBundle;
+ private int sampleSize;
+ private int sampleBytesWritten;
+ private int sampleCurrentNalBytesRemaining;
+ private boolean processSeiNalUnitPayload;
+
+ // Extractor output.
+ private ExtractorOutput extractorOutput;
+ private TrackOutput[] emsgTrackOutputs;
+ private TrackOutput[] cea608TrackOutputs;
+
+ // Whether extractorOutput.seekMap has been called.
+ private boolean haveOutputSeekMap;
+
+ public FragmentedMp4Extractor() {
+ this(0);
+ }
+
+ /**
+ * @param flags Flags that control the extractor's behavior.
+ */
+ public FragmentedMp4Extractor(@Flags int flags) {
+ 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, @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. Null if a moov box is expected.
+ */
+ 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<Format> 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<Format> 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();
+ 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();
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ return Sniffer.sniffFragmented(input);
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ extractorOutput = output;
+ if (sideloadedTrack != null) {
+ TrackBundle bundle = new TrackBundle(output.track(0, sideloadedTrack.type));
+ bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0));
+ trackBundles.put(0, bundle);
+ maybeInitExtraTracks();
+ extractorOutput.endTracks();
+ }
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ int trackCount = trackBundles.size();
+ for (int i = 0; i < trackCount; i++) {
+ trackBundles.valueAt(i).reset();
+ }
+ pendingMetadataSampleInfos.clear();
+ pendingMetadataSampleBytes = 0;
+ pendingSeekTimeUs = timeUs;
+ containerAtoms.clear();
+ enterReadingAtomHeaderState();
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ while (true) {
+ switch (parserState) {
+ case STATE_READING_ATOM_HEADER:
+ if (!readAtomHeader(input)) {
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_READING_ATOM_PAYLOAD:
+ readAtomPayload(input);
+ break;
+ case STATE_READING_ENCRYPTION_DATA:
+ readEncryptionData(input);
+ break;
+ default:
+ if (readSample(input)) {
+ return RESULT_CONTINUE;
+ }
+ }
+ }
+ }
+
+ private void enterReadingAtomHeaderState() {
+ parserState = STATE_READING_ATOM_HEADER;
+ atomHeaderBytesRead = 0;
+ }
+
+ private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException {
+ if (atomHeaderBytesRead == 0) {
+ // Read the standard length atom header.
+ if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) {
+ return false;
+ }
+ atomHeaderBytesRead = Atom.HEADER_SIZE;
+ atomHeader.setPosition(0);
+ atomSize = atomHeader.readUnsignedInt();
+ atomType = atomHeader.readInt();
+ }
+
+ 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).");
+ }
+
+ long atomPosition = input.getPosition() - atomHeaderBytesRead;
+ if (atomType == Atom.TYPE_moof) {
+ // The data positions may be updated when parsing the tfhd/trun.
+ int trackCount = trackBundles.size();
+ for (int i = 0; i < trackCount; i++) {
+ TrackFragment fragment = trackBundles.valueAt(i).fragment;
+ fragment.atomPosition = atomPosition;
+ fragment.auxiliaryDataPosition = atomPosition;
+ fragment.dataPosition = atomPosition;
+ }
+ }
+
+ if (atomType == Atom.TYPE_mdat) {
+ currentTrackBundle = null;
+ endOfMdatPosition = atomPosition + atomSize;
+ if (!haveOutputSeekMap) {
+ // This must be the first mdat in the stream.
+ extractorOutput.seekMap(new SeekMap.Unseekable(durationUs, atomPosition));
+ haveOutputSeekMap = true;
+ }
+ parserState = STATE_READING_ENCRYPTION_DATA;
+ return true;
+ }
+
+ if (shouldParseContainerAtom(atomType)) {
+ long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE;
+ containerAtoms.push(new ContainerAtom(atomType, endPosition));
+ if (atomSize == atomHeaderBytesRead) {
+ processAtomEnded(endPosition);
+ } else {
+ // Start reading the first child atom.
+ enterReadingAtomHeaderState();
+ }
+ } else if (shouldParseLeafAtom(atomType)) {
+ if (atomHeaderBytesRead != Atom.HEADER_SIZE) {
+ throw new ParserException("Leaf atom defines extended atom size (unsupported).");
+ }
+ if (atomSize > Integer.MAX_VALUE) {
+ throw new ParserException("Leaf atom with length > 2147483647 (unsupported).");
+ }
+ atomData = new ParsableByteArray((int) atomSize);
+ System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE);
+ parserState = STATE_READING_ATOM_PAYLOAD;
+ } else {
+ if (atomSize > Integer.MAX_VALUE) {
+ throw new ParserException("Skipping atom with length > 2147483647 (unsupported).");
+ }
+ atomData = null;
+ parserState = STATE_READING_ATOM_PAYLOAD;
+ }
+
+ return true;
+ }
+
+ private void readAtomPayload(ExtractorInput input) throws IOException, InterruptedException {
+ int atomPayloadSize = (int) atomSize - atomHeaderBytesRead;
+ if (atomData != null) {
+ input.readFully(atomData.data, Atom.HEADER_SIZE, atomPayloadSize);
+ onLeafAtomRead(new LeafAtom(atomType, atomData), input.getPosition());
+ } else {
+ input.skipFully(atomPayloadSize);
+ }
+ processAtomEnded(input.getPosition());
+ }
+
+ private void processAtomEnded(long atomEndPosition) throws ParserException {
+ while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) {
+ onContainerAtomRead(containerAtoms.pop());
+ }
+ enterReadingAtomHeaderState();
+ }
+
+ private void onLeafAtomRead(LeafAtom leaf, long inputPosition) throws ParserException {
+ if (!containerAtoms.isEmpty()) {
+ containerAtoms.peek().add(leaf);
+ } else if (leaf.type == Atom.TYPE_sidx) {
+ Pair<Long, ChunkIndex> result = parseSidx(leaf.data, inputPosition);
+ segmentIndexEarliestPresentationTimeUs = result.first;
+ extractorOutput.seekMap(result.second);
+ haveOutputSeekMap = true;
+ } else if (leaf.type == Atom.TYPE_emsg) {
+ onEmsgLeafAtomRead(leaf.data);
+ }
+ }
+
+ private void onContainerAtomRead(ContainerAtom container) throws ParserException {
+ if (container.type == Atom.TYPE_moov) {
+ onMoovContainerAtomRead(container);
+ } else if (container.type == Atom.TYPE_moof) {
+ onMoofContainerAtomRead(container);
+ } else if (!containerAtoms.isEmpty()) {
+ containerAtoms.peek().add(container);
+ }
+ }
+
+ private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException {
+ Assertions.checkState(sideloadedTrack == null, "Unexpected moov box.");
+
+ @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren);
+
+ // Read declaration of track fragments in the Moov box.
+ ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
+ SparseArray<DefaultSampleValues> defaultSampleValuesArray = new SparseArray<>();
+ long duration = C.TIME_UNSET;
+ int mvexChildrenSize = mvex.leafChildren.size();
+ for (int i = 0; i < mvexChildrenSize; i++) {
+ Atom.LeafAtom atom = mvex.leafChildren.get(i);
+ if (atom.type == Atom.TYPE_trex) {
+ Pair<Integer, DefaultSampleValues> trexData = parseTrex(atom.data);
+ defaultSampleValuesArray.put(trexData.first, trexData.second);
+ } else if (atom.type == Atom.TYPE_mehd) {
+ duration = parseMehd(atom.data);
+ }
+ }
+
+ // Construction of tracks.
+ SparseArray<Track> tracks = new SparseArray<>();
+ int moovContainerChildrenSize = moov.containerChildren.size();
+ for (int i = 0; i < moovContainerChildrenSize; i++) {
+ Atom.ContainerAtom atom = moov.containerChildren.get(i);
+ if (atom.type == Atom.TYPE_trak) {
+ 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);
+ }
+ }
+ }
+
+ int trackCount = tracks.size();
+ if (trackBundles.size() == 0) {
+ // We need to create the track bundles.
+ for (int i = 0; i < trackCount; i++) {
+ Track track = tracks.valueAt(i);
+ TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type));
+ trackBundle.init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id));
+ trackBundles.put(track.id, trackBundle);
+ durationUs = Math.max(durationUs, track.durationUs);
+ }
+ maybeInitExtraTracks();
+ extractorOutput.endTracks();
+ } else {
+ Assertions.checkState(trackBundles.size() == trackCount);
+ for (int i = 0; i < trackCount; i++) {
+ Track track = tracks.valueAt(i);
+ trackBundles
+ .get(track.id)
+ .init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id));
+ }
+ }
+ }
+
+ @Nullable
+ protected Track modifyTrack(@Nullable Track track) {
+ return track;
+ }
+
+ private DefaultSampleValues getDefaultSampleValues(
+ SparseArray<DefaultSampleValues> 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, 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 (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 (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). */
+ private void onEmsgLeafAtomRead(ParsableByteArray atom) {
+ if (emsgTrackOutputs == null || emsgTrackOutputs.length == 0) {
+ return;
+ }
+ 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.
+ 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). */
+ private static Pair<Integer, DefaultSampleValues> parseTrex(ParsableByteArray trex) {
+ trex.setPosition(Atom.FULL_HEADER_SIZE);
+ int trackId = trex.readInt();
+ int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1;
+ int defaultSampleDuration = trex.readUnsignedIntToInt();
+ int defaultSampleSize = trex.readUnsignedIntToInt();
+ int defaultSampleFlags = trex.readInt();
+
+ return Pair.create(trackId, new DefaultSampleValues(defaultSampleDescriptionIndex,
+ defaultSampleDuration, defaultSampleSize, defaultSampleFlags));
+ }
+
+ /**
+ * Parses an mehd atom (defined in 14496-12).
+ */
+ private static long parseMehd(ParsableByteArray mehd) {
+ mehd.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = mehd.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ return version == 0 ? mehd.readUnsignedInt() : mehd.readUnsignedLongToLong();
+ }
+
+ private static void parseMoof(ContainerAtom moof, SparseArray<TrackBundle> trackBundleArray,
+ @Flags int flags, byte[] extendedTypeScratch) throws ParserException {
+ int moofContainerChildrenSize = moof.containerChildren.size();
+ for (int i = 0; i < moofContainerChildrenSize; i++) {
+ Atom.ContainerAtom child = moof.containerChildren.get(i);
+ // TODO: Support multiple traf boxes per track in a single moof.
+ if (child.type == Atom.TYPE_traf) {
+ parseTraf(child, trackBundleArray, flags, extendedTypeScratch);
+ }
+ }
+ }
+
+ /**
+ * Parses a traf atom (defined in 14496-12).
+ */
+ private static void parseTraf(ContainerAtom traf, SparseArray<TrackBundle> trackBundleArray,
+ @Flags int flags, byte[] extendedTypeScratch) throws ParserException {
+ LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
+ TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray);
+ if (trackBundle == null) {
+ return;
+ }
+
+ TrackFragment fragment = trackBundle.fragment;
+ long decodeTime = fragment.nextFragmentDecodeTime;
+ trackBundle.reset();
+
+ LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt);
+ if (tfdtAtom != null && (flags & FLAG_WORKAROUND_IGNORE_TFDT_BOX) == 0) {
+ decodeTime = parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data);
+ }
+
+ parseTruns(traf, trackBundle, decodeTime, flags);
+
+ TrackEncryptionBox encryptionBox = trackBundle.track
+ .getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex);
+
+ LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
+ if (saiz != null) {
+ parseSaiz(encryptionBox, saiz.data, fragment);
+ }
+
+ LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio);
+ if (saio != null) {
+ parseSaio(saio.data, fragment);
+ }
+
+ LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc);
+ if (senc != null) {
+ parseSenc(senc.data, fragment);
+ }
+
+ LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp);
+ LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd);
+ if (sbgp != null && sgpd != null) {
+ parseSgpd(sbgp.data, sgpd.data, encryptionBox != null ? encryptionBox.schemeType : null,
+ fragment);
+ }
+
+ int leafChildrenSize = traf.leafChildren.size();
+ for (int i = 0; i < leafChildrenSize; i++) {
+ LeafAtom atom = traf.leafChildren.get(i);
+ if (atom.type == Atom.TYPE_uuid) {
+ parseUuid(atom.data, fragment, extendedTypeScratch);
+ }
+ }
+ }
+
+ private static void parseTruns(ContainerAtom traf, TrackBundle trackBundle, long decodeTime,
+ @Flags int flags) {
+ int trunCount = 0;
+ int totalSampleCount = 0;
+ List<LeafAtom> leafChildren = traf.leafChildren;
+ int leafChildrenSize = leafChildren.size();
+ for (int i = 0; i < leafChildrenSize; i++) {
+ LeafAtom atom = leafChildren.get(i);
+ if (atom.type == Atom.TYPE_trun) {
+ ParsableByteArray trunData = atom.data;
+ trunData.setPosition(Atom.FULL_HEADER_SIZE);
+ int trunSampleCount = trunData.readUnsignedIntToInt();
+ if (trunSampleCount > 0) {
+ totalSampleCount += trunSampleCount;
+ trunCount++;
+ }
+ }
+ }
+ trackBundle.currentTrackRunIndex = 0;
+ trackBundle.currentSampleInTrackRun = 0;
+ trackBundle.currentSampleIndex = 0;
+ trackBundle.fragment.initTables(trunCount, totalSampleCount);
+
+ int trunIndex = 0;
+ int trunStartPosition = 0;
+ for (int i = 0; i < leafChildrenSize; i++) {
+ LeafAtom trun = leafChildren.get(i);
+ if (trun.type == Atom.TYPE_trun) {
+ trunStartPosition = parseTrun(trackBundle, trunIndex++, decodeTime, flags, trun.data,
+ trunStartPosition);
+ }
+ }
+ }
+
+ private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz,
+ TrackFragment out) throws ParserException {
+ int vectorSize = encryptionBox.perSampleIvSize;
+ saiz.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = saiz.readInt();
+ int flags = Atom.parseFullAtomFlags(fullAtom);
+ if ((flags & 0x01) == 1) {
+ saiz.skipBytes(8);
+ }
+ int defaultSampleInfoSize = saiz.readUnsignedByte();
+
+ int sampleCount = saiz.readUnsignedIntToInt();
+ if (sampleCount != out.sampleCount) {
+ throw new ParserException("Length mismatch: " + sampleCount + ", " + out.sampleCount);
+ }
+
+ int totalSize = 0;
+ if (defaultSampleInfoSize == 0) {
+ boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable;
+ for (int i = 0; i < sampleCount; i++) {
+ int sampleInfoSize = saiz.readUnsignedByte();
+ totalSize += sampleInfoSize;
+ sampleHasSubsampleEncryptionTable[i] = sampleInfoSize > vectorSize;
+ }
+ } else {
+ boolean subsampleEncryption = defaultSampleInfoSize > vectorSize;
+ totalSize += defaultSampleInfoSize * sampleCount;
+ Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
+ }
+ out.initEncryptionData(totalSize);
+ }
+
+ /**
+ * Parses a saio atom (defined in 14496-12).
+ *
+ * @param saio The saio atom to decode.
+ * @param out The {@link TrackFragment} to populate with data from the saio atom.
+ */
+ private static void parseSaio(ParsableByteArray saio, TrackFragment out) throws ParserException {
+ saio.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = saio.readInt();
+ int flags = Atom.parseFullAtomFlags(fullAtom);
+ if ((flags & 0x01) == 1) {
+ saio.skipBytes(8);
+ }
+
+ int entryCount = saio.readUnsignedIntToInt();
+ if (entryCount != 1) {
+ // We only support one trun element currently, so always expect one entry.
+ throw new ParserException("Unexpected saio entry count: " + entryCount);
+ }
+
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ out.auxiliaryDataPosition +=
+ version == 0 ? saio.readUnsignedInt() : saio.readUnsignedLongToLong();
+ }
+
+ /**
+ * Parses a tfhd atom (defined in 14496-12), updates the corresponding {@link TrackFragment} and
+ * returns the {@link TrackBundle} of the corresponding {@link Track}. If the tfhd does not refer
+ * to any {@link TrackBundle}, {@code null} is returned and no changes are made.
+ *
+ * @param tfhd The tfhd atom to decode.
+ * @param trackBundles The track bundles, one of which corresponds to the tfhd atom being parsed.
+ * @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<TrackBundle> trackBundles) {
+ tfhd.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = tfhd.readInt();
+ int atomFlags = Atom.parseFullAtomFlags(fullAtom);
+ int trackId = tfhd.readInt();
+ TrackBundle trackBundle = getTrackBundle(trackBundles, trackId);
+ if (trackBundle == null) {
+ return null;
+ }
+ if ((atomFlags & 0x01 /* base_data_offset_present */) != 0) {
+ long baseDataPosition = tfhd.readUnsignedLongToLong();
+ trackBundle.fragment.dataPosition = baseDataPosition;
+ trackBundle.fragment.auxiliaryDataPosition = baseDataPosition;
+ }
+
+ DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues;
+ int defaultSampleDescriptionIndex =
+ ((atomFlags & 0x02 /* default_sample_description_index_present */) != 0)
+ ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex;
+ int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0)
+ ? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration;
+ int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0)
+ ? tfhd.readUnsignedIntToInt() : defaultSampleValues.size;
+ int defaultSampleFlags = ((atomFlags & 0x20 /* default_sample_flags_present */) != 0)
+ ? tfhd.readUnsignedIntToInt() : defaultSampleValues.flags;
+ trackBundle.fragment.header = new DefaultSampleValues(defaultSampleDescriptionIndex,
+ defaultSampleDuration, defaultSampleSize, defaultSampleFlags);
+ return trackBundle;
+ }
+
+ private static @Nullable TrackBundle getTrackBundle(
+ SparseArray<TrackBundle> 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).
+ *
+ * @return baseMediaDecodeTime The sum of the decode durations of all earlier samples in the
+ * media, expressed in the media's timescale.
+ */
+ private static long parseTfdt(ParsableByteArray tfdt) {
+ tfdt.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = tfdt.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt();
+ }
+
+ /**
+ * Parses a trun atom (defined in 14496-12).
+ *
+ * @param trackBundle The {@link TrackBundle} that contains the {@link TrackFragment} into
+ * which parsed data should be placed.
+ * @param index Index of the track run in the fragment.
+ * @param decodeTime The decode time of the first sample in the fragment run.
+ * @param flags Flags to allow any required workaround to be executed.
+ * @param trun The trun atom to decode.
+ * @return The starting position of samples for the next run.
+ */
+ private static int parseTrun(TrackBundle trackBundle, int index, long decodeTime,
+ @Flags int flags, ParsableByteArray trun, int trackRunStart) {
+ trun.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = trun.readInt();
+ int atomFlags = Atom.parseFullAtomFlags(fullAtom);
+
+ Track track = trackBundle.track;
+ TrackFragment fragment = trackBundle.fragment;
+ DefaultSampleValues defaultSampleValues = fragment.header;
+
+ fragment.trunLength[index] = trun.readUnsignedIntToInt();
+ fragment.trunDataPosition[index] = fragment.dataPosition;
+ if ((atomFlags & 0x01 /* data_offset_present */) != 0) {
+ fragment.trunDataPosition[index] += trun.readInt();
+ }
+
+ boolean firstSampleFlagsPresent = (atomFlags & 0x04 /* first_sample_flags_present */) != 0;
+ int firstSampleFlags = defaultSampleValues.flags;
+ if (firstSampleFlagsPresent) {
+ firstSampleFlags = trun.readUnsignedIntToInt();
+ }
+
+ boolean sampleDurationsPresent = (atomFlags & 0x100 /* sample_duration_present */) != 0;
+ boolean sampleSizesPresent = (atomFlags & 0x200 /* sample_size_present */) != 0;
+ boolean sampleFlagsPresent = (atomFlags & 0x400 /* sample_flags_present */) != 0;
+ boolean sampleCompositionTimeOffsetsPresent =
+ (atomFlags & 0x800 /* sample_composition_time_offsets_present */) != 0;
+
+ // Offset to the entire video timeline. In the presence of B-frames this is usually used to
+ // ensure that the first frame's presentation timestamp is zero.
+ long edtsOffset = 0;
+
+ // Currently we only support a single edit that moves the entire media timeline (indicated by
+ // 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], C.MILLIS_PER_SECOND, track.timescale);
+ }
+
+ int[] sampleSizeTable = fragment.sampleSizeTable;
+ int[] sampleCompositionTimeOffsetTable = fragment.sampleCompositionTimeOffsetTable;
+ long[] sampleDecodingTimeTable = fragment.sampleDecodingTimeTable;
+ boolean[] sampleIsSyncFrameTable = fragment.sampleIsSyncFrameTable;
+
+ boolean workaroundEveryVideoFrameIsSyncFrame = track.type == C.TRACK_TYPE_VIDEO
+ && (flags & FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME) != 0;
+
+ int trackRunEnd = trackRunStart + fragment.trunLength[index];
+ long timescale = track.timescale;
+ long cumulativeTime = index > 0 ? fragment.nextFragmentDecodeTime : decodeTime;
+ for (int i = trackRunStart; i < trackRunEnd; i++) {
+ // Use trun values if present, otherwise tfhd, otherwise trex.
+ int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()
+ : defaultSampleValues.duration;
+ int sampleSize = sampleSizesPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.size;
+ int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags
+ : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;
+ if (sampleCompositionTimeOffsetsPresent) {
+ // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
+ // version 0 trun boxes, however a significant number of streams violate the spec and use
+ // signed integers instead. It's safe to always decode sample offsets 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 offsets are always small).
+ int sampleOffset = trun.readInt();
+ sampleCompositionTimeOffsetTable[i] =
+ (int) ((sampleOffset * C.MILLIS_PER_SECOND) / timescale);
+ } else {
+ sampleCompositionTimeOffsetTable[i] = 0;
+ }
+ sampleDecodingTimeTable[i] =
+ Util.scaleLargeTimestamp(cumulativeTime, C.MILLIS_PER_SECOND, timescale) - edtsOffset;
+ sampleSizeTable[i] = sampleSize;
+ sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0
+ && (!workaroundEveryVideoFrameIsSyncFrame || i == 0);
+ cumulativeTime += sampleDuration;
+ }
+ fragment.nextFragmentDecodeTime = cumulativeTime;
+ return trackRunEnd;
+ }
+
+ private static void parseUuid(ParsableByteArray uuid, TrackFragment out,
+ byte[] extendedTypeScratch) throws ParserException {
+ uuid.setPosition(Atom.HEADER_SIZE);
+ uuid.readBytes(extendedTypeScratch, 0, 16);
+
+ // Currently this parser only supports Microsoft's PIFF SampleEncryptionBox.
+ if (!Arrays.equals(extendedTypeScratch, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) {
+ return;
+ }
+
+ // Except for the extended type, this box is identical to a SENC box. See "Portable encoding of
+ // audio-video objects: The Protected Interoperable File Format (PIFF), John A. Bocharov et al,
+ // Section 5.3.2.1."
+ parseSenc(uuid, 16, out);
+ }
+
+ private static void parseSenc(ParsableByteArray senc, TrackFragment out) throws ParserException {
+ parseSenc(senc, 0, out);
+ }
+
+ private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out)
+ throws ParserException {
+ senc.setPosition(Atom.HEADER_SIZE + offset);
+ int fullAtom = senc.readInt();
+ int flags = Atom.parseFullAtomFlags(fullAtom);
+
+ if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) {
+ // TODO: Implement this.
+ throw new ParserException("Overriding TrackEncryptionBox parameters is unsupported.");
+ }
+
+ boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0;
+ int sampleCount = senc.readUnsignedIntToInt();
+ if (sampleCount != out.sampleCount) {
+ throw new ParserException("Length mismatch: " + sampleCount + ", " + out.sampleCount);
+ }
+
+ Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
+ out.initEncryptionData(senc.bytesLeft());
+ out.fillEncryptionData(senc);
+ }
+
+ 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) {
+ // Only seig grouping type is supported.
+ return;
+ }
+ if (Atom.parseFullAtomVersion(sbgpFullAtom) == 1) {
+ sbgp.skipBytes(4); // default_length.
+ }
+ if (sbgp.readInt() != 1) { // entry_count.
+ throw new ParserException("Entry count in sbgp != 1 (unsupported).");
+ }
+
+ sgpd.setPosition(Atom.HEADER_SIZE);
+ int sgpdFullAtom = sgpd.readInt();
+ if (sgpd.readInt() != SAMPLE_GROUP_TYPE_seig) {
+ // Only seig grouping type is supported.
+ return;
+ }
+ int sgpdVersion = Atom.parseFullAtomVersion(sgpdFullAtom);
+ if (sgpdVersion == 1) {
+ if (sgpd.readUnsignedInt() == 0) {
+ throw new ParserException("Variable length description in sgpd found (unsupported)");
+ }
+ } else if (sgpdVersion >= 2) {
+ sgpd.skipBytes(4); // default_sample_description_index.
+ }
+ if (sgpd.readUnsignedInt() != 1) { // entry_count.
+ throw new ParserException("Entry count in sgpd != 1 (unsupported).");
+ }
+ // CencSampleEncryptionInformationGroupEntry
+ 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 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, schemeType, perSampleIvSize, keyId,
+ cryptByteBlock, skipByteBlock, constantIv);
+ }
+
+ /**
+ * Parses a sidx atom (defined in 14496-12).
+ *
+ * @param atom The atom data.
+ * @param inputPosition The input position of the first byte after the atom.
+ * @return A pair consisting of the earliest presentation time in microseconds, and the parsed
+ * {@link ChunkIndex}.
+ */
+ private static Pair<Long, ChunkIndex> parseSidx(ParsableByteArray atom, long inputPosition)
+ throws ParserException {
+ atom.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = atom.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+
+ atom.skipBytes(4);
+ long timescale = atom.readUnsignedInt();
+ long earliestPresentationTime;
+ long offset = inputPosition;
+ if (version == 0) {
+ earliestPresentationTime = atom.readUnsignedInt();
+ offset += atom.readUnsignedInt();
+ } else {
+ earliestPresentationTime = atom.readUnsignedLongToLong();
+ offset += atom.readUnsignedLongToLong();
+ }
+ long earliestPresentationTimeUs = Util.scaleLargeTimestamp(earliestPresentationTime,
+ C.MICROS_PER_SECOND, timescale);
+
+ atom.skipBytes(2);
+
+ int referenceCount = atom.readUnsignedShort();
+ int[] sizes = new int[referenceCount];
+ long[] offsets = new long[referenceCount];
+ long[] durationsUs = new long[referenceCount];
+ long[] timesUs = new long[referenceCount];
+
+ long time = earliestPresentationTime;
+ long timeUs = earliestPresentationTimeUs;
+ for (int i = 0; i < referenceCount; i++) {
+ int firstInt = atom.readInt();
+
+ int type = 0x80000000 & firstInt;
+ if (type != 0) {
+ throw new ParserException("Unhandled indirect reference");
+ }
+ long referenceDuration = atom.readUnsignedInt();
+
+ sizes[i] = 0x7FFFFFFF & firstInt;
+ offsets[i] = offset;
+
+ // Calculate time and duration values such that any rounding errors are consistent. i.e. That
+ // timesUs[i] + durationsUs[i] == timesUs[i + 1].
+ timesUs[i] = timeUs;
+ time += referenceDuration;
+ timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
+ durationsUs[i] = timeUs - timesUs[i];
+
+ atom.skipBytes(4);
+ offset += sizes[i];
+ }
+
+ return Pair.create(earliestPresentationTimeUs,
+ new ChunkIndex(sizes, offsets, durationsUs, timesUs));
+ }
+
+ private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException {
+ TrackBundle nextTrackBundle = null;
+ long nextDataOffset = Long.MAX_VALUE;
+ int trackBundlesSize = trackBundles.size();
+ for (int i = 0; i < trackBundlesSize; i++) {
+ TrackFragment trackFragment = trackBundles.valueAt(i).fragment;
+ if (trackFragment.sampleEncryptionDataNeedsFill
+ && trackFragment.auxiliaryDataPosition < nextDataOffset) {
+ nextDataOffset = trackFragment.auxiliaryDataPosition;
+ nextTrackBundle = trackBundles.valueAt(i);
+ }
+ }
+ if (nextTrackBundle == null) {
+ parserState = STATE_READING_SAMPLE_START;
+ return;
+ }
+ int bytesToSkip = (int) (nextDataOffset - input.getPosition());
+ if (bytesToSkip < 0) {
+ throw new ParserException("Offset to encryption data was negative.");
+ }
+ input.skipFully(bytesToSkip);
+ nextTrackBundle.fragment.fillEncryptionData(input);
+ }
+
+ /**
+ * Attempts to read the next sample in the current mdat atom. The read sample may be output or
+ * skipped.
+ *
+ * <p>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.
+ *
+ * <p>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 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.
+ */
+ private boolean readSample(ExtractorInput input) throws IOException, InterruptedException {
+ if (parserState == STATE_READING_SAMPLE_START) {
+ if (currentTrackBundle == null) {
+ TrackBundle currentTrackBundle = getNextFragmentRun(trackBundles);
+ if (currentTrackBundle == null) {
+ // We've run out of samples in the current mdat. Discard any trailing data and prepare to
+ // read the header of the next atom.
+ int bytesToSkip = (int) (endOfMdatPosition - input.getPosition());
+ if (bytesToSkip < 0) {
+ throw new ParserException("Offset to end of mdat was negative.");
+ }
+ input.skipFully(bytesToSkip);
+ enterReadingAtomHeaderState();
+ return false;
+ }
+
+ long nextDataPosition = currentTrackBundle.fragment
+ .trunDataPosition[currentTrackBundle.currentTrackRunIndex];
+ // We skip bytes preceding the next sample to read.
+ int bytesToSkip = (int) (nextDataPosition - input.getPosition());
+ if (bytesToSkip < 0) {
+ // Assume the sample data must be contiguous in the mdat with no preceding data.
+ Log.w(TAG, "Ignoring negative offset to sample data.");
+ bytesToSkip = 0;
+ }
+ input.skipFully(bytesToSkip);
+ this.currentTrackBundle = currentTrackBundle;
+ }
+
+ sampleSize = currentTrackBundle.fragment
+ .sampleSizeTable[currentTrackBundle.currentSampleIndex];
+
+ 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;
+ }
+
+ TrackFragment fragment = currentTrackBundle.fragment;
+ 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.
+ byte[] nalPrefixData = nalPrefix.data;
+ nalPrefixData[0] = 0;
+ nalPrefixData[1] = 0;
+ nalPrefixData[2] = 0;
+ int nalUnitPrefixLength = track.nalUnitLengthFieldLength + 1;
+ int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength;
+ // NAL units are length delimited, but the decoder requires start code delimited units.
+ // Loop until we've written the sample to the track output, replacing length delimiters with
+ // start codes as we encounter them.
+ while (sampleBytesWritten < sampleSize) {
+ if (sampleCurrentNalBytesRemaining == 0) {
+ // 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);
+ 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.length > 0
+ && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]);
+ sampleBytesWritten += 5;
+ sampleSize += nalUnitLengthFieldLengthDiff;
+ } else {
+ int writtenBytes;
+ if (processSeiNalUnitPayload) {
+ // Read and write the payload of the SEI NAL unit.
+ nalBuffer.reset(sampleCurrentNalBytesRemaining);
+ input.readFully(nalBuffer.data, 0, sampleCurrentNalBytesRemaining);
+ output.sampleData(nalBuffer, sampleCurrentNalBytesRemaining);
+ writtenBytes = sampleCurrentNalBytesRemaining;
+ // Unescape and process the SEI NAL unit.
+ int unescapedLength = NalUnitUtil.unescapeStream(nalBuffer.data, nalBuffer.limit());
+ // 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(sampleTimeUs, nalBuffer, cea608TrackOutputs);
+ } else {
+ // Write the payload of the NAL unit.
+ writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false);
+ }
+ sampleBytesWritten += writtenBytes;
+ sampleCurrentNalBytesRemaining -= writtenBytes;
+ }
+ }
+ } else {
+ while (sampleBytesWritten < sampleSize) {
+ int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false);
+ sampleBytesWritten += writtenBytes;
+ }
+ }
+
+ @C.BufferFlags int sampleFlags = fragment.sampleIsSyncFrameTable[sampleIndex]
+ ? C.BUFFER_FLAG_KEY_FRAME : 0;
+
+ // Encryption data.
+ TrackOutput.CryptoData cryptoData = null;
+ TrackEncryptionBox encryptionBox = currentTrackBundle.getEncryptionBoxIfEncrypted();
+ if (encryptionBox != null) {
+ sampleFlags |= C.BUFFER_FLAG_ENCRYPTED;
+ cryptoData = encryptionBox.cryptoData;
+ }
+
+ 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.
+ */
+ private static TrackBundle getNextFragmentRun(SparseArray<TrackBundle> trackBundles) {
+ TrackBundle nextTrackBundle = null;
+ long nextTrackRunOffset = Long.MAX_VALUE;
+
+ int trackBundlesSize = trackBundles.size();
+ for (int i = 0; i < trackBundlesSize; i++) {
+ TrackBundle trackBundle = trackBundles.valueAt(i);
+ if (trackBundle.currentTrackRunIndex == trackBundle.fragment.trunCount) {
+ // This track fragment contains no more runs in the next mdat box.
+ } else {
+ long trunOffset = trackBundle.fragment.trunDataPosition[trackBundle.currentTrackRunIndex];
+ if (trunOffset < nextTrackRunOffset) {
+ nextTrackBundle = trackBundle;
+ nextTrackRunOffset = trunOffset;
+ }
+ }
+ }
+ return nextTrackBundle;
+ }
+
+ /** Returns DrmInitData from leaf atoms. */
+ @Nullable
+ private static DrmInitData getDrmInitDataFromAtoms(List<Atom.LeafAtom> leafChildren) {
+ ArrayList<SchemeData> schemeDatas = null;
+ int leafChildrenSize = leafChildren.size();
+ for (int i = 0; i < leafChildrenSize; i++) {
+ LeafAtom child = leafChildren.get(i);
+ if (child.type == Atom.TYPE_pssh) {
+ if (schemeDatas == null) {
+ schemeDatas = new ArrayList<>();
+ }
+ byte[] psshData = child.data.data;
+ UUID uuid = PsshAtomUtil.parseUuid(psshData);
+ if (uuid == null) {
+ Log.w(TAG, "Skipped pssh atom (failed to extract uuid)");
+ } else {
+ schemeDatas.add(new SchemeData(uuid, MimeTypes.VIDEO_MP4, psshData));
+ }
+ }
+ }
+ return schemeDatas == null ? null : new DrmInitData(schemeDatas);
+ }
+
+ /** Returns whether the extractor should decode a leaf atom with type {@code atom}. */
+ private static boolean shouldParseLeafAtom(int atom) {
+ return atom == Atom.TYPE_hdlr || atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd
+ || atom == Atom.TYPE_sidx || atom == Atom.TYPE_stsd || atom == Atom.TYPE_tfdt
+ || atom == Atom.TYPE_tfhd || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_trex
+ || atom == Atom.TYPE_trun || atom == Atom.TYPE_pssh || atom == Atom.TYPE_saiz
+ || atom == Atom.TYPE_saio || atom == Atom.TYPE_senc || atom == Atom.TYPE_uuid
+ || atom == Atom.TYPE_sbgp || atom == Atom.TYPE_sgpd || atom == Atom.TYPE_elst
+ || atom == Atom.TYPE_mehd || atom == Atom.TYPE_emsg;
+ }
+
+ /** 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_moof
+ || atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex || atom == Atom.TYPE_edts;
+ }
+
+ /**
+ * Holds data corresponding to a metadata sample.
+ */
+ private static final class MetadataSampleInfo {
+
+ public final long presentationTimeDeltaUs;
+ public final int size;
+
+ public MetadataSampleInfo(long presentationTimeDeltaUs, int size) {
+ this.presentationTimeDeltaUs = presentationTimeDeltaUs;
+ this.size = size;
+ }
+
+ }
+
+ /**
+ * Holds data corresponding to a single track.
+ */
+ private static final class TrackBundle {
+
+ 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) {
+ this.output = output;
+ fragment = new TrackFragment();
+ scratch = new ParsableByteArray();
+ encryptionSignalByte = new ParsableByteArray(1);
+ defaultInitializationVector = new ParsableByteArray();
+ }
+
+ public void init(Track track, DefaultSampleValues defaultSampleValues) {
+ this.track = Assertions.checkNotNull(track);
+ this.defaultSampleValues = Assertions.checkNotNull(defaultSampleValues);
+ output.format(track.format);
+ 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;
+ }
+
+ /**
+ * 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java
new file mode 100644
index 0000000000..7040df6425
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<MdtaMetadataEntry> CREATOR =
+ new Parcelable.Creator<MdtaMetadataEntry>() {
+
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
new file mode 100644
index 0000000000..7d4de0e498
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
@@ -0,0 +1,588 @@
+/*
+ * 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.mp4;
+
+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 java.nio.ByteBuffer;
+
+/** 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 = 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 = 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 = 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 = 0x2d2d2d2d;
+
+ private static final int PICTURE_TYPE_FRONT_COVER = 3;
+
+ // Standard genres.
+ @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() {}
+
+ /**
+ * 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 == TYPE_TOP_BYTE_COPYRIGHT || typeTopByte == TYPE_TOP_BYTE_REPLACEMENT) {
+ int shortType = type & 0x00FFFFFF;
+ if (shortType == SHORT_TYPE_COMMENT) {
+ return parseCommentAttribute(type, ilst);
+ } else if (shortType == SHORT_TYPE_NAME_1 || shortType == SHORT_TYPE_NAME_2) {
+ return parseTextAttribute(type, "TIT2", ilst);
+ } else if (shortType == SHORT_TYPE_COMPOSER_1 || shortType == SHORT_TYPE_COMPOSER_2) {
+ return parseTextAttribute(type, "TCOM", ilst);
+ } else if (shortType == SHORT_TYPE_YEAR) {
+ return parseTextAttribute(type, "TDRC", ilst);
+ } else if (shortType == SHORT_TYPE_ARTIST) {
+ return parseTextAttribute(type, "TPE1", ilst);
+ } else if (shortType == SHORT_TYPE_ENCODER) {
+ return parseTextAttribute(type, "TSSE", ilst);
+ } else if (shortType == SHORT_TYPE_ALBUM) {
+ return parseTextAttribute(type, "TALB", ilst);
+ } else if (shortType == SHORT_TYPE_LYRICS) {
+ return parseTextAttribute(type, "USLT", ilst);
+ } else if (shortType == SHORT_TYPE_GENRE) {
+ return parseTextAttribute(type, "TCON", ilst);
+ } else if (shortType == TYPE_GROUPING) {
+ return parseTextAttribute(type, "TIT1", ilst);
+ }
+ } else if (type == TYPE_GENRE) {
+ return parseStandardGenreAttribute(ilst);
+ } else if (type == TYPE_DISK_NUMBER) {
+ return parseIndexAndCountAttribute(type, "TPOS", ilst);
+ } else if (type == TYPE_TRACK_NUMBER) {
+ return parseIndexAndCountAttribute(type, "TRCK", ilst);
+ } else if (type == TYPE_TEMPO) {
+ return parseUint8Attribute(type, "TBPM", ilst, true, false);
+ } else if (type == TYPE_COMPILATION) {
+ return parseUint8Attribute(type, "TCMP", ilst, true, true);
+ } else if (type == TYPE_COVER_ART) {
+ return parseCoverArt(ilst);
+ } else if (type == TYPE_ALBUM_ARTIST) {
+ return parseTextAttribute(type, "TPE2", ilst);
+ } else if (type == TYPE_SORT_TRACK_NAME) {
+ return parseTextAttribute(type, "TSOT", ilst);
+ } else if (type == TYPE_SORT_ALBUM) {
+ return parseTextAttribute(type, "TSO2", ilst);
+ } else if (type == TYPE_SORT_ARTIST) {
+ return parseTextAttribute(type, "TSOA", ilst);
+ } else if (type == TYPE_SORT_ALBUM_ARTIST) {
+ return parseTextAttribute(type, "TSOP", ilst);
+ } else if (type == TYPE_SORT_COMPOSER) {
+ return parseTextAttribute(type, "TSOC", ilst);
+ } else if (type == TYPE_RATING) {
+ return parseUint8Attribute(type, "ITUNESADVISORY", ilst, false, false);
+ } else if (type == TYPE_GAPLESS_ALBUM) {
+ return parseUint8Attribute(type, "ITUNESGAPLESS", ilst, false, true);
+ } else if (type == TYPE_TV_SORT_SHOW) {
+ return parseTextAttribute(type, "TVSHOWSORT", ilst);
+ } else if (type == TYPE_TV_SHOW) {
+ return parseTextAttribute(type, "TVSHOW", ilst);
+ } else if (type == TYPE_INTERNAL) {
+ return parseInternalAttribute(ilst, endPosition);
+ }
+ Log.d(TAG, "Skipped unknown metadata entry: " + Atom.getAtomTypeString(type));
+ return null;
+ } finally {
+ ilst.setPosition(endPosition);
+ }
+ }
+
+ /**
+ * 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, /* 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();
+ if (atomType == Atom.TYPE_data) {
+ data.skipBytes(8); // version (1), flags (3), empty (4)
+ String value = data.readNullTerminatedString(atomSize - 16);
+ return new CommentFrame(LANGUAGE_UNDEFINED, value, value);
+ }
+ Log.w(TAG, "Failed to parse comment attribute: " + Atom.getAtomTypeString(type));
+ return null;
+ }
+
+ @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, /* 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;
+ }
+
+ @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) {
+ data.skipBytes(10); // version (1), flags (3), empty (4), empty (2)
+ int index = data.readUnsignedShort();
+ if (index > 0) {
+ String value = "" + index;
+ int count = data.readUnsignedShort();
+ if (count > 0) {
+ value += "/" + count;
+ }
+ 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", /* 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();
+ if (atomType == Atom.TYPE_data) {
+ int fullVersionInt = data.readInt();
+ int flags = Atom.parseFullAtomFlags(fullVersionInt);
+ String mimeType = flags == 13 ? "image/jpeg" : flags == 14 ? "image/png" : null;
+ if (mimeType == null) {
+ Log.w(TAG, "Unrecognized cover art flags: " + flags);
+ return null;
+ }
+ data.skipBytes(4); // empty (4)
+ byte[] pictureData = new byte[atomSize - 16];
+ data.readBytes(pictureData, 0, pictureData.length);
+ 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;
+ int dataAtomPosition = -1;
+ int dataAtomSize = -1;
+ while (data.getPosition() < endPosition) {
+ int atomPosition = data.getPosition();
+ int atomSize = data.readInt();
+ int atomType = data.readInt();
+ data.skipBytes(4); // version (1), flags (3)
+ if (atomType == Atom.TYPE_mean) {
+ domain = data.readNullTerminatedString(atomSize - 12);
+ } else if (atomType == Atom.TYPE_name) {
+ name = data.readNullTerminatedString(atomSize - 12);
+ } else {
+ if (atomType == Atom.TYPE_data) {
+ dataAtomPosition = atomPosition;
+ dataAtomSize = atomSize;
+ }
+ data.skipBytes(atomSize - 12);
+ }
+ }
+ 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 InternalFrame(domain, name, value);
+ }
+
+ private static int parseUint8AttributeValue(ParsableByteArray data) {
+ data.skipBytes(4); // atomSize
+ int atomType = data.readInt();
+ if (atomType == Atom.TYPE_data) {
+ data.skipBytes(8); // version (1), flags (3), empty (4)
+ return data.readUnsignedByte();
+ }
+ Log.w(TAG, "Failed to parse uint8 attribute value");
+ return -1;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
new file mode 100644
index 0000000000..254cad1eb1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
@@ -0,0 +1,824 @@
+/*
+ * 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.mp4;
+
+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;
+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.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 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;
+
+/**
+ * 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 Extractor[] {new Mp4Extractor()};
+
+ /**
+ * 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 = 0x71742020;
+
+ /**
+ * When seeking within the source, if the offset is greater than or equal to this value (or the
+ * offset is negative), the source will be reloaded.
+ */
+ 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 ArrayDeque<ContainerAtom> containerAtoms;
+
+ @State private int parserState;
+ private int atomType;
+ private long atomSize;
+ 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 ArrayDeque<>();
+ nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
+ nalLength = new ParsableByteArray(4);
+ scratch = new ParsableByteArray();
+ sampleTrackIndex = C.INDEX_UNSET;
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ return Sniffer.sniffUnfragmented(input);
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ extractorOutput = output;
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ containerAtoms.clear();
+ atomHeaderBytesRead = 0;
+ sampleTrackIndex = C.INDEX_UNSET;
+ sampleBytesRead = 0;
+ sampleBytesWritten = 0;
+ sampleCurrentNalBytesRemaining = 0;
+ if (position == 0) {
+ enterReadingAtomHeaderState();
+ } else if (tracks != null) {
+ updateSampleIndices(timeUs);
+ }
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ while (true) {
+ switch (parserState) {
+ case STATE_READING_ATOM_HEADER:
+ if (!readAtomHeader(input)) {
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_READING_ATOM_PAYLOAD:
+ if (readAtomPayload(input, seekPosition)) {
+ return RESULT_SEEK;
+ }
+ break;
+ case STATE_READING_SAMPLE:
+ return readSample(input, seekPosition);
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ // SeekMap implementation.
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ 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) {
+ return new SeekPoints(SeekPoint.START);
+ }
+ 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);
+ }
+ }
+ }
+
+ 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.
+
+ private void enterReadingAtomHeaderState() {
+ parserState = STATE_READING_ATOM_HEADER;
+ atomHeaderBytesRead = 0;
+ }
+
+ private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException {
+ if (atomHeaderBytesRead == 0) {
+ // Read the standard length atom header.
+ if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) {
+ return false;
+ }
+ atomHeaderBytesRead = Atom.HEADER_SIZE;
+ atomHeader.setPosition(0);
+ atomSize = atomHeader.readUnsignedInt();
+ atomType = atomHeader.readInt();
+ }
+
+ 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;
+ if (atomSize != atomHeaderBytesRead && atomType == Atom.TYPE_meta) {
+ maybeSkipRemainingMetaAtomHeaderBytes(input);
+ }
+ containerAtoms.push(new ContainerAtom(atomType, endPosition));
+ if (atomSize == atomHeaderBytesRead) {
+ processAtomEnded(endPosition);
+ } else {
+ // Start reading the first child atom.
+ enterReadingAtomHeaderState();
+ }
+ } else if (shouldParseLeafAtom(atomType)) {
+ // We don't support parsing of leaf atoms that define extended atom sizes, or that have
+ // lengths greater than Integer.MAX_VALUE.
+ Assertions.checkState(atomHeaderBytesRead == Atom.HEADER_SIZE);
+ Assertions.checkState(atomSize <= Integer.MAX_VALUE);
+ atomData = new ParsableByteArray((int) atomSize);
+ System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE);
+ parserState = STATE_READING_ATOM_PAYLOAD;
+ } else {
+ atomData = null;
+ parserState = STATE_READING_ATOM_PAYLOAD;
+ }
+
+ return true;
+ }
+
+ /**
+ * Processes the atom payload. If {@link #atomData} is null and the size is at or above the
+ * threshold {@link #RELOAD_MINIMUM_SEEK_DISTANCE}, {@code true} is returned and the caller should
+ * restart loading at the position in {@code positionHolder}. Otherwise, the atom is read/skipped.
+ */
+ private boolean readAtomPayload(ExtractorInput input, PositionHolder positionHolder)
+ throws IOException, InterruptedException {
+ long atomPayloadSize = atomSize - atomHeaderBytesRead;
+ long atomEndPosition = input.getPosition() + atomPayloadSize;
+ boolean seekRequired = false;
+ if (atomData != null) {
+ input.readFully(atomData.data, atomHeaderBytesRead, (int) atomPayloadSize);
+ if (atomType == Atom.TYPE_ftyp) {
+ isQuickTime = processFtypAtom(atomData);
+ } else if (!containerAtoms.isEmpty()) {
+ containerAtoms.peek().add(new Atom.LeafAtom(atomType, atomData));
+ }
+ } else {
+ // We don't need the data. Skip or seek, depending on how large the atom is.
+ if (atomPayloadSize < RELOAD_MINIMUM_SEEK_DISTANCE) {
+ input.skipFully((int) atomPayloadSize);
+ } else {
+ positionHolder.position = input.getPosition() + atomPayloadSize;
+ seekRequired = true;
+ }
+ }
+ processAtomEnded(atomEndPosition);
+ return seekRequired && parserState != STATE_READING_SAMPLE;
+ }
+
+ private void processAtomEnded(long atomEndPosition) throws ParserException {
+ while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) {
+ Atom.ContainerAtom containerAtom = containerAtoms.pop();
+ if (containerAtom.type == Atom.TYPE_moov) {
+ // We've reached the end of the moov atom. Process it and prepare to read samples.
+ processMoovAtom(containerAtom);
+ containerAtoms.clear();
+ parserState = STATE_READING_SAMPLE;
+ } else if (!containerAtoms.isEmpty()) {
+ containerAtoms.peek().add(containerAtom);
+ }
+ }
+ if (parserState != STATE_READING_SAMPLE) {
+ enterReadingAtomHeaderState();
+ }
+ }
+
+ /**
+ * 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<Mp4Track> tracks = new ArrayList<>();
+
+ // Process metadata.
+ Metadata udtaMetadata = null;
+ GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
+ Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
+ if (udta != null) {
+ 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<TrackSampleTable> 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<TrackSampleTable> getTrackSampleTables(
+ ContainerAtom moov, GaplessInfoHolder gaplessInfoHolder, boolean ignoreEditLists)
+ throws ParserException {
+ ArrayList<TrackSampleTable> 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),
+ /* 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);
+ TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);
+ if (trackSampleTable.sampleCount == 0) {
+ continue;
+ }
+ trackSampleTables.add(trackSampleTable);
+ }
+ return trackSampleTables;
+ }
+
+ /**
+ * Attempts to extract the next sample in the current mdat atom for the specified track.
+ * <p>
+ * Returns {@link #RESULT_SEEK} if the source should be reloaded from the position in
+ * {@code positionHolder}.
+ * <p>
+ * Returns {@link #RESULT_END_OF_INPUT} if no samples are left. Otherwise, returns
+ * {@link #RESULT_CONTINUE}.
+ *
+ * @param input The {@link ExtractorInput} from which to read data.
+ * @param positionHolder If {@link #RESULT_SEEK} is returned, this holder is updated to hold the
+ * position of the required data.
+ * @return One of the {@code RESULT_*} flags in {@link Extractor}.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private int readSample(ExtractorInput input, PositionHolder positionHolder)
+ throws IOException, InterruptedException {
+ 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[sampleTrackIndex];
+ TrackOutput trackOutput = track.trackOutput;
+ int sampleIndex = track.sampleIndex;
+ long position = track.sampleTable.offsets[sampleIndex];
+ int sampleSize = track.sampleTable.sizes[sampleIndex];
+ 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
+ // they're only 1 or 2 bytes long.
+ byte[] nalLengthData = nalLength.data;
+ nalLengthData[0] = 0;
+ nalLengthData[1] = 0;
+ nalLengthData[2] = 0;
+ int nalUnitLengthFieldLength = track.track.nalUnitLengthFieldLength;
+ int nalUnitLengthFieldLengthDiff = 4 - track.track.nalUnitLengthFieldLength;
+ // NAL units are length delimited, but the decoder requires start code delimited units.
+ // Loop until we've written the sample to the track output, replacing length delimiters with
+ // start codes as we encounter them.
+ while (sampleBytesWritten < sampleSize) {
+ if (sampleCurrentNalBytesRemaining == 0) {
+ // Read the NAL length so that we know where we find the next one.
+ input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);
+ sampleBytesRead += nalUnitLengthFieldLength;
+ nalLength.setPosition(0);
+ 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);
+ sampleBytesWritten += 4;
+ sampleSize += nalUnitLengthFieldLengthDiff;
+ } 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;
+ }
+ }
+ 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 next sample to be read, or {@link
+ * C#INDEX_UNSET} if no samples remain.
+ *
+ * <p>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.
+ *
+ * <p>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 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 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 minAccumulatedBytes == Long.MAX_VALUE
+ || !minAccumulatedBytesRequiresReload
+ || preferredAccumulatedBytes < minAccumulatedBytes + MAXIMUM_READ_AHEAD_BYTES_STREAM
+ ? preferredTrackIndex
+ : minAccumulatedBytesTrackIndex;
+ }
+
+ /**
+ * Updates every track's sample index to point its latest sync sample before/at {@code timeUs}.
+ */
+ private void updateSampleIndices(long timeUs) {
+ for (Mp4Track track : tracks) {
+ TrackSampleTable sampleTable = track.sampleTable;
+ 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);
+ }
+ track.sampleIndex = sampleIndex;
+ }
+ }
+
+ /**
+ * Possibly skips the version and flags fields (1+3 byte) of a full meta atom of the {@code
+ * input}.
+ *
+ * <p>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 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);
+ }
+ }
+
+ /**
+ * 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
+ || atom == Atom.TYPE_meta;
+ }
+
+ private static final class Mp4Track {
+
+ public final Track track;
+ public final TrackSampleTable sampleTable;
+ public final TrackOutput trackOutput;
+
+ public int sampleIndex;
+
+ public Mp4Track(Track track, TrackSampleTable sampleTable, TrackOutput trackOutput) {
+ this.track = track;
+ this.sampleTable = sampleTable;
+ this.trackOutput = trackOutput;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java
new file mode 100644
index 0000000000..ddb13aeb9c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java
@@ -0,0 +1,208 @@
+/*
+ * 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.mp4;
+
+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;
+
+/**
+ * Utility methods for handling PSSH atoms.
+ */
+public final class PsshAtomUtil {
+
+ private static final String TAG = "PsshAtomUtil";
+
+ private PsshAtomUtil() {}
+
+ /**
+ * Builds a version 0 PSSH atom for a given system id, containing the given data.
+ *
+ * @param systemId The system id of the scheme.
+ * @param data The scheme specific data.
+ * @return The PSSH atom.
+ */
+ 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(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.
+ *
+ * <p>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.
+ */
+ public static @Nullable UUID parseUuid(byte[] atom) {
+ PsshAtom parsedAtom = parsePsshAtom(atom);
+ if (parsedAtom == null) {
+ return null;
+ }
+ return parsedAtom.uuid;
+ }
+
+ /**
+ * Parses the version from a PSSH atom. Version 0 and 1 PSSH atoms are supported.
+ * <p>
+ * 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.
+ *
+ * <p>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.
+ * @param uuid The required UUID of the PSSH atom, or null to accept any UUID.
+ * @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 @Nullable byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) {
+ PsshAtom parsedAtom = parsePsshAtom(atom);
+ if (parsedAtom == null) {
+ return null;
+ }
+ if (uuid != null && !uuid.equals(parsedAtom.uuid)) {
+ Log.w(TAG, "UUID mismatch. Expected: " + uuid + ", got: " + parsedAtom.uuid + ".");
+ return null;
+ }
+ return parsedAtom.schemeData;
+ }
+
+ /**
+ * Parses a PSSH atom. Version 0 and 1 PSSH atoms are supported.
+ *
+ * @param atom The atom to parse.
+ * @return The parsed PSSH atom. Null if the input is not a valid PSSH atom, or if the PSSH atom
+ * has an unsupported version.
+ */
+ // 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.
+ return null;
+ }
+ atomData.setPosition(0);
+ int atomSize = atomData.readInt();
+ if (atomSize != atomData.bytesLeft() + 4) {
+ // Not an atom, or incorrect atom size.
+ return null;
+ }
+ int atomType = atomData.readInt();
+ if (atomType != Atom.TYPE_pssh) {
+ // Not an atom, or incorrect atom type.
+ return null;
+ }
+ int atomVersion = Atom.parseFullAtomVersion(atomData.readInt());
+ if (atomVersion > 1) {
+ Log.w(TAG, "Unsupported pssh version: " + atomVersion);
+ return null;
+ }
+ UUID uuid = new UUID(atomData.readLong(), atomData.readLong());
+ if (atomVersion == 1) {
+ int keyIdCount = atomData.readUnsignedIntToInt();
+ atomData.skipBytes(16 * keyIdCount);
+ }
+ int dataSize = atomData.readUnsignedIntToInt();
+ if (dataSize != atomData.bytesLeft()) {
+ // Incorrect dataSize.
+ return null;
+ }
+ byte[] data = new byte[dataSize];
+ atomData.readBytes(data, 0, dataSize);
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java
new file mode 100644
index 0000000000..d58c2f06eb
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java
@@ -0,0 +1,201 @@
+/*
+ * 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.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 java.io.IOException;
+
+/**
+ * Provides methods that peek data from an {@link ExtractorInput} and return whether the input
+ * appears to be in MP4 format.
+ */
+/* package */ final class Sniffer {
+
+ /** 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[] {
+ 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
+ * input being a fragmented MP4 file.
+ *
+ * @param input The extractor input from which to peek data. The peek position will be modified.
+ * @return Whether the input appears to be in the fragmented MP4 format.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ public static boolean sniffFragmented(ExtractorInput input)
+ throws IOException, InterruptedException {
+ return sniffInternal(input, true);
+ }
+
+ /**
+ * Returns whether data peeked from the current position in {@code input} is consistent with the
+ * input being an unfragmented MP4 file.
+ *
+ * @param input The extractor input from which to peek data. The peek position will be modified.
+ * @return Whether the input appears to be in the unfragmented MP4 format.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ public static boolean sniffUnfragmented(ExtractorInput input)
+ throws IOException, InterruptedException {
+ return sniffInternal(input, false);
+ }
+
+ private static boolean sniffInternal(ExtractorInput input, boolean fragmented)
+ throws IOException, InterruptedException {
+ long inputLength = input.getLength();
+ int bytesToSearch = (int) (inputLength == C.LENGTH_UNSET || inputLength > SEARCH_LENGTH
+ ? SEARCH_LENGTH : inputLength);
+
+ ParsableByteArray buffer = new ParsableByteArray(64);
+ int bytesSearched = 0;
+ boolean foundGoodFileType = false;
+ boolean isFragmented = false;
+ while (bytesSearched < bytesToSearch) {
+ // Read an atom header.
+ int headerSize = Atom.HEADER_SIZE;
+ buffer.reset(headerSize);
+ input.peekFully(buffer.data, 0, headerSize);
+ long atomSize = buffer.readUnsignedInt();
+ int atomType = buffer.readInt();
+ 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.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) {
+ // The file is invalid because the atom size is too small for its header.
+ return false;
+ }
+ 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;
+ }
+
+ if (atomType == Atom.TYPE_moof || atomType == Atom.TYPE_mvex) {
+ // The movie is fragmented. Stop searching as we must have read any ftyp atom already.
+ isFragmented = true;
+ break;
+ }
+
+ if (bytesSearched + atomSize - headerSize >= bytesToSearch) {
+ // Stop searching as peeking this atom would exceed the search limit.
+ break;
+ }
+
+ int atomDataSize = (int) (atomSize - headerSize);
+ bytesSearched += atomDataSize;
+ if (atomType == Atom.TYPE_ftyp) {
+ // Parse the atom and check the file type/brand is compatible with the extractors.
+ if (atomDataSize < 8) {
+ return false;
+ }
+ buffer.reset(atomDataSize);
+ input.peekFully(buffer.data, 0, atomDataSize);
+ int brandsCount = atomDataSize / 4;
+ for (int i = 0; i < brandsCount; i++) {
+ if (i == 1) {
+ // This index refers to the minorVersion, not a brand, so skip it.
+ buffer.skipBytes(4);
+ } else if (isCompatibleBrand(buffer.readInt())) {
+ foundGoodFileType = true;
+ break;
+ }
+ }
+ if (!foundGoodFileType) {
+ // The types were not compatible and there is only one ftyp atom, so reject the file.
+ return false;
+ }
+ } else if (atomDataSize != 0) {
+ // Skip the atom.
+ input.advancePeekPosition(atomDataSize);
+ }
+ }
+ return foundGoodFileType && fragmented == isFragmented;
+ }
+
+ /**
+ * Returns whether {@code brand} is an ftyp atom brand that is compatible with the MP4 extractors.
+ */
+ private static boolean isCompatibleBrand(int brand) {
+ // Accept all brands starting '3gp'.
+ if (brand >>> 8 == 0x00336770) {
+ return true;
+ }
+ for (int compatibleBrand : COMPATIBLE_BRANDS) {
+ if (compatibleBrand == brand) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private Sniffer() {
+ // Prevent instantiation.
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java
new file mode 100644
index 0000000000..b7a1555a76
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java
@@ -0,0 +1,148 @@
+/*
+ * 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.mp4;
+
+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;
+
+/**
+ * Encapsulates information describing an MP4 track.
+ */
+public final class Track {
+
+ /**
+ * 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 {}
+ /**
+ * A no-op sample transformation.
+ */
+ public static final int TRANSFORMATION_NONE = 0;
+ /**
+ * A transformation for caption samples in cdat atoms.
+ */
+ public static final int TRANSFORMATION_CEA608_CDAT = 1;
+
+ /**
+ * The track identifier.
+ */
+ public final int id;
+
+ /**
+ * One of {@link C#TRACK_TYPE_AUDIO}, {@link C#TRACK_TYPE_VIDEO} and {@link C#TRACK_TYPE_TEXT}.
+ */
+ public final int type;
+
+ /**
+ * The track timescale, defined as the number of time units that pass in one second.
+ */
+ public final long timescale;
+
+ /**
+ * The movie timescale.
+ */
+ public final long movieTimescale;
+
+ /**
+ * The duration of the track in microseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public final long durationUs;
+
+ /**
+ * The format.
+ */
+ public final Format format;
+
+ /**
+ * One of {@code TRANSFORMATION_*}. Defines the transformation to apply before outputting each
+ * sample.
+ */
+ @Transformation public final int sampleTransformation;
+
+ /**
+ * Durations of edit list segments in the movie timescale. Null if there is no edit list.
+ */
+ @Nullable public final long[] editListDurations;
+
+ /**
+ * Media times for edit list segments in the track timescale. Null if there is no edit list.
+ */
+ @Nullable public final long[] editListMediaTimes;
+
+ /**
+ * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. 0 for
+ * other track types.
+ */
+ 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,
+ @Nullable TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength,
+ @Nullable long[] editListDurations, @Nullable long[] editListMediaTimes) {
+ this.id = id;
+ this.type = type;
+ this.timescale = timescale;
+ this.movieTimescale = movieTimescale;
+ this.durationUs = durationUs;
+ this.format = format;
+ this.sampleTransformation = sampleTransformation;
+ this.sampleDescriptionEncryptionBoxes = sampleDescriptionEncryptionBoxes;
+ this.nalUnitLengthFieldLength = nalUnitLengthFieldLength;
+ this.editListDurations = editListDurations;
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java
new file mode 100644
index 0000000000..04bfb82210
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java
@@ -0,0 +1,103 @@
+/*
+ * 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.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
+ * (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 protection scheme type, as defined by the 'schm' box, or null if unknown.
+ */
+ @Nullable public final String schemeType;
+
+ /**
+ * A {@link TrackOutput.CryptoData} instance containing the encryption information from this
+ * {@link TrackEncryptionBox}.
+ */
+ public final TrackOutput.CryptoData cryptoData;
+
+ /** The initialization vector size in bytes for the samples in the corresponding sample group. */
+ public final int perSampleIvSize;
+
+ /**
+ * If {@link #perSampleIvSize} is 0, holds the default initialization vector as defined in the
+ * track encryption box or sample group description box. Null otherwise.
+ */
+ @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.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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java
new file mode 100644
index 0000000000..e027d6ed76
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java
@@ -0,0 +1,197 @@
+/*
+ * 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.mp4;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * A holder for information corresponding to a single fragment of an mp4 file.
+ */
+/* package */ final class TrackFragment {
+
+ /**
+ * The default values for samples from the track fragment header.
+ */
+ public DefaultSampleValues header;
+ /**
+ * The position (byte offset) of the start of fragment.
+ */
+ public long atomPosition;
+ /**
+ * The position (byte offset) of the start of data contained in the fragment.
+ */
+ public long dataPosition;
+ /**
+ * The position (byte offset) of the start of auxiliary data.
+ */
+ public long auxiliaryDataPosition;
+ /**
+ * The number of track runs of the fragment.
+ */
+ public int trunCount;
+ /**
+ * The total number of samples in the fragment.
+ */
+ public int sampleCount;
+ /**
+ * The position (byte offset) of the start of sample data of each track run in the fragment.
+ */
+ public long[] trunDataPosition;
+ /**
+ * The number of samples contained by each track run in the fragment.
+ */
+ public int[] trunLength;
+ /**
+ * The size of each sample in the fragment.
+ */
+ public int[] sampleSizeTable;
+ /**
+ * The composition time offset of each sample in the fragment.
+ */
+ public int[] sampleCompositionTimeOffsetTable;
+ /**
+ * The decoding time of each sample in the fragment.
+ */
+ public long[] sampleDecodingTimeTable;
+ /**
+ * Indicates which samples are sync frames.
+ */
+ public boolean[] sampleIsSyncFrameTable;
+ /**
+ * Whether the fragment defines encryption data.
+ */
+ public boolean definesEncryptionData;
+ /**
+ * If {@link #definesEncryptionData} is true, indicates which samples use sub-sample encryption.
+ * Undefined otherwise.
+ */
+ public boolean[] sampleHasSubsampleEncryptionTable;
+ /**
+ * Fragment specific track encryption. May be null.
+ */
+ public TrackEncryptionBox trackEncryptionBox;
+ /**
+ * If {@link #definesEncryptionData} is true, indicates the length of the sample encryption data.
+ * Undefined otherwise.
+ */
+ public int sampleEncryptionDataLength;
+ /**
+ * If {@link #definesEncryptionData} is true, contains binary sample encryption data. Undefined
+ * otherwise.
+ */
+ public ParsableByteArray sampleEncryptionData;
+ /**
+ * Whether {@link #sampleEncryptionData} needs populating with the actual encryption data.
+ */
+ public boolean sampleEncryptionDataNeedsFill;
+ /**
+ * The absolute decode time of the start of the next fragment.
+ */
+ public long nextFragmentDecodeTime;
+
+ /**
+ * Resets the fragment.
+ * <p>
+ * {@link #sampleCount} and {@link #nextFragmentDecodeTime} are set to 0, and both
+ * {@link #definesEncryptionData} and {@link #sampleEncryptionDataNeedsFill} is set to false,
+ * and {@link #trackEncryptionBox} is set to null.
+ */
+ public void reset() {
+ trunCount = 0;
+ nextFragmentDecodeTime = 0;
+ definesEncryptionData = false;
+ sampleEncryptionDataNeedsFill = false;
+ trackEncryptionBox = null;
+ }
+
+ /**
+ * Configures the fragment for the specified number of samples.
+ * <p>
+ * The {@link #sampleCount} of the fragment is set to the specified sample count, and the
+ * contained tables are resized if necessary such that they are at least this length.
+ *
+ * @param sampleCount The number of samples in the new run.
+ */
+ public void initTables(int trunCount, int sampleCount) {
+ this.trunCount = trunCount;
+ this.sampleCount = sampleCount;
+ if (trunLength == null || trunLength.length < trunCount) {
+ trunDataPosition = new long[trunCount];
+ trunLength = new int[trunCount];
+ }
+ if (sampleSizeTable == null || sampleSizeTable.length < sampleCount) {
+ // Size the tables 25% larger than needed, so as to make future resize operations less
+ // likely. The choice of 25% is relatively arbitrary.
+ int tableSize = (sampleCount * 125) / 100;
+ sampleSizeTable = new int[tableSize];
+ sampleCompositionTimeOffsetTable = new int[tableSize];
+ sampleDecodingTimeTable = new long[tableSize];
+ sampleIsSyncFrameTable = new boolean[tableSize];
+ sampleHasSubsampleEncryptionTable = new boolean[tableSize];
+ }
+ }
+
+ /**
+ * Configures the fragment to be one that defines encryption data of the specified length.
+ * <p>
+ * {@link #definesEncryptionData} is set to true, {@link #sampleEncryptionDataLength} is set to
+ * the specified length, and {@link #sampleEncryptionData} is resized if necessary such that it
+ * is at least this length.
+ *
+ * @param length The length in bytes of the encryption data.
+ */
+ public void initEncryptionData(int length) {
+ if (sampleEncryptionData == null || sampleEncryptionData.limit() < length) {
+ sampleEncryptionData = new ParsableByteArray(length);
+ }
+ sampleEncryptionDataLength = length;
+ definesEncryptionData = true;
+ sampleEncryptionDataNeedsFill = true;
+ }
+
+ /**
+ * Fills {@link #sampleEncryptionData} from the provided input.
+ *
+ * @param input An {@link ExtractorInput} from which to read the encryption data.
+ */
+ public void fillEncryptionData(ExtractorInput input) throws IOException, InterruptedException {
+ input.readFully(sampleEncryptionData.data, 0, sampleEncryptionDataLength);
+ sampleEncryptionData.setPosition(0);
+ sampleEncryptionDataNeedsFill = false;
+ }
+
+ /**
+ * Fills {@link #sampleEncryptionData} from the provided source.
+ *
+ * @param source A source from which to read the encryption data.
+ */
+ public void fillEncryptionData(ParsableByteArray source) {
+ source.readBytes(sampleEncryptionData.data, 0, sampleEncryptionDataLength);
+ sampleEncryptionData.setPosition(0);
+ sampleEncryptionDataNeedsFill = false;
+ }
+
+ public long getSamplePresentationTime(int index) {
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
new file mode 100644
index 0000000000..bb9891b302
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
@@ -0,0 +1,108 @@
+/*
+ * 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.mp4;
+
+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;
+
+/**
+ * Sample table for a track in an MP4 file.
+ */
+/* package */ final class TrackSampleTable {
+
+ /** The track corresponding to this sample table. */
+ public final Track track;
+ /** Number of samples. */
+ public final int sampleCount;
+ /** Sample offsets in bytes. */
+ public final long[] offsets;
+ /** Sample sizes in bytes. */
+ public final int[] sizes;
+ /** Maximum sample size in {@link #sizes}. */
+ public final int maximumSize;
+ /** Sample timestamps in microseconds. */
+ public final long[] timestampsUs;
+ /** 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(
+ 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;
+ }
+ }
+
+ /**
+ * Returns the sample index of the closest synchronization sample at or before the given
+ * timestamp, if one is available.
+ *
+ * @param timeUs Timestamp adjacent to which to find a synchronization sample.
+ * @return Index of the synchronization sample, or {@link C#INDEX_UNSET} if none.
+ */
+ public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) {
+ // Video frame timestamps may not be sorted, so the behavior of this call can be undefined.
+ // Frames are not reordered past synchronization samples so this works in practice.
+ int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false);
+ for (int i = startIndex; i >= 0; i--) {
+ if ((flags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ /**
+ * Returns the sample index of the closest synchronization sample at or after the given timestamp,
+ * if one is available.
+ *
+ * @param timeUs Timestamp adjacent to which to find a synchronization sample.
+ * @return index Index of the synchronization sample, or {@link C#INDEX_UNSET} if none.
+ */
+ public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) {
+ int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false);
+ for (int i = startIndex; i < timestampsUs.length; i++) {
+ if ((flags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java
new file mode 100644
index 0000000000..5d3b27e294
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java
@@ -0,0 +1,313 @@
+/*
+ * 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.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;
+
+/** Seeks in an Ogg stream. */
+/* package */ final class DefaultOggSeeker implements OggSeeker {
+
+ 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_SKIP = 3;
+ private static final int STATE_IDLE = 4;
+
+ private final OggPageHeader pageHeader = new OggPageHeader();
+ private final long payloadStartPosition;
+ private final long payloadEndPosition;
+ private final StreamReader streamReader;
+
+ private int state;
+ private long totalGranules;
+ private long positionBeforeSeekToEnd;
+ private long targetGranule;
+
+ private long start;
+ private long end;
+ private long startGranule;
+ private long endGranule;
+
+ /**
+ * Constructs an 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(
+ StreamReader streamReader,
+ long payloadStartPosition,
+ long payloadEndPosition,
+ long firstPayloadPageSize,
+ long firstPayloadPageGranulePosition,
+ boolean firstPayloadPageIsLastPage) {
+ Assertions.checkArgument(
+ payloadStartPosition >= 0 && payloadEndPosition > payloadStartPosition);
+ this.streamReader = streamReader;
+ this.payloadStartPosition = payloadStartPosition;
+ this.payloadEndPosition = payloadEndPosition;
+ if (firstPayloadPageSize == payloadEndPosition - payloadStartPosition
+ || firstPayloadPageIsLastPage) {
+ totalGranules = firstPayloadPageGranulePosition;
+ state = STATE_IDLE;
+ } else {
+ state = STATE_SEEK_TO_END;
+ }
+ }
+
+ @Override
+ @SuppressWarnings("fallthrough")
+ public long read(ExtractorInput input) throws IOException, InterruptedException {
+ switch (state) {
+ case STATE_IDLE:
+ return -1;
+ case STATE_SEEK_TO_END:
+ 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 = payloadEndPosition - OggPageHeader.MAX_PAGE_SIZE;
+ if (lastPageSearchPosition > positionBeforeSeekToEnd) {
+ return lastPageSearchPosition;
+ }
+ // Fall through.
+ case STATE_READ_LAST_PAGE:
+ totalGranules = readGranuleOfLastPage(input);
+ state = STATE_IDLE;
+ return positionBeforeSeekToEnd;
+ case STATE_SEEK:
+ 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 -(startGranule + 2);
+ default:
+ // Never happens.
+ throw new IllegalStateException();
+ }
+ }
+
+ @Override
+ public OggSeekMap createSeekMap() {
+ return totalGranules != 0 ? new OggSeekMap() : null;
+ }
+
+ @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;
+ }
+
+ /**
+ * 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 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.
+ */
+ private long getNextSeekPosition(ExtractorInput input) throws IOException, InterruptedException {
+ if (start == end) {
+ return C.POSITION_UNSET;
+ }
+
+ long currentPosition = input.getPosition();
+ if (!skipToNextPage(input, end)) {
+ if (start == currentPosition) {
+ throw new IOException("No ogg page can be found.");
+ }
+ return start;
+ }
+
+ pageHeader.populate(input, /* quiet= */ false);
+ input.resetPeekPosition();
+
+ long granuleDistance = targetGranule - pageHeader.granulePosition;
+ int pageSize = pageHeader.headerSize + pageHeader.bodySize;
+ if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) {
+ return C.POSITION_UNSET;
+ }
+
+ 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);
+ }
+
+ /**
+ * 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);
+ }
+ input.resetPeekPosition();
+ }
+
+ /**
+ * Skips to the next page.
+ *
+ * @param input The {@code ExtractorInput} to skip to the next page.
+ * @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
+ void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException {
+ if (!skipToNextPage(input, payloadEndPosition)) {
+ // Not found until eof.
+ throw new EOFException();
+ }
+ }
+
+ /**
+ * Skips to the next page. Searches for the next page header.
+ *
+ * @param input The {@code ExtractorInput} to skip to the next page.
+ * @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.
+ */
+ private boolean skipToNextPage(ExtractorInput input, long limit)
+ throws IOException, InterruptedException {
+ limit = Math.min(limit + 3, payloadEndPosition);
+ byte[] buffer = new byte[2048];
+ int peekLength = buffer.length;
+ while (true) {
+ if (input.getPosition() + peekLength > limit) {
+ // Make sure to not peek beyond the end of the input.
+ peekLength = (int) (limit - input.getPosition());
+ if (peekLength < 4) {
+ // Not found until end.
+ return false;
+ }
+ }
+ 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'
+ && buffer[i + 3] == 'S') {
+ // Match! Skip to the start of the pattern.
+ input.skipFully(i);
+ return true;
+ }
+ }
+ // Overlap by not skipping the entire peekLength.
+ input.skipFully(peekLength - 3);
+ }
+ }
+
+ /**
+ * Skips to the last Ogg page in the stream and reads the header's granule field which is the
+ * 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 If reading from the input fails.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ @VisibleForTesting
+ long readGranuleOfLastPage(ExtractorInput input) throws IOException, InterruptedException {
+ skipToNextPage(input);
+ pageHeader.reset();
+ while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < payloadEndPosition) {
+ pageHeader.populate(input, /* quiet= */ false);
+ input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
+ }
+ return pageHeader.granulePosition;
+ }
+
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java
new file mode 100644
index 0000000000..449bf35f78
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java
@@ -0,0 +1,143 @@
+/*
+ * 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.ogg;
+
+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.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;
+
+/**
+ * {@link StreamReader} to extract Flac data out of Ogg byte stream.
+ */
+/* package */ final class FlacReader extends StreamReader {
+
+ private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF;
+
+ private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4;
+
+ private FlacStreamMetadata streamMetadata;
+ private FlacOggSeeker flacOggSeeker;
+
+ public static boolean verifyBitstreamType(ParsableByteArray data) {
+ return data.bytesLeft() >= 5 && data.readUnsignedByte() == 0x7F && // packet type
+ data.readUnsignedInt() == 0x464C4143; // ASCII signature "FLAC"
+ }
+
+ @Override
+ protected void reset(boolean headerData) {
+ super.reset(headerData);
+ if (headerData) {
+ streamMetadata = null;
+ flacOggSeeker = null;
+ }
+ }
+
+ private static boolean isAudioPacket(byte[] data) {
+ return data[0] == AUDIO_PACKET_TYPE;
+ }
+
+ @Override
+ protected long preparePayload(ParsableByteArray packet) {
+ if (!isAudioPacket(packet.data)) {
+ return -1;
+ }
+ return getFlacFrameBlockSize(packet);
+ }
+
+ @Override
+ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) {
+ byte[] data = packet.data;
+ if (streamMetadata == null) {
+ streamMetadata = new FlacStreamMetadata(data, 17);
+ byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());
+ setupData.format = streamMetadata.getFormat(metadata, /* id3Metadata= */ null);
+ } else if ((data[0] & 0x7F) == FlacConstants.METADATA_TYPE_SEEK_TABLE) {
+ flacOggSeeker = new FlacOggSeeker();
+ FlacStreamMetadata.SeekTable seekTable =
+ FlacMetadataReader.readSeekTableMetadataBlock(packet);
+ streamMetadata = streamMetadata.copyWithSeekTable(seekTable);
+ } else if (isAudioPacket(data)) {
+ if (flacOggSeeker != null) {
+ flacOggSeeker.setFirstFrameOffset(position);
+ setupData.oggSeeker = flacOggSeeker;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private int getFlacFrameBlockSize(ParsableByteArray packet) {
+ 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();
+ }
+ int result = FlacFrameReader.readFrameBlockSizeSamplesFromKey(packet, blockSizeKey);
+ packet.setPosition(0);
+ return result;
+ }
+
+ private class FlacOggSeeker implements OggSeeker {
+
+ private long firstFrameOffset;
+ private long pendingSeekGranule;
+
+ public FlacOggSeeker() {
+ firstFrameOffset = -1;
+ pendingSeekGranule = -1;
+ }
+
+ public void setFirstFrameOffset(long firstFrameOffset) {
+ this.firstFrameOffset = firstFrameOffset;
+ }
+
+ @Override
+ public long read(ExtractorInput input) throws IOException, InterruptedException {
+ if (pendingSeekGranule >= 0) {
+ long result = -(pendingSeekGranule + 2);
+ pendingSeekGranule = -1;
+ return result;
+ }
+ return -1;
+ }
+
+ @Override
+ 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];
+ }
+
+ @Override
+ public SeekMap createSeekMap() {
+ Assertions.checkState(firstFrameOffset != -1);
+ return new FlacSeekTableSeekMap(streamMetadata, firstFrameOffset);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java
new file mode 100644
index 0000000000..da53a47dc0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java
@@ -0,0 +1,114 @@
+/*
+ * 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.ogg;
+
+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;
+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.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * Extracts data from the Ogg container format.
+ */
+public class OggExtractor implements Extractor {
+
+ /** 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 {
+ return sniffInternal(input);
+ } catch (ParserException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.output = output;
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ if (streamReader != null) {
+ streamReader.seek(position, timeUs);
+ }
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @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);
+ }
+
+ 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) {
+ scratch.setPosition(0);
+ return scratch;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java
new file mode 100644
index 0000000000..1f3bf38c73
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java
@@ -0,0 +1,155 @@
+/*
+ * 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.ogg;
+
+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.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * OGG packet class.
+ */
+/* package */ final class OggPacket {
+
+ private final OggPageHeader pageHeader = new OggPageHeader();
+ private final ParsableByteArray packetArray = new ParsableByteArray(
+ new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0);
+
+ private int currentSegmentIndex = C.INDEX_UNSET;
+ private int segmentCount;
+ private boolean populated;
+
+ /**
+ * Resets this reader.
+ */
+ public void reset() {
+ pageHeader.reset();
+ packetArray.reset();
+ currentSegmentIndex = C.INDEX_UNSET;
+ populated = false;
+ }
+
+ /**
+ * Reads the next packet of the ogg stream. In case of an {@code IOException} the caller must make
+ * sure to pass the same instance of {@code ParsableByteArray} to this method again so this reader
+ * 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. 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);
+
+ if (populated) {
+ populated = false;
+ packetArray.reset();
+ }
+
+ while (!populated) {
+ if (currentSegmentIndex < 0) {
+ // We're at the start of a page.
+ if (!pageHeader.populate(input, true)) {
+ return false;
+ }
+ int segmentIndex = 0;
+ int bytesToSkip = pageHeader.headerSize;
+ if ((pageHeader.type & 0x01) == 0x01 && packetArray.limit() == 0) {
+ // After seeking, the first packet may be the remainder
+ // part of a continued packet which has to be discarded.
+ bytesToSkip += calculatePacketSize(segmentIndex);
+ segmentIndex += segmentCount;
+ }
+ input.skipFully(bytesToSkip);
+ currentSegmentIndex = segmentIndex;
+ }
+
+ 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;
+ }
+ // Advance now since we are sure reading didn't throw an exception.
+ currentSegmentIndex = segmentIndex == pageHeader.pageSegmentCount ? C.INDEX_UNSET
+ : segmentIndex;
+ }
+ return true;
+ }
+
+ /**
+ * 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.
+ *
+ * <p>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.
+ */
+ public OggPageHeader getPageHeader() {
+ return pageHeader;
+ }
+
+ /**
+ * Returns a {@link ParsableByteArray} containing the packet's payload.
+ */
+ public ParsableByteArray getPayload() {
+ 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}.
+ *
+ * @param startSegmentIndex the index of the first segment of the packet.
+ * @return Size of the packet.
+ */
+ private int calculatePacketSize(int startSegmentIndex) {
+ segmentCount = 0;
+ int size = 0;
+ while (startSegmentIndex + segmentCount < pageHeader.pageSegmentCount) {
+ int segmentLength = pageHeader.laces[startSegmentIndex + segmentCount++];
+ size += segmentLength;
+ if (segmentLength != 255) {
+ // packets end at first lace < 255
+ break;
+ }
+ }
+ return size;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java
new file mode 100644
index 0000000000..afdccf80fd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java
@@ -0,0 +1,135 @@
+/*
+ * 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.ogg;
+
+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 java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Data object to store header information.
+ */
+/* package */ final class OggPageHeader {
+
+ public static final int EMPTY_PAGE_HEADER_SIZE = 27;
+ public static final int MAX_SEGMENT_COUNT = 255;
+ public static final int MAX_PAGE_PAYLOAD = 255 * 255;
+ public static final int MAX_PAGE_SIZE = EMPTY_PAGE_HEADER_SIZE + MAX_SEGMENT_COUNT
+ + MAX_PAGE_PAYLOAD;
+
+ 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 <em>end</em> 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;
+ public int pageSegmentCount;
+ public int headerSize;
+ public int bodySize;
+ /**
+ * Be aware that {@code laces.length} is always {@link #MAX_SEGMENT_COUNT}. Instead use
+ * {@link #pageSegmentCount} to iterate.
+ */
+ public final int[] laces = new int[MAX_SEGMENT_COUNT];
+
+ private final ParsableByteArray scratch = new ParsableByteArray(MAX_SEGMENT_COUNT);
+
+ /**
+ * Resets all primitive member fields to zero.
+ */
+ public void reset() {
+ revision = 0;
+ type = 0;
+ granulePosition = 0;
+ streamSerialNumber = 0;
+ pageSequenceNumber = 0;
+ pageChecksum = 0;
+ pageSegmentCount = 0;
+ headerSize = 0;
+ bodySize = 0;
+ }
+
+ /**
+ * Peeks an Ogg page header and updates this {@link OggPageHeader}.
+ *
+ * @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 {
+ scratch.reset();
+ reset();
+ boolean hasEnoughBytes = input.getLength() == C.LENGTH_UNSET
+ || input.getLength() - input.getPeekPosition() >= EMPTY_PAGE_HEADER_SIZE;
+ if (!hasEnoughBytes || !input.peekFully(scratch.data, 0, EMPTY_PAGE_HEADER_SIZE, true)) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new EOFException();
+ }
+ }
+ if (scratch.readUnsignedInt() != TYPE_OGGS) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("expected OggS capture pattern at begin of page");
+ }
+ }
+
+ revision = scratch.readUnsignedByte();
+ if (revision != 0x00) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("unsupported bit stream revision");
+ }
+ }
+ type = scratch.readUnsignedByte();
+
+ granulePosition = scratch.readLittleEndianLong();
+ streamSerialNumber = scratch.readLittleEndianUnsignedInt();
+ pageSequenceNumber = scratch.readLittleEndianUnsignedInt();
+ pageChecksum = scratch.readLittleEndianUnsignedInt();
+ pageSegmentCount = scratch.readUnsignedByte();
+ headerSize = EMPTY_PAGE_HEADER_SIZE + pageSegmentCount;
+
+ // calculate total size of header including laces
+ scratch.reset();
+ input.peekFully(scratch.data, 0, pageSegmentCount);
+ for (int i = 0; i < pageSegmentCount; i++) {
+ laces[i] = scratch.readUnsignedByte();
+ bodySize += laces[i];
+ }
+
+ return true;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java
new file mode 100644
index 0000000000..0a0be963f7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.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.extractor.ogg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import java.io.IOException;
+
+/**
+ * Used to seek in an Ogg stream. OggSeeker implementation may do direct seeking or progressive
+ * seeking. OggSeeker works together with a {@link SeekMap} instance to capture the queried position
+ * and start the seeking with an initial estimated position.
+ */
+/* package */ interface OggSeeker {
+
+ /**
+ * Returns a {@link SeekMap} that returns an initial estimated position for progressive seeking
+ * or the final position for direct seeking. Returns null if {@link #read} has yet to return -1.
+ */
+ SeekMap createSeekMap();
+
+ /**
+ * Starts a seek operation.
+ *
+ * @param targetGranule The target granule position.
+ */
+ void startSeek(long targetGranule);
+
+ /**
+ * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a seek.
+ * <p/>
+ * 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
+ * has been completed then the value returned is -(currentGranule + 2). Else it is -1.
+ *
+ * @param input The {@link ExtractorInput} to read from.
+ * @return A non-negative position to seek the {@link ExtractorInput} to, or -(currentGranule + 2)
+ * if the progressive seek has completed, or -1 otherwise.
+ * @throws IOException If reading from the {@link ExtractorInput} fails.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ long read(ExtractorInput input) throws IOException, InterruptedException;
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java
new file mode 100644
index 0000000000..c3f3a13d54
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java
@@ -0,0 +1,132 @@
+/*
+ * 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.ogg;
+
+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 java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * {@link StreamReader} to extract Opus data out of Ogg byte stream.
+ */
+/* package */ final class OpusReader extends StreamReader {
+
+ private static final int DEFAULT_SEEK_PRE_ROLL_SAMPLES = 3840;
+
+ /**
+ * Opus streams are always decoded at 48000 Hz.
+ */
+ private static final int SAMPLE_RATE = 48000;
+
+ private static final int OPUS_CODE = 0x4f707573;
+ private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'};
+
+ private boolean headerRead;
+
+ public static boolean verifyBitstreamType(ParsableByteArray data) {
+ if (data.bytesLeft() < OPUS_SIGNATURE.length) {
+ return false;
+ }
+ byte[] header = new byte[OPUS_SIGNATURE.length];
+ data.readBytes(header, 0, OPUS_SIGNATURE.length);
+ return Arrays.equals(header, OPUS_SIGNATURE);
+ }
+
+ @Override
+ protected void reset(boolean headerData) {
+ super.reset(headerData);
+ if (headerData) {
+ headerRead = false;
+ }
+ }
+
+ @Override
+ protected long preparePayload(ParsableByteArray packet) {
+ return convertTimeToGranule(getPacketDurationUs(packet.data));
+ }
+
+ @Override
+ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) {
+ if (!headerRead) {
+ byte[] metadata = Arrays.copyOf(packet.data, packet.limit());
+ int channelCount = metadata[9] & 0xFF;
+ int preskip = ((metadata[11] & 0xFF) << 8) | (metadata[10] & 0xFF);
+
+ List<byte[]> initializationData = new ArrayList<>(3);
+ initializationData.add(metadata);
+ putNativeOrderLong(initializationData, preskip);
+ putNativeOrderLong(initializationData, DEFAULT_SEEK_PRE_ROLL_SAMPLES);
+
+ setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS, null,
+ Format.NO_VALUE, Format.NO_VALUE, channelCount, SAMPLE_RATE, initializationData, null, 0,
+ null);
+ headerRead = true;
+ } else {
+ boolean headerPacket = packet.readInt() == OPUS_CODE;
+ packet.setPosition(0);
+ return headerPacket;
+ }
+ return true;
+ }
+
+ private void putNativeOrderLong(List<byte[]> initializationData, int samples) {
+ long ns = (samples * C.NANOS_PER_SECOND) / SAMPLE_RATE;
+ byte[] array = ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(ns).array();
+ initializationData.add(array);
+ }
+
+ /**
+ * Returns the duration of the given audio packet.
+ *
+ * @param packet Contains audio data.
+ * @return Returns the duration of the given audio packet.
+ */
+ private long getPacketDurationUs(byte[] packet) {
+ int toc = packet[0] & 0xFF;
+ int frames;
+ switch (toc & 0x3) {
+ case 0:
+ frames = 1;
+ break;
+ case 1:
+ case 2:
+ frames = 2;
+ break;
+ default:
+ frames = packet[1] & 0x3F;
+ break;
+ }
+
+ int config = toc >> 3;
+ int length = config & 0x3;
+ if (config >= 16) {
+ length = 2500 << length;
+ } else if (config >= 12) {
+ length = 10000 << (length & 0x1);
+ } else if (length == 3) {
+ length = 60000;
+ } else {
+ length = 10000 << length;
+ }
+ return (long) frames * length;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java
new file mode 100644
index 0000000000..067c8aef03
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java
@@ -0,0 +1,268 @@
+/*
+ * 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.ogg;
+
+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.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.util.ParsableByteArray;
+import java.io.IOException;
+
+/** StreamReader abstract class. */
+@SuppressWarnings("UngroupedOverloads")
+/* package */ abstract class StreamReader {
+
+ private static final int STATE_READ_HEADERS = 0;
+ private static final int STATE_SKIP_HEADERS = 1;
+ private static final int STATE_READ_PAYLOAD = 2;
+ private static final int STATE_END_OF_INPUT = 3;
+
+ static class SetupData {
+ Format format;
+ OggSeeker oggSeeker;
+ }
+
+ private final OggPacket oggPacket;
+
+ private TrackOutput trackOutput;
+ private ExtractorOutput extractorOutput;
+ private OggSeeker oggSeeker;
+ private long targetGranule;
+ private long payloadStartPosition;
+ private long currentGranule;
+ private int state;
+ private int sampleRate;
+ private SetupData setupData;
+ private long lengthOfReadPacket;
+ private boolean seekMapSet;
+ private boolean formatSet;
+
+ public StreamReader() {
+ oggPacket = new OggPacket();
+ }
+
+ void init(ExtractorOutput output, TrackOutput trackOutput) {
+ this.extractorOutput = output;
+ this.trackOutput = trackOutput;
+ reset(true);
+ }
+
+ /**
+ * Resets the state of the {@link StreamReader}.
+ *
+ * @param headerData Resets parsed header data too.
+ */
+ protected void reset(boolean headerData) {
+ if (headerData) {
+ setupData = new SetupData();
+ payloadStartPosition = 0;
+ state = STATE_READ_HEADERS;
+ } else {
+ state = STATE_SKIP_HEADERS;
+ }
+ targetGranule = -1;
+ currentGranule = 0;
+ }
+
+ /**
+ * @see Extractor#seek(long, long)
+ */
+ final void seek(long position, long timeUs) {
+ oggPacket.reset();
+ if (position == 0) {
+ reset(!seekMapSet);
+ } else {
+ if (state != STATE_READ_HEADERS) {
+ targetGranule = convertTimeToGranule(timeUs);
+ oggSeeker.startSeek(targetGranule);
+ state = STATE_READ_PAYLOAD;
+ }
+ }
+ }
+
+ /**
+ * @see Extractor#read(ExtractorInput, PositionHolder)
+ */
+ final int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ 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();
+ }
+ }
+
+ private int readHeaders(ExtractorInput input) throws IOException, InterruptedException {
+ boolean readingHeaders = true;
+ while (readingHeaders) {
+ if (!oggPacket.populate(input)) {
+ state = STATE_END_OF_INPUT;
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+ lengthOfReadPacket = input.getPosition() - payloadStartPosition;
+
+ readingHeaders = readHeaders(oggPacket.getPayload(), payloadStartPosition, setupData);
+ if (readingHeaders) {
+ payloadStartPosition = input.getPosition();
+ }
+ }
+
+ sampleRate = setupData.format.sampleRate;
+ if (!formatSet) {
+ trackOutput.format(setupData.format);
+ formatSet = true;
+ }
+
+ if (setupData.oggSeeker != null) {
+ oggSeeker = setupData.oggSeeker;
+ } else if (input.getLength() == C.LENGTH_UNSET) {
+ oggSeeker = new UnseekableOggSeeker();
+ } else {
+ OggPageHeader firstPayloadPageHeader = oggPacket.getPageHeader();
+ 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;
+ }
+
+ private int readPayload(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ long position = oggSeeker.read(input);
+ if (position >= 0) {
+ seekPosition.position = position;
+ return Extractor.RESULT_SEEK;
+ } else if (position < -1) {
+ onSeekEnd(-(position + 2));
+ }
+ if (!seekMapSet) {
+ SeekMap seekMap = oggSeeker.createSeekMap();
+ extractorOutput.seekMap(seekMap);
+ seekMapSet = true;
+ }
+
+ if (lengthOfReadPacket > 0 || oggPacket.populate(input)) {
+ lengthOfReadPacket = 0;
+ ParsableByteArray payload = oggPacket.getPayload();
+ long granulesInPacket = preparePayload(payload);
+ if (granulesInPacket >= 0 && currentGranule + granulesInPacket >= targetGranule) {
+ // calculate time and send payload data to codec
+ long timeUs = convertGranuleToTime(currentGranule);
+ trackOutput.sampleData(payload, payload.limit());
+ trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, payload.limit(), 0, null);
+ targetGranule = -1;
+ }
+ currentGranule += granulesInPacket;
+ } else {
+ state = STATE_END_OF_INPUT;
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ /**
+ * Converts granule value to time.
+ *
+ * @param granule The granule value.
+ * @return Time in milliseconds.
+ */
+ protected long convertGranuleToTime(long granule) {
+ return (granule * C.MICROS_PER_SECOND) / sampleRate;
+ }
+
+ /**
+ * Converts time value to granule.
+ *
+ * @param timeUs Time in milliseconds.
+ * @return The granule value.
+ */
+ protected long convertTimeToGranule(long timeUs) {
+ return (sampleRate * timeUs) / C.MICROS_PER_SECOND;
+ }
+
+ /**
+ * Prepares payload data in the packet for submitting to TrackOutput and returns number of
+ * granules in the packet.
+ *
+ * @param packet Ogg payload data packet.
+ * @return Number of granules in the packet or -1 if the packet doesn't contain payload data.
+ */
+ protected abstract long preparePayload(ParsableByteArray packet);
+
+ /**
+ * Checks if the given packet is a header packet and reads it.
+ *
+ * @param packet An ogg packet.
+ * @param position Position of the given header packet.
+ * @param setupData Setup data to be filled.
+ * @return Whether the packet contains header data.
+ */
+ protected abstract boolean readHeaders(ParsableByteArray packet, long position,
+ SetupData setupData) throws IOException, InterruptedException;
+
+ /**
+ * Called on end of seeking.
+ *
+ * @param currentGranule The granule at the current input position.
+ */
+ protected void onSeekEnd(long currentGranule) {
+ this.currentGranule = currentGranule;
+ }
+
+ private static final class UnseekableOggSeeker implements OggSeeker {
+
+ @Override
+ public long read(ExtractorInput input) {
+ return -1;
+ }
+
+ @Override
+ public void startSeek(long targetGranule) {
+ // Do nothing.
+ }
+
+ @Override
+ public SeekMap createSeekMap() {
+ return new SeekMap.Unseekable(C.TIME_UNSET);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java
new file mode 100644
index 0000000000..cb0678a285
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java
@@ -0,0 +1,198 @@
+/*
+ * 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.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.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;
+import java.util.ArrayList;
+
+/**
+ * {@link StreamReader} to extract Vorbis data out of Ogg byte stream.
+ */
+/* package */ final class VorbisReader extends StreamReader {
+
+ private VorbisSetup vorbisSetup;
+ private int previousPacketBlockSize;
+ private boolean seenFirstAudioPacket;
+
+ private VorbisUtil.VorbisIdHeader vorbisIdHeader;
+ private VorbisUtil.CommentHeader commentHeader;
+
+ public static boolean verifyBitstreamType(ParsableByteArray data) {
+ try {
+ return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, data, true);
+ } catch (ParserException e) {
+ return false;
+ }
+ }
+
+ @Override
+ protected void reset(boolean headerData) {
+ super.reset(headerData);
+ if (headerData) {
+ vorbisSetup = null;
+ vorbisIdHeader = null;
+ commentHeader = null;
+ }
+ previousPacketBlockSize = 0;
+ seenFirstAudioPacket = false;
+ }
+
+ @Override
+ protected void onSeekEnd(long currentGranule) {
+ super.onSeekEnd(currentGranule);
+ seenFirstAudioPacket = currentGranule != 0;
+ previousPacketBlockSize = vorbisIdHeader != null ? vorbisIdHeader.blockSize0 : 0;
+ }
+
+ @Override
+ protected long preparePayload(ParsableByteArray packet) {
+ // if this is not an audio packet...
+ if ((packet.data[0] & 0x01) == 1) {
+ return -1;
+ }
+
+ // ... we need to decode the block size
+ int packetBlockSize = decodeBlockSize(packet.data[0], vorbisSetup);
+ // a packet contains samples produced from overlapping the previous and current frame data
+ // (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2)
+ int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4
+ : 0;
+ // codec expects the number of samples appended to audio data
+ appendNumberOfSamples(packet, samplesInPacket);
+
+ // update state in members for next iteration
+ seenFirstAudioPacket = true;
+ previousPacketBlockSize = packetBlockSize;
+ return samplesInPacket;
+ }
+
+ @Override
+ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
+ throws IOException, InterruptedException {
+ if (vorbisSetup != null) {
+ return false;
+ }
+
+ vorbisSetup = readSetupHeaders(packet);
+ if (vorbisSetup == null) {
+ return true;
+ }
+
+ ArrayList<byte[]> codecInitialisationData = new ArrayList<>();
+ codecInitialisationData.add(vorbisSetup.idHeader.data);
+ codecInitialisationData.add(vorbisSetup.setupHeaderData);
+
+ setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, null,
+ this.vorbisSetup.idHeader.bitrateNominal, Format.NO_VALUE,
+ this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate,
+ codecInitialisationData, null, 0, null);
+ return true;
+ }
+
+ @VisibleForTesting
+ /* package */ VorbisSetup readSetupHeaders(ParsableByteArray scratch) throws IOException {
+
+ if (vorbisIdHeader == null) {
+ vorbisIdHeader = VorbisUtil.readVorbisIdentificationHeader(scratch);
+ return null;
+ }
+
+ if (commentHeader == null) {
+ commentHeader = VorbisUtil.readVorbisCommentHeader(scratch);
+ return null;
+ }
+
+ // the third packet contains the setup header
+ byte[] setupHeaderData = new byte[scratch.limit()];
+ // raw data of vorbis setup header has to be passed to decoder as CSD buffer #2
+ System.arraycopy(scratch.data, 0, setupHeaderData, 0, scratch.limit());
+ // partially decode setup header to get the modes
+ Mode[] modes = VorbisUtil.readVorbisModes(scratch, vorbisIdHeader.channels);
+ // we need the ilog of modes all the time when extracting, so we compute it once
+ int iLogModes = VorbisUtil.iLog(modes.length - 1);
+
+ return new VorbisSetup(vorbisIdHeader, commentHeader, setupHeaderData, modes, iLogModes);
+ }
+
+ /**
+ * 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
+ /* 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) {
+
+ 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() - 3] = (byte) ((packetSampleCount >>> 8) & 0xFF);
+ buffer.data[buffer.limit() - 2] = (byte) ((packetSampleCount >>> 16) & 0xFF);
+ buffer.data[buffer.limit() - 1] = (byte) ((packetSampleCount >>> 24) & 0xFF);
+ }
+
+ private static int decodeBlockSize(byte firstByteOfAudioPacket, VorbisSetup vorbisSetup) {
+ // read modeNumber (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-730004.3.1)
+ int modeNumber = readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1);
+ int currentBlockSize;
+ if (!vorbisSetup.modes[modeNumber].blockFlag) {
+ currentBlockSize = vorbisSetup.idHeader.blockSize0;
+ } else {
+ currentBlockSize = vorbisSetup.idHeader.blockSize1;
+ }
+ return currentBlockSize;
+ }
+
+ /**
+ * Class to hold all data read from Vorbis setup headers.
+ */
+ /* package */ static final class VorbisSetup {
+
+ public final VorbisUtil.VorbisIdHeader idHeader;
+ public final VorbisUtil.CommentHeader commentHeader;
+ public final byte[] setupHeaderData;
+ public final Mode[] modes;
+ public final int iLogModes;
+
+ public VorbisSetup(VorbisUtil.VorbisIdHeader idHeader, VorbisUtil.CommentHeader
+ commentHeader, byte[] setupHeaderData, Mode[] modes, int iLogModes) {
+ this.idHeader = idHeader;
+ this.commentHeader = commentHeader;
+ this.setupHeaderData = setupHeaderData;
+ this.modes = modes;
+ this.iLogModes = iLogModes;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java
new file mode 100644
index 0000000000..a7b32782ff
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java
@@ -0,0 +1,170 @@
+/*
+ * 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.rawcc;
+
+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.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.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * 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 = 0x52434301;
+ private static final int TIMESTAMP_SIZE_V0 = 4;
+ private static final int TIMESTAMP_SIZE_V1 = 8;
+
+ // Parser states.
+ private static final int STATE_READING_HEADER = 0;
+ private static final int STATE_READING_TIMESTAMP_AND_COUNT = 1;
+ private static final int STATE_READING_SAMPLES = 2;
+
+ private final Format format;
+
+ private final ParsableByteArray dataScratch;
+
+ private TrackOutput trackOutput;
+
+ private int parserState;
+ private int version;
+ private long timestampUs;
+ private int remainingSampleCount;
+ private int sampleBytesWritten;
+
+ public RawCcExtractor(Format format) {
+ this.format = format;
+ dataScratch = new ParsableByteArray(SCRATCH_SIZE);
+ parserState = STATE_READING_HEADER;
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ trackOutput = output.track(0, C.TRACK_TYPE_TEXT);
+ output.endTracks();
+ trackOutput.format(format);
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ dataScratch.reset();
+ input.peekFully(dataScratch.data, 0, HEADER_SIZE);
+ return dataScratch.readInt() == HEADER_ID;
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ while (true) {
+ switch (parserState) {
+ case STATE_READING_HEADER:
+ if (parseHeader(input)) {
+ parserState = STATE_READING_TIMESTAMP_AND_COUNT;
+ } else {
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_READING_TIMESTAMP_AND_COUNT:
+ if (parseTimestampAndSampleCount(input)) {
+ parserState = STATE_READING_SAMPLES;
+ } else {
+ parserState = STATE_READING_HEADER;
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_READING_SAMPLES:
+ parseSamples(input);
+ parserState = STATE_READING_TIMESTAMP_AND_COUNT;
+ return RESULT_CONTINUE;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ parserState = STATE_READING_HEADER;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ private boolean parseHeader(ExtractorInput input) throws IOException, InterruptedException {
+ dataScratch.reset();
+ if (input.readFully(dataScratch.data, 0, HEADER_SIZE, true)) {
+ if (dataScratch.readInt() != HEADER_ID) {
+ throw new IOException("Input not RawCC");
+ }
+ version = dataScratch.readUnsignedByte();
+ // no versions use the flag fields yet
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException,
+ InterruptedException {
+ dataScratch.reset();
+ if (version == 0) {
+ if (!input.readFully(dataScratch.data, 0, TIMESTAMP_SIZE_V0 + 1, true)) {
+ return false;
+ }
+ // version 0 timestamps are 45kHz, so we need to convert them into us
+ timestampUs = dataScratch.readUnsignedInt() * 1000 / 45;
+ } else if (version == 1) {
+ if (!input.readFully(dataScratch.data, 0, TIMESTAMP_SIZE_V1 + 1, true)) {
+ return false;
+ }
+ timestampUs = dataScratch.readLong();
+ } else {
+ throw new ParserException("Unsupported version number: " + version);
+ }
+
+ remainingSampleCount = dataScratch.readUnsignedByte();
+ sampleBytesWritten = 0;
+ return true;
+ }
+
+ private void parseSamples(ExtractorInput input) throws IOException, InterruptedException {
+ for (; remainingSampleCount > 0; remainingSampleCount--) {
+ dataScratch.reset();
+ input.readFully(dataScratch.data, 0, 3);
+
+ trackOutput.sampleData(dataScratch, 3);
+ sampleBytesWritten += 3;
+ }
+
+ if (sampleBytesWritten > 0) {
+ trackOutput.sampleMetadata(timestampUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null);
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java
new file mode 100644
index 0000000000..a0a1365935
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java
@@ -0,0 +1,149 @@
+/*
+ * 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.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;
+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 (E-)AC-3 bitstreams.
+ */
+public final class Ac3Extractor implements Extractor {
+
+ /** 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
+ * up.
+ */
+ 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 final Ac3Reader reader;
+ private final ParsableByteArray sampleData;
+
+ private boolean startedPacket;
+
+ /** Creates a new extractor for AC-3 bitstreams. */
+ public Ac3Extractor() {
+ 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(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, 0, 6);
+ scratch.setPosition(0);
+ int syncBytes = scratch.readUnsignedShort();
+ if (syncBytes != AC3_SYNC_WORD) {
+ validFramesCount = 0;
+ input.resetPeekPosition();
+ if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) {
+ return false;
+ }
+ input.advancePeekPosition(headerPosition);
+ } else {
+ if (++validFramesCount >= 4) {
+ return true;
+ }
+ int frameSize = Ac3Util.parseAc3SyncframeSize(scratch.data);
+ if (frameSize == C.LENGTH_UNSET) {
+ return false;
+ }
+ input.advancePeekPosition(frameSize - 6);
+ }
+ }
+ }
+
+ @Override
+ public void init(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();
+ }
+
+ @Override
+ public void release() {
+ // Do nothing.
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
+ InterruptedException {
+ int bytesRead = input.read(sampleData.data, 0, MAX_SYNC_FRAME_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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java
new file mode 100644
index 0000000000..3a6eebbcd2
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java
@@ -0,0 +1,209 @@
+/*
+ * 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.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.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;
+
+/**
+ * Parses a continuous (E-)AC-3 byte stream and extracts individual samples.
+ */
+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 = 128;
+
+ 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 lastByteWas0B;
+
+ // 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 (E-)AC-3 elementary streams.
+ */
+ public Ac3Reader() {
+ this(null);
+ }
+
+ /**
+ * Constructs a new reader for (E-)AC-3 elementary streams.
+ *
+ * @param language Track language.
+ */
+ public Ac3Reader(String language) {
+ headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]);
+ headerScratchBytes = new ParsableByteArray(headerScratchBits.data);
+ state = STATE_FINDING_SYNC;
+ this.language = language;
+ }
+
+ @Override
+ public void seek() {
+ state = STATE_FINDING_SYNC;
+ bytesRead = 0;
+ lastByteWas0B = 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] = 0x0B;
+ headerScratchBytes.data[1] = 0x77;
+ bytesRead = 2;
+ }
+ break;
+ case STATE_READING_HEADER:
+ if (continueRead(data, headerScratchBytes.data, HEADER_SIZE)) {
+ parseHeader();
+ headerScratchBytes.setPosition(0);
+ output.sampleData(headerScratchBytes, HEADER_SIZE);
+ 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 (!lastByteWas0B) {
+ lastByteWas0B = pesBuffer.readUnsignedByte() == 0x0B;
+ continue;
+ }
+ int secondByte = pesBuffer.readUnsignedByte();
+ if (secondByte == 0x77) {
+ lastByteWas0B = false;
+ return true;
+ } else {
+ lastByteWas0B = secondByte == 0x0B;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Parses the sample header.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ private void parseHeader() {
+ headerScratchBits.setPosition(0);
+ SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits);
+ if (format == null || frameInfo.channelCount != format.channelCount
+ || frameInfo.sampleRate != format.sampleRate
+ || frameInfo.mimeType != format.sampleMimeType) {
+ format = Format.createAudioSampleFormat(trackFormatId, frameInfo.mimeType, null,
+ Format.NO_VALUE, Format.NO_VALUE, frameInfo.channelCount, frameInfo.sampleRate, null,
+ null, 0, language);
+ output.format(format);
+ }
+ sampleSize = frameInfo.frameSize;
+ // 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java
new file mode 100644
index 0000000000..9578d110b7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java
new file mode 100644
index 0000000000..2b9965b19b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
new file mode 100644
index 0000000000..b91abfc75a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
@@ -0,0 +1,332 @@
+/*
+ * 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.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;
+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.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+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 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()};
+
+ /**
+ * 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.
+ *
+ * <p>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;
+
+ 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 @Flags int flags;
+
+ private final AdtsReader reader;
+ private final ParsableByteArray packetBuffer;
+ private final ParsableByteArray scratch;
+ private final ParsableBitArray scratchBits;
+
+ @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(/* flags= */ 0);
+ }
+
+ /**
+ * 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.
+ int startPosition = peekId3Header(input);
+
+ // Try to find four or more consecutive AAC audio frames, exceeding the MPEG TS packet size.
+ int headerPosition = startPosition;
+ int totalValidFramesSize = 0;
+ int validFramesCount = 0;
+ while (true) {
+ input.peekFully(scratch.data, 0, 2);
+ scratch.setPosition(0);
+ int syncBytes = scratch.readUnsignedShort();
+ if (!AdtsReader.isAdtsSyncWord(syncBytes)) {
+ validFramesCount = 0;
+ totalValidFramesSize = 0;
+ input.resetPeekPosition();
+ if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) {
+ return false;
+ }
+ input.advancePeekPosition(headerPosition);
+ } else {
+ if (++validFramesCount >= 4 && totalValidFramesSize > TsExtractor.TS_PACKET_SIZE) {
+ return true;
+ }
+
+ // Skip the frame.
+ input.peekFully(scratch.data, 0, 4);
+ scratchBits.setPosition(14);
+ int frameSize = scratchBits.readBits(13);
+ // Either the stream is malformed OR we're not parsing an ADTS stream.
+ if (frameSize <= 6) {
+ return false;
+ }
+ input.advancePeekPosition(frameSize - 6);
+ totalValidFramesSize += frameSize;
+ }
+ }
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.extractorOutput = output;
+ reader.createTracks(output, new TrackIdGenerator(0, 1));
+ output.endTracks();
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ startedPacket = false;
+ reader.seek();
+ firstSampleTimestampUs = timeUs;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @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);
+ boolean readEndOfStream = bytesRead == RESULT_END_OF_INPUT;
+ maybeOutputSeekMap(inputLength, canUseConstantBitrateSeeking, readEndOfStream);
+ if (readEndOfStream) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ // Feed whatever data we have to the reader, regardless of whether the read finished or not.
+ packetBuffer.setPosition(0);
+ packetBuffer.setLimit(bytesRead);
+
+ if (!startedPacket) {
+ // Pass data to the reader as though it's contained within a single infinitely long packet.
+ reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR);
+ startedPacket = true;
+ }
+ // TODO: Make it possible for reader to consume the dataSource directly, so that it becomes
+ // unnecessary to copy the data through packetBuffer.
+ reader.consume(packetBuffer);
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java
new file mode 100644
index 0000000000..f577747ec2
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java
@@ -0,0 +1,532 @@
+/*
+ * 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.ts;
+
+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;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Parses a continuous ADTS byte stream and extracts individual frames.
+ */
+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_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;
+
+ // Match states used while looking for the next sample
+ private static final int MATCH_STATE_VALUE_SHIFT = 8;
+ private static final int MATCH_STATE_START = 1 << MATCH_STATE_VALUE_SHIFT;
+ private static final int MATCH_STATE_FF = 2 << MATCH_STATE_VALUE_SHIFT;
+ private static final int MATCH_STATE_I = 3 << MATCH_STATE_VALUE_SHIFT;
+ private static final int MATCH_STATE_ID = 4 << MATCH_STATE_VALUE_SHIFT;
+
+ 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;
+ private final ParsableByteArray id3HeaderBuffer;
+ private final String language;
+
+ private String formatId;
+ private TrackOutput output;
+ private TrackOutput id3Output;
+
+ private int state;
+ private int bytesRead;
+
+ 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;
+ private long sampleDurationUs;
+ private int sampleSize;
+
+ // Used when reading the samples.
+ private long timeUs;
+
+ private TrackOutput currentOutput;
+ private long currentSampleDuration;
+
+ /**
+ * @param exposeId3 True if the reader should expose ID3 information.
+ */
+ public AdtsReader(boolean exposeId3) {
+ this(exposeId3, null);
+ }
+
+ /**
+ * @param exposeId3 True if the reader should expose ID3 information.
+ * @param language Track language.
+ */
+ public AdtsReader(boolean exposeId3, String language) {
+ 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() {
+ resetSync();
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);
+ if (exposeId3) {
+ idGenerator.generateNewId();
+ id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);
+ id3Output.format(Format.createSampleFormat(idGenerator.getFormatId(),
+ MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null));
+ } else {
+ id3Output = new DummyTrackOutput();
+ }
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ timeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) throws ParserException {
+ while (data.bytesLeft() > 0) {
+ switch (state) {
+ case STATE_FINDING_SAMPLE:
+ findNextSample(data);
+ break;
+ case STATE_READING_ID3_HEADER:
+ if (continueRead(data, id3HeaderBuffer.data, ID3_HEADER_SIZE)) {
+ 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)) {
+ parseAdtsHeader();
+ }
+ break;
+ case STATE_READING_SAMPLE:
+ readSample(data);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // 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.
+ *
+ * @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;
+ }
+
+ /**
+ * Sets the state to STATE_FINDING_SAMPLE.
+ */
+ private void setFindingSampleState() {
+ state = STATE_FINDING_SAMPLE;
+ bytesRead = 0;
+ matchState = MATCH_STATE_START;
+ }
+
+ /**
+ * Sets the state to STATE_READING_ID3_HEADER and resets the fields required for
+ * {@link #parseId3Header()}.
+ */
+ private void setReadingId3HeaderState() {
+ state = STATE_READING_ID3_HEADER;
+ bytesRead = ID3_IDENTIFIER.length;
+ sampleSize = 0;
+ id3HeaderBuffer.setPosition(0);
+ }
+
+ /**
+ * Sets the state to STATE_READING_SAMPLE.
+ *
+ * @param outputToUse TrackOutput object to write the sample to
+ * @param currentSampleDuration Duration of the sample to be read
+ * @param priorReadBytes Size of prior read bytes
+ * @param sampleSize Size of the sample
+ */
+ private void setReadingSampleState(TrackOutput outputToUse, long currentSampleDuration,
+ int priorReadBytes, int sampleSize) {
+ state = STATE_READING_SAMPLE;
+ bytesRead = priorReadBytes;
+ this.currentOutput = outputToUse;
+ this.currentSampleDuration = currentSampleDuration;
+ this.sampleSize = sampleSize;
+ }
+
+ /**
+ * Sets the state to STATE_READING_ADTS_HEADER.
+ */
+ private void setReadingAdtsHeaderState() {
+ state = STATE_READING_ADTS_HEADER;
+ 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.
+ *
+ * @param pesBuffer The buffer whose position should be advanced.
+ */
+ private void findNextSample(ParsableByteArray pesBuffer) {
+ byte[] adtsData = pesBuffer.data;
+ int position = pesBuffer.getPosition();
+ int endOffset = pesBuffer.limit();
+ while (position < endOffset) {
+ int data = adtsData[position++] & 0xFF;
+ 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;
+ break;
+ case MATCH_STATE_START | 'I':
+ matchState = MATCH_STATE_I;
+ break;
+ case MATCH_STATE_I | 'D':
+ matchState = MATCH_STATE_ID;
+ break;
+ case MATCH_STATE_ID | '3':
+ setReadingId3HeaderState();
+ pesBuffer.setPosition(position);
+ return;
+ default:
+ if (matchState != MATCH_STATE_START) {
+ // If matching fails in a later state, revert to MATCH_STATE_START and
+ // check this byte again
+ matchState = MATCH_STATE_START;
+ position--;
+ }
+ break;
+ }
+ }
+ 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:
+ *
+ * <ul>
+ * <li>The MPEG version of this frame must match the previously detected version.
+ * <li>The sample rate index of this frame must match the previously detected sample rate index.
+ * <li>The frame size must be at least 7 bytes
+ * <li>The bytes following the frame must be either another SYNC word with the same MPEG
+ * version, or the start of an ID3 header.
+ * </ul>
+ *
+ * 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.
+ */
+ private void parseId3Header() {
+ id3Output.sampleData(id3HeaderBuffer, ID3_HEADER_SIZE);
+ id3HeaderBuffer.setPosition(ID3_SIZE_OFFSET);
+ setReadingSampleState(id3Output, 0, ID3_HEADER_SIZE,
+ id3HeaderBuffer.readSynchSafeInt() + ID3_HEADER_SIZE);
+ }
+
+ /**
+ * Parses the sample header.
+ */
+ private void parseAdtsHeader() throws ParserException {
+ adtsScratch.setPosition(0);
+
+ if (!hasOutputFormat) {
+ int audioObjectType = adtsScratch.readBits(2) + 1;
+ if (audioObjectType != 2) {
+ // The stream indicates AAC-Main (1), AAC-SSR (3) or AAC-LTP (4). When the stream indicates
+ // AAC-Main it's more likely that the stream contains HE-AAC (5), which cannot be
+ // represented correctly in the 2 bit audio_object_type field in the ADTS header. In
+ // practice when the stream indicates AAC-SSR or AAC-LTP it more commonly contains AAC-LC or
+ // HE-AAC. Since most Android devices don't support AAC-Main, AAC-SSR or AAC-LTP, and since
+ // indicating AAC-LC works for HE-AAC streams, we pretend that we're dealing with AAC-LC and
+ // hope for the best. In practice this often works.
+ // See: https://github.com/google/ExoPlayer/issues/774
+ // See: https://github.com/google/ExoPlayer/issues/1383
+ Log.w(TAG, "Detected audio object type: " + audioObjectType + ", but assuming AAC LC.");
+ audioObjectType = 2;
+ }
+
+ adtsScratch.skipBits(5);
+ int channelConfig = adtsScratch.readBits(3);
+
+ byte[] audioSpecificConfig =
+ CodecSpecificDataUtil.buildAacAudioSpecificConfig(
+ audioObjectType, firstFrameSampleRateIndex, channelConfig);
+ Pair<Integer, Integer> audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig(
+ audioSpecificConfig);
+
+ Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null,
+ Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first,
+ Collections.singletonList(audioSpecificConfig), null, 0, language);
+ // In this class a sample is an access unit, but the MediaFormat sample rate specifies the
+ // number of PCM audio samples per second.
+ sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate;
+ output.format(format);
+ hasOutputFormat = true;
+ } else {
+ adtsScratch.skipBits(10);
+ }
+
+ adtsScratch.skipBits(4);
+ int sampleSize = adtsScratch.readBits(13) - 2 /* the sync word */ - HEADER_SIZE;
+ if (hasCrc) {
+ sampleSize -= CRC_SIZE;
+ }
+
+ setReadingSampleState(output, sampleDurationUs, 0, sampleSize);
+ }
+
+ /**
+ * Reads the rest of the sample
+ */
+ private void readSample(ParsableByteArray data) {
+ int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
+ currentOutput.sampleData(data, bytesToRead);
+ bytesRead += bytesToRead;
+ if (bytesRead == sampleSize) {
+ currentOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ timeUs += currentSampleDuration;
+ setFindingSampleState();
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
new file mode 100644
index 0000000000..cfbc64d2ee
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
@@ -0,0 +1,283 @@
+/*
+ * 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.ts;
+
+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;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Default {@link TsPayloadReader.Factory} implementation.
+ */
+public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Factory {
+
+ /**
+ * 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,
+ 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;
+
+ @Flags private final int flags;
+ private final List<Format> closedCaptionFormats;
+
+ public DefaultTsPayloadReaderFactory() {
+ this(0);
+ }
+
+ /**
+ * @param flags A combination of {@code FLAG_*} values that control the behavior of the created
+ * readers.
+ */
+ public DefaultTsPayloadReaderFactory(@Flags int flags) {
+ this(
+ flags,
+ Collections.singletonList(
+ Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null)));
+ }
+
+ /**
+ * @param flags A combination of {@code FLAG_*} values that control the behavior of the created
+ * readers.
+ * @param closedCaptionFormats {@link Format}s to be exposed by payload readers for streams with
+ * embedded closed captions when no caption service descriptors are provided. If
+ * {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, {@code closedCaptionFormats} overrides
+ * any descriptor information. If not set, and {@code closedCaptionFormats} is empty, a
+ * closed caption track with {@link Format#accessibilityChannel} {@link Format#NO_VALUE} will
+ * be exposed.
+ */
+ public DefaultTsPayloadReaderFactory(@Flags int flags, List<Format> closedCaptionFormats) {
+ this.flags = flags;
+ this.closedCaptionFormats = closedCaptionFormats;
+ }
+
+ @Override
+ public SparseArray<TsPayloadReader> createInitialPayloadReaders() {
+ return new SparseArray<>();
+ }
+
+ @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_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_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(buildUserDataReader(esInfo)));
+ case TsExtractor.TS_STREAM_TYPE_H264:
+ return isSet(FLAG_IGNORE_H264_STREAM) ? null
+ : new PesReader(new H264Reader(buildSeiReader(esInfo),
+ isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), isSet(FLAG_DETECT_ACCESS_UNITS)));
+ case TsExtractor.TS_STREAM_TYPE_H265:
+ return new PesReader(new H265Reader(buildSeiReader(esInfo)));
+ case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO:
+ return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM)
+ ? null : new SectionReader(new SpliceInfoSectionReader());
+ case TsExtractor.TS_STREAM_TYPE_ID3:
+ return new PesReader(new Id3Reader());
+ case TsExtractor.TS_STREAM_TYPE_DVBSUBS:
+ return new PesReader(
+ new DvbSubtitleReader(esInfo.dvbSubtitleInfos));
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link SeiReader} for
+ * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a
+ * {@link SeiReader} 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 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<Format>} of {@link
+ * #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a {@link
+ * List<Format>} 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<Format>} containing list of closed caption formats.
+ */
+ private List<Format> getClosedCaptionFormats(EsInfo esInfo) {
+ if (isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS)) {
+ return closedCaptionFormats;
+ }
+ ParsableByteArray scratchDescriptorData = new ParsableByteArray(esInfo.descriptorBytes);
+ List<Format> closedCaptionFormats = this.closedCaptionFormats;
+ while (scratchDescriptorData.bytesLeft() > 0) {
+ int descriptorTag = scratchDescriptorData.readUnsignedByte();
+ int descriptorLength = scratchDescriptorData.readUnsignedByte();
+ int nextDescriptorPosition = scratchDescriptorData.getPosition() + descriptorLength;
+ if (descriptorTag == DESCRIPTOR_TAG_CAPTION_SERVICE) {
+ // Note: see ATSC A/65 for detailed information about the caption service descriptor.
+ closedCaptionFormats = new ArrayList<>();
+ int numberOfServices = scratchDescriptorData.readUnsignedByte() & 0x1F;
+ for (int i = 0; i < numberOfServices; i++) {
+ String language = scratchDescriptorData.readString(3);
+ int captionTypeByte = scratchDescriptorData.readUnsignedByte();
+ boolean isDigital = (captionTypeByte & 0x80) != 0;
+ String mimeType;
+ int accessibilityChannel;
+ if (isDigital) {
+ mimeType = MimeTypes.APPLICATION_CEA708;
+ accessibilityChannel = captionTypeByte & 0x3F;
+ } else {
+ mimeType = MimeTypes.APPLICATION_CEA608;
+ accessibilityChannel = 1;
+ }
+
+ // easy_reader(1), wide_aspect_ratio(1), reserved(6).
+ byte flags = (byte) scratchDescriptorData.readUnsignedByte();
+ // Skip reserved (8).
+ scratchDescriptorData.skipBytes(1);
+
+ List<byte[]> 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 closedCaptionFormats;
+ }
+
+ private boolean isSet(@Flags int flag) {
+ return (flags & flag) != 0;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java
new file mode 100644
index 0000000000..a4205add7b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java
@@ -0,0 +1,181 @@
+/*
+ * 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.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.audio.DtsUtil;
+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.ParsableByteArray;
+
+/**
+ * Parses a continuous DTS byte stream and extracts individual samples.
+ */
+public final class DtsReader implements ElementaryStreamReader {
+
+ 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 = 18;
+
+ private final ParsableByteArray headerScratchBytes;
+ private final String language;
+
+ private String formatId;
+ private TrackOutput output;
+
+ private int state;
+ private int bytesRead;
+
+ // Used to find the header.
+ private int syncBytes;
+
+ // 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 DTS elementary streams.
+ *
+ * @param language Track language.
+ */
+ public DtsReader(String language) {
+ headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]);
+ state = STATE_FINDING_SYNC;
+ this.language = language;
+ }
+
+ @Override
+ public void seek() {
+ state = STATE_FINDING_SYNC;
+ bytesRead = 0;
+ syncBytes = 0;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.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;
+ }
+ break;
+ case STATE_READING_HEADER:
+ if (continueRead(data, headerScratchBytes.data, HEADER_SIZE)) {
+ parseHeader();
+ headerScratchBytes.setPosition(0);
+ output.sampleData(headerScratchBytes, HEADER_SIZE);
+ 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:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ @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 SYNC value in the buffer, advancing the position to the byte that immediately
+ * follows it. If SYNC was not located, the position is advanced to the limit.
+ *
+ * @param pesBuffer The buffer whose position should be advanced.
+ * @return Whether SYNC was found.
+ */
+ private boolean skipToNextSync(ParsableByteArray pesBuffer) {
+ while (pesBuffer.bytesLeft() > 0) {
+ syncBytes <<= 8;
+ syncBytes |= pesBuffer.readUnsignedByte();
+ 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;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Parses the sample header.
+ */
+ private void parseHeader() {
+ byte[] frameData = headerScratchBytes.data;
+ if (format == null) {
+ format = DtsUtil.parseDtsFormat(frameData, formatId, language, null);
+ output.format(format);
+ }
+ sampleSize = DtsUtil.getDtsFrameSize(frameData);
+ // In this class a sample is an access unit (frame in DTS), but the format's sample rate
+ // specifies the number of PCM audio samples per second.
+ sampleDurationUs = (int) (C.MICROS_PER_SECOND
+ * DtsUtil.parseDtsAudioSampleCount(frameData) / format.sampleRate);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java
new file mode 100644
index 0000000000..aceab78bf0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java
@@ -0,0 +1,130 @@
+/*
+ * 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 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;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.DvbSubtitleInfo;
+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.ParsableByteArray;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Parses DVB subtitle data and extracts individual frames.
+ */
+public final class DvbSubtitleReader implements ElementaryStreamReader {
+
+ private final List<DvbSubtitleInfo> subtitleInfos;
+ private final TrackOutput[] outputs;
+
+ private boolean writingSample;
+ private int bytesToCheck;
+ private int sampleBytesWritten;
+ private long sampleTimeUs;
+
+ /**
+ * @param subtitleInfos Information about the DVB subtitles associated to the stream.
+ */
+ public DvbSubtitleReader(List<DvbSubtitleInfo> subtitleInfos) {
+ this.subtitleInfos = subtitleInfos;
+ outputs = new TrackOutput[subtitleInfos.size()];
+ }
+
+ @Override
+ public void seek() {
+ writingSample = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ for (int i = 0; i < outputs.length; i++) {
+ 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,
+ 0,
+ Collections.singletonList(subtitleInfo.initializationData),
+ subtitleInfo.language,
+ null));
+ outputs[i] = output;
+ }
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) {
+ return;
+ }
+ writingSample = true;
+ sampleTimeUs = pesTimeUs;
+ sampleBytesWritten = 0;
+ bytesToCheck = 2;
+ }
+
+ @Override
+ public void packetFinished() {
+ if (writingSample) {
+ for (TrackOutput output : outputs) {
+ output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null);
+ }
+ writingSample = false;
+ }
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ if (writingSample) {
+ if (bytesToCheck == 2 && !checkNextByte(data, 0x20)) {
+ // Failed to check data_identifier
+ return;
+ }
+ if (bytesToCheck == 1 && !checkNextByte(data, 0x00)) {
+ // Check and discard the subtitle_stream_id
+ return;
+ }
+ int dataPosition = data.getPosition();
+ int bytesAvailable = data.bytesLeft();
+ for (TrackOutput output : outputs) {
+ data.setPosition(dataPosition);
+ output.sampleData(data, bytesAvailable);
+ }
+ sampleBytesWritten += bytesAvailable;
+ }
+ }
+
+ private boolean checkNextByte(ParsableByteArray data, int expectedValue) {
+ if (data.bytesLeft() == 0) {
+ return false;
+ }
+ if (data.readUnsignedByte() != expectedValue) {
+ writingSample = false;
+ }
+ bytesToCheck--;
+ return writingSample;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java
new file mode 100644
index 0000000000..edd33d02c2
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java
@@ -0,0 +1,63 @@
+/*
+ * 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.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;
+
+/**
+ * Extracts individual samples from an elementary media stream, preserving original order.
+ */
+public interface ElementaryStreamReader {
+
+ /**
+ * Notifies the reader that a seek has occurred.
+ */
+ void seek();
+
+ /**
+ * Initializes the reader by providing outputs and ids for the tracks.
+ *
+ * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data.
+ * @param idGenerator A {@link PesReader.TrackIdGenerator} that generates unique track ids for the
+ * {@link TrackOutput}s.
+ */
+ void createTracks(ExtractorOutput extractorOutput, PesReader.TrackIdGenerator idGenerator);
+
+ /**
+ * Called when a packet starts.
+ *
+ * @param pesTimeUs The timestamp associated with the packet.
+ * @param flags See {@link TsPayloadReader.Flags}.
+ */
+ 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) throws ParserException;
+
+ /**
+ * Called when a packet ends.
+ */
+ void packetFinished();
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java
new file mode 100644
index 0000000000..576607366e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java
@@ -0,0 +1,333 @@
+/*
+ * 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.ts;
+
+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.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.NalUnitUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Parses a continuous H262 byte stream and extracts individual frames.
+ */
+public final class H262Reader implements ElementaryStreamReader {
+
+ private static final int START_PICTURE = 0x00;
+ 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;
+
+ // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4.
+ private static final double[] FRAME_RATE_VALUES = new double[] {
+ 24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60};
+
+ // State that should not be reset on seek.
+ 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 final NalUnitTargetBuffer userData;
+ private long totalBytesWritten;
+ private boolean startedFirstSample;
+
+ // Per packet state that gets reset at the start of each packet.
+ private long pesTimeUs;
+
+ // 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();
+ if (userDataReader != null) {
+ userData.reset();
+ }
+ totalBytesWritten = 0;
+ startedFirstSample = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ 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, @TsPayloadReader.Flags int flags) {
+ // TODO (Internal b/32267012): Consider using random access indicator.
+ this.pesTimeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ int offset = data.getPosition();
+ int limit = data.limit();
+ byte[] dataArray = data.data;
+
+ // Append the data to the buffer.
+ totalBytesWritten += data.bytesLeft();
+ output.sampleData(data, data.bytesLeft());
+
+ while (true) {
+ 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) {
+ 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 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.
+ Pair<Format, Long> result = parseCsdBuffer(csdBuffer, formatId);
+ output.format(result.first);
+ frameDurationUs = result.second;
+ hasOutputFormat = true;
+ }
+ }
+ if (userDataReader != null) {
+ int bytesAlreadyPassed = 0;
+ if (lengthToStartCode > 0) {
+ userData.appendToNalUnit(dataArray, offset, startCodeOffset);
+ } else {
+ bytesAlreadyPassed = -lengthToStartCode;
+ }
+
+ 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 + 3;
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ /**
+ * Parses the {@link Format} and frame duration from a csd buffer.
+ *
+ * @param csdBuffer The csd buffer.
+ * @param formatId The id for the generated format. May be null.
+ * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or
+ * 0 if the duration could not be determined.
+ */
+ private static Pair<Format, Long> parseCsdBuffer(CsdBuffer csdBuffer, String formatId) {
+ byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length);
+
+ int firstByte = csdData[4] & 0xFF;
+ int secondByte = csdData[5] & 0xFF;
+ int thirdByte = csdData[6] & 0xFF;
+ int width = (firstByte << 4) | (secondByte >> 4);
+ int height = (secondByte & 0x0F) << 8 | thirdByte;
+
+ float pixelWidthHeightRatio = 1f;
+ int aspectRatioCode = (csdData[7] & 0xF0) >> 4;
+ switch(aspectRatioCode) {
+ case 2:
+ pixelWidthHeightRatio = (4 * height) / (float) (3 * width);
+ break;
+ case 3:
+ pixelWidthHeightRatio = (16 * height) / (float) (9 * width);
+ break;
+ case 4:
+ pixelWidthHeightRatio = (121 * height) / (float) (100 * width);
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+
+ Format format = Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_MPEG2, null,
+ Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE,
+ Collections.singletonList(csdData), Format.NO_VALUE, pixelWidthHeightRatio, null);
+
+ long frameDurationUs = 0;
+ int frameRateCodeMinusOne = (csdData[7] & 0x0F) - 1;
+ if (0 <= frameRateCodeMinusOne && frameRateCodeMinusOne < FRAME_RATE_VALUES.length) {
+ double frameRate = FRAME_RATE_VALUES[frameRateCodeMinusOne];
+ int sequenceExtensionPosition = csdBuffer.sequenceExtensionPosition;
+ int frameRateExtensionN = (csdData[sequenceExtensionPosition + 9] & 0x60) >> 5;
+ int frameRateExtensionD = (csdData[sequenceExtensionPosition + 9] & 0x1F);
+ if (frameRateExtensionN != frameRateExtensionD) {
+ frameRate *= (frameRateExtensionN + 1d) / (frameRateExtensionD + 1);
+ }
+ frameDurationUs = (long) (C.MICROS_PER_SECOND / frameRate);
+ }
+
+ return Pair.create(format, frameDurationUs);
+ }
+
+ private static final class CsdBuffer {
+
+ private static final byte[] START_CODE = new byte[] {0, 0, 1};
+
+ private boolean isFilling;
+
+ public int length;
+ public int sequenceExtensionPosition;
+ public byte[] data;
+
+ public CsdBuffer(int initialCapacity) {
+ data = new byte[initialCapacity];
+ }
+
+ /**
+ * Resets the buffer, clearing any data that it holds.
+ */
+ public void reset() {
+ isFilling = false;
+ length = 0;
+ sequenceExtensionPosition = 0;
+ }
+
+ /**
+ * 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 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 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 {
+ isFilling = false;
+ return true;
+ }
+ } else if (startCodeValue == START_SEQUENCE_HEADER) {
+ isFilling = true;
+ }
+ onData(START_CODE, 0, START_CODE.length);
+ return false;
+ }
+
+ /**
+ * Called to pass stream data.
+ *
+ * @param newData Holds the data being passed.
+ * @param offset The offset of the data in {@code data}.
+ * @param limit The limit (exclusive) of the data in {@code data}.
+ */
+ public void onData(byte[] newData, int offset, int limit) {
+ if (!isFilling) {
+ return;
+ }
+ int readLength = limit - offset;
+ if (data.length < length + readLength) {
+ data = Arrays.copyOf(data, (length + readLength) * 2);
+ }
+ System.arraycopy(newData, offset, data, length, readLength);
+ length += readLength;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java
new file mode 100644
index 0000000000..164c115159
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java
@@ -0,0 +1,567 @@
+/*
+ * 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.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;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Parses a continuous H264 byte stream and extracts individual frames.
+ */
+public final class H264Reader implements ElementaryStreamReader {
+
+ private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information
+ private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set
+ private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set
+
+ private final SeiReader seiReader;
+ private final boolean allowNonIdrKeyframes;
+ private final boolean detectAccessUnits;
+ private final NalUnitTargetBuffer sps;
+ private final NalUnitTargetBuffer pps;
+ private final NalUnitTargetBuffer sei;
+ private long totalBytesWritten;
+ private final boolean[] prefixFlags;
+
+ private String formatId;
+ private TrackOutput output;
+ private SampleReader sampleReader;
+
+ // State that should not be reset on seek.
+ private boolean hasOutputFormat;
+
+ // 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;
+
+ /**
+ * @param seiReader An SEI reader for consuming closed caption channels.
+ * @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as
+ * synchronization samples (key-frames).
+ * @param detectAccessUnits Whether to split the input stream into access units (samples) based on
+ * slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs).
+ */
+ public H264Reader(SeiReader seiReader, boolean allowNonIdrKeyframes, boolean detectAccessUnits) {
+ this.seiReader = seiReader;
+ this.allowNonIdrKeyframes = allowNonIdrKeyframes;
+ this.detectAccessUnits = detectAccessUnits;
+ prefixFlags = new boolean[3];
+ sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128);
+ pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128);
+ sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128);
+ seiWrapper = new ParsableByteArray();
+ }
+
+ @Override
+ public void seek() {
+ NalUnitUtil.clearPrefixFlags(prefixFlags);
+ sps.reset();
+ pps.reset();
+ sei.reset();
+ sampleReader.reset();
+ totalBytesWritten = 0;
+ randomAccessIndicator = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO);
+ sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits);
+ seiReader.createTracks(extractorOutput, idGenerator);
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ this.pesTimeUs = pesTimeUs;
+ randomAccessIndicator |= (flags & FLAG_RANDOM_ACCESS_INDICATOR) != 0;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ int offset = data.getPosition();
+ int limit = data.limit();
+ byte[] dataArray = data.data;
+
+ // Append the data to the buffer.
+ totalBytesWritten += data.bytesLeft();
+ output.sampleData(data, data.bytesLeft());
+
+ // Scan the appended data, processing NAL units as they are encountered
+ while (true) {
+ int nalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);
+
+ if (nalUnitOffset == limit) {
+ // We've scanned to the end of the data without finding the start of another NAL unit.
+ nalUnitData(dataArray, offset, limit);
+ return;
+ }
+
+ // We've seen the start of a NAL unit of the following type.
+ int nalUnitType = NalUnitUtil.getNalUnitType(dataArray, nalUnitOffset);
+
+ // This is the number of bytes from the current offset to the start of the next NAL unit.
+ // It may be negative if the NAL unit started in the previously consumed data.
+ int lengthToNalUnit = nalUnitOffset - offset;
+ if (lengthToNalUnit > 0) {
+ nalUnitData(dataArray, offset, nalUnitOffset);
+ }
+ int bytesWrittenPastPosition = limit - nalUnitOffset;
+ long absolutePosition = totalBytesWritten - bytesWrittenPastPosition;
+ // Indicate the end of the previous NAL unit. If the length to the start of the next unit
+ // is negative then we wrote too many bytes to the NAL buffers. Discard the excess bytes
+ // when notifying that the unit has ended.
+ endNalUnit(absolutePosition, bytesWrittenPastPosition,
+ lengthToNalUnit < 0 ? -lengthToNalUnit : 0, pesTimeUs);
+ // Indicate the start of the next NAL unit.
+ startNalUnit(absolutePosition, nalUnitType, pesTimeUs);
+ // Continue scanning the data.
+ offset = nalUnitOffset + 3;
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ private void startNalUnit(long position, int nalUnitType, long pesTimeUs) {
+ if (!hasOutputFormat || sampleReader.needsSpsPps()) {
+ sps.startNalUnit(nalUnitType);
+ pps.startNalUnit(nalUnitType);
+ }
+ sei.startNalUnit(nalUnitType);
+ sampleReader.startNalUnit(position, nalUnitType, pesTimeUs);
+ }
+
+ private void nalUnitData(byte[] dataArray, int offset, int limit) {
+ if (!hasOutputFormat || sampleReader.needsSpsPps()) {
+ sps.appendToNalUnit(dataArray, offset, limit);
+ pps.appendToNalUnit(dataArray, offset, limit);
+ }
+ sei.appendToNalUnit(dataArray, offset, limit);
+ sampleReader.appendToNalUnit(dataArray, offset, limit);
+ }
+
+ private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) {
+ if (!hasOutputFormat || sampleReader.needsSpsPps()) {
+ sps.endNalUnit(discardPadding);
+ pps.endNalUnit(discardPadding);
+ if (!hasOutputFormat) {
+ if (sps.isCompleted() && pps.isCompleted()) {
+ List<byte[]> initializationData = new ArrayList<>();
+ initializationData.add(Arrays.copyOf(sps.nalData, sps.nalLength));
+ 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,
+ 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);
+ sps.reset();
+ pps.reset();
+ }
+ } else if (sps.isCompleted()) {
+ NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength);
+ sampleReader.putSps(spsData);
+ sps.reset();
+ } else if (pps.isCompleted()) {
+ NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength);
+ sampleReader.putPps(ppsData);
+ pps.reset();
+ }
+ }
+ if (sei.endNalUnit(discardPadding)) {
+ int unescapedLength = NalUnitUtil.unescapeStream(sei.nalData, sei.nalLength);
+ seiWrapper.reset(sei.nalData, unescapedLength);
+ seiWrapper.setPosition(4); // NAL prefix and nal_unit() header.
+ seiReader.consume(pesTimeUs, seiWrapper);
+ }
+ 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. */
+ private static final class SampleReader {
+
+ private static final int DEFAULT_BUFFER_SIZE = 128;
+
+ private static final int NAL_UNIT_TYPE_NON_IDR = 1; // Coded slice of a non-IDR picture
+ private static final int NAL_UNIT_TYPE_PARTITION_A = 2; // Coded slice data partition A
+ private static final int NAL_UNIT_TYPE_IDR = 5; // Coded slice of an IDR picture
+ private static final int NAL_UNIT_TYPE_AUD = 9; // Access unit delimiter
+
+ private final TrackOutput output;
+ private final boolean allowNonIdrKeyframes;
+ private final boolean detectAccessUnits;
+ private final SparseArray<NalUnitUtil.SpsData> sps;
+ private final SparseArray<NalUnitUtil.PpsData> pps;
+ private final ParsableNalUnitBitArray bitArray;
+
+ private byte[] buffer;
+ private int bufferLength;
+
+ // Per NAL unit state. A sample consists of one or more NAL units.
+ private int nalUnitType;
+ private long nalUnitStartPosition;
+ private boolean isFilling;
+ private long nalUnitTimeUs;
+ private SliceHeaderData previousSliceHeader;
+ private SliceHeaderData sliceHeader;
+
+ // Per sample state that gets reset at the start of each sample.
+ private boolean readingSample;
+ private long samplePosition;
+ private long sampleTimeUs;
+ private boolean sampleIsKeyframe;
+
+ public SampleReader(TrackOutput output, boolean allowNonIdrKeyframes,
+ boolean detectAccessUnits) {
+ this.output = output;
+ this.allowNonIdrKeyframes = allowNonIdrKeyframes;
+ this.detectAccessUnits = detectAccessUnits;
+ sps = new SparseArray<>();
+ pps = new SparseArray<>();
+ previousSliceHeader = new SliceHeaderData();
+ sliceHeader = new SliceHeaderData();
+ buffer = new byte[DEFAULT_BUFFER_SIZE];
+ bitArray = new ParsableNalUnitBitArray(buffer, 0, 0);
+ reset();
+ }
+
+ public boolean needsSpsPps() {
+ return detectAccessUnits;
+ }
+
+ public void putSps(NalUnitUtil.SpsData spsData) {
+ sps.append(spsData.seqParameterSetId, spsData);
+ }
+
+ public void putPps(NalUnitUtil.PpsData ppsData) {
+ pps.append(ppsData.picParameterSetId, ppsData);
+ }
+
+ public void reset() {
+ isFilling = false;
+ readingSample = false;
+ sliceHeader.clear();
+ }
+
+ public void startNalUnit(long position, int type, long pesTimeUs) {
+ nalUnitType = type;
+ nalUnitTimeUs = pesTimeUs;
+ nalUnitStartPosition = position;
+ if ((allowNonIdrKeyframes && nalUnitType == NAL_UNIT_TYPE_NON_IDR)
+ || (detectAccessUnits && (nalUnitType == NAL_UNIT_TYPE_IDR
+ || nalUnitType == NAL_UNIT_TYPE_NON_IDR
+ || nalUnitType == NAL_UNIT_TYPE_PARTITION_A))) {
+ // Store the previous header and prepare to populate the new one.
+ SliceHeaderData newSliceHeader = previousSliceHeader;
+ previousSliceHeader = sliceHeader;
+ sliceHeader = newSliceHeader;
+ sliceHeader.clear();
+ bufferLength = 0;
+ isFilling = true;
+ }
+ }
+
+ /**
+ * Called to pass stream data. The data passed should not include the 3 byte start code.
+ *
+ * @param data Holds the data being passed.
+ * @param offset The offset of the data in {@code data}.
+ * @param limit The limit (exclusive) of the data in {@code data}.
+ */
+ public void appendToNalUnit(byte[] data, int offset, int limit) {
+ if (!isFilling) {
+ return;
+ }
+ int readLength = limit - offset;
+ if (buffer.length < bufferLength + readLength) {
+ buffer = Arrays.copyOf(buffer, (bufferLength + readLength) * 2);
+ }
+ System.arraycopy(data, offset, buffer, bufferLength, readLength);
+ bufferLength += readLength;
+
+ bitArray.reset(buffer, 0, bufferLength);
+ if (!bitArray.canReadBits(8)) {
+ return;
+ }
+ bitArray.skipBit(); // forbidden_zero_bit
+ int nalRefIdc = bitArray.readBits(2);
+ bitArray.skipBits(5); // nal_unit_type
+
+ // Read the slice header using the syntax defined in ITU-T Recommendation H.264 (2013)
+ // subsection 7.3.3.
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ bitArray.readUnsignedExpGolombCodedInt(); // first_mb_in_slice
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ int sliceType = bitArray.readUnsignedExpGolombCodedInt();
+ if (!detectAccessUnits) {
+ // There are AUDs in the stream so the rest of the header can be ignored.
+ isFilling = false;
+ sliceHeader.setSliceType(sliceType);
+ return;
+ }
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ int picParameterSetId = bitArray.readUnsignedExpGolombCodedInt();
+ if (pps.indexOfKey(picParameterSetId) < 0) {
+ // We have not seen the PPS yet, so don't try to decode the slice header.
+ isFilling = false;
+ return;
+ }
+ NalUnitUtil.PpsData ppsData = pps.get(picParameterSetId);
+ NalUnitUtil.SpsData spsData = sps.get(ppsData.seqParameterSetId);
+ if (spsData.separateColorPlaneFlag) {
+ if (!bitArray.canReadBits(2)) {
+ return;
+ }
+ bitArray.skipBits(2); // colour_plane_id
+ }
+ if (!bitArray.canReadBits(spsData.frameNumLength)) {
+ return;
+ }
+ boolean fieldPicFlag = false;
+ boolean bottomFieldFlagPresent = false;
+ boolean bottomFieldFlag = false;
+ int frameNum = bitArray.readBits(spsData.frameNumLength);
+ if (!spsData.frameMbsOnlyFlag) {
+ if (!bitArray.canReadBits(1)) {
+ return;
+ }
+ fieldPicFlag = bitArray.readBit();
+ if (fieldPicFlag) {
+ if (!bitArray.canReadBits(1)) {
+ return;
+ }
+ bottomFieldFlag = bitArray.readBit();
+ bottomFieldFlagPresent = true;
+ }
+ }
+ boolean idrPicFlag = nalUnitType == NAL_UNIT_TYPE_IDR;
+ int idrPicId = 0;
+ if (idrPicFlag) {
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ idrPicId = bitArray.readUnsignedExpGolombCodedInt();
+ }
+ int picOrderCntLsb = 0;
+ int deltaPicOrderCntBottom = 0;
+ int deltaPicOrderCnt0 = 0;
+ int deltaPicOrderCnt1 = 0;
+ if (spsData.picOrderCountType == 0) {
+ if (!bitArray.canReadBits(spsData.picOrderCntLsbLength)) {
+ return;
+ }
+ picOrderCntLsb = bitArray.readBits(spsData.picOrderCntLsbLength);
+ if (ppsData.bottomFieldPicOrderInFramePresentFlag && !fieldPicFlag) {
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ deltaPicOrderCntBottom = bitArray.readSignedExpGolombCodedInt();
+ }
+ } else if (spsData.picOrderCountType == 1
+ && !spsData.deltaPicOrderAlwaysZeroFlag) {
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ deltaPicOrderCnt0 = bitArray.readSignedExpGolombCodedInt();
+ if (ppsData.bottomFieldPicOrderInFramePresentFlag && !fieldPicFlag) {
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ deltaPicOrderCnt1 = bitArray.readSignedExpGolombCodedInt();
+ }
+ }
+ sliceHeader.setAll(spsData, nalRefIdc, sliceType, frameNum, picParameterSetId, fieldPicFlag,
+ bottomFieldFlagPresent, bottomFieldFlag, idrPicFlag, idrPicId, picOrderCntLsb,
+ deltaPicOrderCntBottom, deltaPicOrderCnt0, deltaPicOrderCnt1);
+ isFilling = false;
+ }
+
+ 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 (hasOutputFormat && readingSample) {
+ int nalUnitLength = (int) (position - nalUnitStartPosition);
+ outputSample(offset + nalUnitLength);
+ }
+ samplePosition = nalUnitStartPosition;
+ sampleTimeUs = nalUnitTimeUs;
+ sampleIsKeyframe = false;
+ readingSample = true;
+ }
+ 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) {
+ @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
+ int size = (int) (nalUnitStartPosition - samplePosition);
+ output.sampleMetadata(sampleTimeUs, flags, size, offset, null);
+ }
+
+ private static final class SliceHeaderData {
+
+ private static final int SLICE_TYPE_I = 2;
+ private static final int SLICE_TYPE_ALL_I = 7;
+
+ private boolean isComplete;
+ private boolean hasSliceType;
+
+ private SpsData spsData;
+ private int nalRefIdc;
+ private int sliceType;
+ private int frameNum;
+ private int picParameterSetId;
+ private boolean fieldPicFlag;
+ private boolean bottomFieldFlagPresent;
+ private boolean bottomFieldFlag;
+ private boolean idrPicFlag;
+ private int idrPicId;
+ private int picOrderCntLsb;
+ private int deltaPicOrderCntBottom;
+ private int deltaPicOrderCnt0;
+ private int deltaPicOrderCnt1;
+
+ public void clear() {
+ hasSliceType = false;
+ isComplete = false;
+ }
+
+ public void setSliceType(int sliceType) {
+ this.sliceType = sliceType;
+ 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) {
+ this.spsData = spsData;
+ this.nalRefIdc = nalRefIdc;
+ this.sliceType = sliceType;
+ this.frameNum = frameNum;
+ this.picParameterSetId = picParameterSetId;
+ this.fieldPicFlag = fieldPicFlag;
+ this.bottomFieldFlagPresent = bottomFieldFlagPresent;
+ this.bottomFieldFlag = bottomFieldFlag;
+ this.idrPicFlag = idrPicFlag;
+ this.idrPicId = idrPicId;
+ this.picOrderCntLsb = picOrderCntLsb;
+ this.deltaPicOrderCntBottom = deltaPicOrderCntBottom;
+ this.deltaPicOrderCnt0 = deltaPicOrderCnt0;
+ this.deltaPicOrderCnt1 = deltaPicOrderCnt1;
+ isComplete = true;
+ hasSliceType = true;
+ }
+
+ public boolean isISlice() {
+ return hasSliceType && (sliceType == SLICE_TYPE_ALL_I || sliceType == SLICE_TYPE_I);
+ }
+
+ 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));
+ }
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java
new file mode 100644
index 0000000000..6aa7c5d71d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java
@@ -0,0 +1,494 @@
+/*
+ * 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.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.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;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
+import java.util.Collections;
+
+/**
+ * Parses a continuous H.265 byte stream and extracts individual frames.
+ */
+public final class H265Reader implements ElementaryStreamReader {
+
+ private static final String TAG = "H265Reader";
+
+ // nal_unit_type values from H.265/HEVC (2014) Table 7-1.
+ private static final int RASL_R = 9;
+ private static final int BLA_W_LP = 16;
+ private static final int CRA_NUT = 21;
+ private static final int VPS_NUT = 32;
+ private static final int SPS_NUT = 33;
+ private static final int PPS_NUT = 34;
+ private static final int PREFIX_SEI_NUT = 39;
+ private static final int SUFFIX_SEI_NUT = 40;
+
+ private final SeiReader seiReader;
+
+ private String formatId;
+ private TrackOutput output;
+ private SampleReader sampleReader;
+
+ // State that should not be reset on seek.
+ private boolean hasOutputFormat;
+
+ // State that should be reset on seek.
+ private final boolean[] prefixFlags;
+ private final NalUnitTargetBuffer vps;
+ private final NalUnitTargetBuffer sps;
+ private final NalUnitTargetBuffer pps;
+ private final NalUnitTargetBuffer prefixSei;
+ private final NalUnitTargetBuffer suffixSei; // TODO: Are both needed?
+ private long totalBytesWritten;
+
+ // Per packet state that gets reset at the start of each packet.
+ private long pesTimeUs;
+
+ // Scratch variables to avoid allocations.
+ private final ParsableByteArray seiWrapper;
+
+ /**
+ * @param seiReader An SEI reader for consuming closed caption channels.
+ */
+ public H265Reader(SeiReader seiReader) {
+ this.seiReader = seiReader;
+ prefixFlags = new boolean[3];
+ vps = new NalUnitTargetBuffer(VPS_NUT, 128);
+ sps = new NalUnitTargetBuffer(SPS_NUT, 128);
+ pps = new NalUnitTargetBuffer(PPS_NUT, 128);
+ prefixSei = new NalUnitTargetBuffer(PREFIX_SEI_NUT, 128);
+ suffixSei = new NalUnitTargetBuffer(SUFFIX_SEI_NUT, 128);
+ seiWrapper = new ParsableByteArray();
+ }
+
+ @Override
+ public void seek() {
+ NalUnitUtil.clearPrefixFlags(prefixFlags);
+ vps.reset();
+ sps.reset();
+ pps.reset();
+ prefixSei.reset();
+ suffixSei.reset();
+ sampleReader.reset();
+ totalBytesWritten = 0;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO);
+ sampleReader = new SampleReader(output);
+ seiReader.createTracks(extractorOutput, idGenerator);
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ // TODO (Internal b/32267012): Consider using random access indicator.
+ this.pesTimeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ while (data.bytesLeft() > 0) {
+ int offset = data.getPosition();
+ int limit = data.limit();
+ byte[] dataArray = data.data;
+
+ // Append the data to the buffer.
+ totalBytesWritten += data.bytesLeft();
+ output.sampleData(data, data.bytesLeft());
+
+ // Scan the appended data, processing NAL units as they are encountered
+ while (offset < limit) {
+ int nalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);
+
+ if (nalUnitOffset == limit) {
+ // We've scanned to the end of the data without finding the start of another NAL unit.
+ nalUnitData(dataArray, offset, limit);
+ return;
+ }
+
+ // We've seen the start of a NAL unit of the following type.
+ int nalUnitType = NalUnitUtil.getH265NalUnitType(dataArray, nalUnitOffset);
+
+ // This is the number of bytes from the current offset to the start of the next NAL unit.
+ // It may be negative if the NAL unit started in the previously consumed data.
+ int lengthToNalUnit = nalUnitOffset - offset;
+ if (lengthToNalUnit > 0) {
+ nalUnitData(dataArray, offset, nalUnitOffset);
+ }
+
+ int bytesWrittenPastPosition = limit - nalUnitOffset;
+ long absolutePosition = totalBytesWritten - bytesWrittenPastPosition;
+ // Indicate the end of the previous NAL unit. If the length to the start of the next unit
+ // is negative then we wrote too many bytes to the NAL buffers. Discard the excess bytes
+ // when notifying that the unit has ended.
+ endNalUnit(absolutePosition, bytesWrittenPastPosition,
+ lengthToNalUnit < 0 ? -lengthToNalUnit : 0, pesTimeUs);
+ // Indicate the start of the next NAL unit.
+ startNalUnit(absolutePosition, bytesWrittenPastPosition, nalUnitType, pesTimeUs);
+ // Continue scanning the data.
+ offset = nalUnitOffset + 3;
+ }
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ private void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) {
+ if (hasOutputFormat) {
+ sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs);
+ } else {
+ vps.startNalUnit(nalUnitType);
+ sps.startNalUnit(nalUnitType);
+ pps.startNalUnit(nalUnitType);
+ }
+ prefixSei.startNalUnit(nalUnitType);
+ suffixSei.startNalUnit(nalUnitType);
+ }
+
+ private void nalUnitData(byte[] dataArray, int offset, int limit) {
+ if (hasOutputFormat) {
+ sampleReader.readNalUnitData(dataArray, offset, limit);
+ } else {
+ vps.appendToNalUnit(dataArray, offset, limit);
+ sps.appendToNalUnit(dataArray, offset, limit);
+ pps.appendToNalUnit(dataArray, offset, limit);
+ }
+ prefixSei.appendToNalUnit(dataArray, offset, limit);
+ suffixSei.appendToNalUnit(dataArray, offset, limit);
+ }
+
+ private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) {
+ if (hasOutputFormat) {
+ sampleReader.endNalUnit(position, offset);
+ } else {
+ vps.endNalUnit(discardPadding);
+ sps.endNalUnit(discardPadding);
+ pps.endNalUnit(discardPadding);
+ if (vps.isCompleted() && sps.isCompleted() && pps.isCompleted()) {
+ output.format(parseMediaFormat(formatId, vps, sps, pps));
+ hasOutputFormat = true;
+ }
+ }
+ if (prefixSei.endNalUnit(discardPadding)) {
+ int unescapedLength = NalUnitUtil.unescapeStream(prefixSei.nalData, prefixSei.nalLength);
+ seiWrapper.reset(prefixSei.nalData, unescapedLength);
+
+ // Skip the NAL prefix and type.
+ seiWrapper.skipBytes(5);
+ seiReader.consume(pesTimeUs, seiWrapper);
+ }
+ if (suffixSei.endNalUnit(discardPadding)) {
+ int unescapedLength = NalUnitUtil.unescapeStream(suffixSei.nalData, suffixSei.nalLength);
+ seiWrapper.reset(suffixSei.nalData, unescapedLength);
+
+ // Skip the NAL prefix and type.
+ seiWrapper.skipBytes(5);
+ seiReader.consume(pesTimeUs, seiWrapper);
+ }
+ }
+
+ private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps,
+ NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) {
+ // Build codec-specific data.
+ byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength];
+ System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength);
+ System.arraycopy(sps.nalData, 0, csd, vps.nalLength, sps.nalLength);
+ System.arraycopy(pps.nalData, 0, csd, vps.nalLength + sps.nalLength, pps.nalLength);
+
+ // Parse the SPS NAL unit, as per H.265/HEVC (2014) 7.3.2.2.1.
+ 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.skipBit(); // sps_temporal_id_nesting_flag
+
+ // profile_tier_level(1, sps_max_sub_layers_minus1)
+ bitArray.skipBits(88); // if (profilePresentFlag) {...}
+ bitArray.skipBits(8); // general_level_idc
+ int toSkip = 0;
+ for (int i = 0; i < maxSubLayersMinus1; i++) {
+ if (bitArray.readBit()) { // sub_layer_profile_present_flag[i]
+ toSkip += 89;
+ }
+ if (bitArray.readBit()) { // sub_layer_level_present_flag[i]
+ toSkip += 8;
+ }
+ }
+ bitArray.skipBits(toSkip);
+ if (maxSubLayersMinus1 > 0) {
+ bitArray.skipBits(2 * (8 - maxSubLayersMinus1));
+ }
+
+ bitArray.readUnsignedExpGolombCodedInt(); // sps_seq_parameter_set_id
+ int chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt();
+ if (chromaFormatIdc == 3) {
+ bitArray.skipBit(); // separate_colour_plane_flag
+ }
+ int picWidthInLumaSamples = bitArray.readUnsignedExpGolombCodedInt();
+ int picHeightInLumaSamples = bitArray.readUnsignedExpGolombCodedInt();
+ if (bitArray.readBit()) { // conformance_window_flag
+ int confWinLeftOffset = bitArray.readUnsignedExpGolombCodedInt();
+ int confWinRightOffset = bitArray.readUnsignedExpGolombCodedInt();
+ int confWinTopOffset = bitArray.readUnsignedExpGolombCodedInt();
+ int confWinBottomOffset = bitArray.readUnsignedExpGolombCodedInt();
+ // H.265/HEVC (2014) Table 6-1
+ int subWidthC = chromaFormatIdc == 1 || chromaFormatIdc == 2 ? 2 : 1;
+ int subHeightC = chromaFormatIdc == 1 ? 2 : 1;
+ picWidthInLumaSamples -= subWidthC * (confWinLeftOffset + confWinRightOffset);
+ picHeightInLumaSamples -= subHeightC * (confWinTopOffset + confWinBottomOffset);
+ }
+ bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8
+ bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8
+ int log2MaxPicOrderCntLsbMinus4 = bitArray.readUnsignedExpGolombCodedInt();
+ // for (i = sps_sub_layer_ordering_info_present_flag ? 0 : sps_max_sub_layers_minus1; ...)
+ for (int i = bitArray.readBit() ? 0 : maxSubLayersMinus1; i <= maxSubLayersMinus1; i++) {
+ bitArray.readUnsignedExpGolombCodedInt(); // sps_max_dec_pic_buffering_minus1[i]
+ bitArray.readUnsignedExpGolombCodedInt(); // sps_max_num_reorder_pics[i]
+ bitArray.readUnsignedExpGolombCodedInt(); // sps_max_latency_increase_plus1[i]
+ }
+ bitArray.readUnsignedExpGolombCodedInt(); // log2_min_luma_coding_block_size_minus3
+ bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_luma_coding_block_size
+ bitArray.readUnsignedExpGolombCodedInt(); // log2_min_luma_transform_block_size_minus2
+ bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_luma_transform_block_size
+ bitArray.readUnsignedExpGolombCodedInt(); // max_transform_hierarchy_depth_inter
+ bitArray.readUnsignedExpGolombCodedInt(); // max_transform_hierarchy_depth_intra
+ // if (scaling_list_enabled_flag) { if (sps_scaling_list_data_present_flag) {...}}
+ boolean scalingListEnabled = bitArray.readBit();
+ if (scalingListEnabled && bitArray.readBit()) {
+ skipScalingList(bitArray);
+ }
+ bitArray.skipBits(2); // amp_enabled_flag (1), sample_adaptive_offset_enabled_flag (1)
+ if (bitArray.readBit()) { // pcm_enabled_flag
+ // pcm_sample_bit_depth_luma_minus1 (4), pcm_sample_bit_depth_chroma_minus1 (4)
+ 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.skipBit(); // pcm_loop_filter_disabled_flag
+ }
+ // Skips all short term reference picture sets.
+ skipShortTermRefPicSets(bitArray);
+ if (bitArray.readBit()) { // long_term_ref_pics_present_flag
+ // num_long_term_ref_pics_sps
+ for (int i = 0; i < bitArray.readUnsignedExpGolombCodedInt(); i++) {
+ int ltRefPicPocLsbSpsLength = log2MaxPicOrderCntLsbMinus4 + 4;
+ // lt_ref_pic_poc_lsb_sps[i], used_by_curr_pic_lt_sps_flag[i]
+ bitArray.skipBits(ltRefPicPocLsbSpsLength + 1);
+ }
+ }
+ bitArray.skipBits(2); // sps_temporal_mvp_enabled_flag, strong_intra_smoothing_enabled_flag
+ float pixelWidthHeightRatio = 1;
+ if (bitArray.readBit()) { // vui_parameters_present_flag
+ if (bitArray.readBit()) { // aspect_ratio_info_present_flag
+ int aspectRatioIdc = bitArray.readBits(8);
+ if (aspectRatioIdc == NalUnitUtil.EXTENDED_SAR) {
+ int sarWidth = bitArray.readBits(16);
+ int sarHeight = bitArray.readBits(16);
+ if (sarWidth != 0 && sarHeight != 0) {
+ pixelWidthHeightRatio = (float) sarWidth / sarHeight;
+ }
+ } else if (aspectRatioIdc < NalUnitUtil.ASPECT_RATIO_IDC_VALUES.length) {
+ pixelWidthHeightRatio = NalUnitUtil.ASPECT_RATIO_IDC_VALUES[aspectRatioIdc];
+ } else {
+ Log.w(TAG, "Unexpected aspect_ratio_idc value: " + aspectRatioIdc);
+ }
+ }
+ }
+
+ return Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H265, null, Format.NO_VALUE,
+ Format.NO_VALUE, picWidthInLumaSamples, picHeightInLumaSamples, Format.NO_VALUE,
+ Collections.singletonList(csd), Format.NO_VALUE, pixelWidthHeightRatio, null);
+ }
+
+ /**
+ * Skips scaling_list_data(). See H.265/HEVC (2014) 7.3.4.
+ */
+ private static void skipScalingList(ParsableNalUnitBitArray bitArray) {
+ for (int sizeId = 0; sizeId < 4; sizeId++) {
+ for (int matrixId = 0; matrixId < 6; matrixId += sizeId == 3 ? 3 : 1) {
+ if (!bitArray.readBit()) { // scaling_list_pred_mode_flag[sizeId][matrixId]
+ // scaling_list_pred_matrix_id_delta[sizeId][matrixId]
+ bitArray.readUnsignedExpGolombCodedInt();
+ } else {
+ int coefNum = Math.min(64, 1 << (4 + (sizeId << 1)));
+ if (sizeId > 1) {
+ // scaling_list_dc_coef_minus8[sizeId - 2][matrixId]
+ bitArray.readSignedExpGolombCodedInt();
+ }
+ for (int i = 0; i < coefNum; i++) {
+ bitArray.readSignedExpGolombCodedInt(); // scaling_list_delta_coef
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Reads the number of short term reference picture sets in a SPS as ue(v), then skips all of
+ * them. See H.265/HEVC (2014) 7.3.7.
+ */
+ private static void skipShortTermRefPicSets(ParsableNalUnitBitArray bitArray) {
+ int numShortTermRefPicSets = bitArray.readUnsignedExpGolombCodedInt();
+ boolean interRefPicSetPredictionFlag = false;
+ int numNegativePics;
+ int numPositivePics;
+ // As this method applies in a SPS, the only element of NumDeltaPocs accessed is the previous
+ // one, so we just keep track of that rather than storing the whole array.
+ // RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1) and delta_idx_minus1 is always zero in SPS.
+ int previousNumDeltaPocs = 0;
+ for (int stRpsIdx = 0; stRpsIdx < numShortTermRefPicSets; stRpsIdx++) {
+ if (stRpsIdx != 0) {
+ interRefPicSetPredictionFlag = bitArray.readBit();
+ }
+ if (interRefPicSetPredictionFlag) {
+ 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.skipBit(); // use_delta_flag[j]
+ }
+ }
+ } else {
+ numNegativePics = bitArray.readUnsignedExpGolombCodedInt();
+ numPositivePics = bitArray.readUnsignedExpGolombCodedInt();
+ previousNumDeltaPocs = numNegativePics + numPositivePics;
+ for (int i = 0; i < numNegativePics; i++) {
+ bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s0_minus1[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.skipBit(); // used_by_curr_pic_s1_flag[i]
+ }
+ }
+ }
+ }
+
+ private static final class SampleReader {
+
+ /**
+ * Offset in bytes of the first_slice_segment_in_pic_flag in a NAL unit containing a
+ * slice_segment_layer_rbsp.
+ */
+ private static final int FIRST_SLICE_FLAG_OFFSET = 2;
+
+ private final TrackOutput output;
+
+ // Per NAL unit state. A sample consists of one or more NAL units.
+ private long nalUnitStartPosition;
+ private boolean nalUnitHasKeyframeData;
+ private int nalUnitBytesRead;
+ private long nalUnitTimeUs;
+ private boolean lookingForFirstSliceFlag;
+ private boolean isFirstSlice;
+ private boolean isFirstParameterSet;
+
+ // Per sample state that gets reset at the start of each sample.
+ private boolean readingSample;
+ private boolean writingParameterSets;
+ private long samplePosition;
+ private long sampleTimeUs;
+ private boolean sampleIsKeyframe;
+
+ public SampleReader(TrackOutput output) {
+ this.output = output;
+ }
+
+ public void reset() {
+ lookingForFirstSliceFlag = false;
+ isFirstSlice = false;
+ isFirstParameterSet = false;
+ readingSample = false;
+ writingParameterSets = false;
+ }
+
+ public void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) {
+ isFirstSlice = false;
+ isFirstParameterSet = false;
+ nalUnitTimeUs = pesTimeUs;
+ nalUnitBytesRead = 0;
+ nalUnitStartPosition = position;
+
+ if (nalUnitType >= VPS_NUT) {
+ if (!writingParameterSets && readingSample) {
+ // This is a non-VCL NAL unit, so flush the previous sample.
+ outputSample(offset);
+ readingSample = false;
+ }
+ if (nalUnitType <= PPS_NUT) {
+ // This sample will have parameter sets at the start.
+ isFirstParameterSet = !writingParameterSets;
+ writingParameterSets = true;
+ }
+ }
+
+ // Look for the flag if this NAL unit contains a slice_segment_layer_rbsp.
+ nalUnitHasKeyframeData = (nalUnitType >= BLA_W_LP && nalUnitType <= CRA_NUT);
+ lookingForFirstSliceFlag = nalUnitHasKeyframeData || nalUnitType <= RASL_R;
+ }
+
+ public void readNalUnitData(byte[] data, int offset, int limit) {
+ if (lookingForFirstSliceFlag) {
+ int headerOffset = offset + FIRST_SLICE_FLAG_OFFSET - nalUnitBytesRead;
+ if (headerOffset < limit) {
+ isFirstSlice = (data[headerOffset] & 0x80) != 0;
+ lookingForFirstSliceFlag = false;
+ } else {
+ nalUnitBytesRead += limit - offset;
+ }
+ }
+ }
+
+ public void endNalUnit(long position, int offset) {
+ if (writingParameterSets && isFirstSlice) {
+ // This sample has parameter sets. Reset the key-frame flag based on the first slice.
+ sampleIsKeyframe = nalUnitHasKeyframeData;
+ writingParameterSets = false;
+ } else if (isFirstParameterSet || isFirstSlice) {
+ // This NAL unit is at the start of a new sample (access unit).
+ if (readingSample) {
+ // Output the sample ending before this NAL unit.
+ int nalUnitLength = (int) (position - nalUnitStartPosition);
+ outputSample(offset + nalUnitLength);
+ }
+ samplePosition = nalUnitStartPosition;
+ sampleTimeUs = nalUnitTimeUs;
+ readingSample = true;
+ sampleIsKeyframe = nalUnitHasKeyframeData;
+ }
+ }
+
+ private void outputSample(int offset) {
+ @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
+ int size = (int) (nalUnitStartPosition - samplePosition);
+ output.sampleMetadata(sampleTimeUs, flags, size, offset, null);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.java
new file mode 100644
index 0000000000..da63e143c2
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.java
@@ -0,0 +1,116 @@
+/*
+ * 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.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 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;
+
+/**
+ * Parses ID3 data and extracts individual text information frames.
+ */
+public final class Id3Reader implements ElementaryStreamReader {
+
+ private static final String TAG = "Id3Reader";
+
+ private final ParsableByteArray id3Header;
+
+ private TrackOutput output;
+
+ // State that should be reset on seek.
+ private boolean writingSample;
+
+ // Per sample state that gets reset at the start of each sample.
+ private long sampleTimeUs;
+ private int sampleSize;
+ private int sampleBytesRead;
+
+ public Id3Reader() {
+ id3Header = new ParsableByteArray(ID3_HEADER_LENGTH);
+ }
+
+ @Override
+ public void seek() {
+ writingSample = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);
+ output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3,
+ null, Format.NO_VALUE, null));
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) {
+ return;
+ }
+ writingSample = true;
+ sampleTimeUs = pesTimeUs;
+ sampleSize = 0;
+ sampleBytesRead = 0;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ if (!writingSample) {
+ return;
+ }
+ int bytesAvailable = data.bytesLeft();
+ if (sampleBytesRead < ID3_HEADER_LENGTH) {
+ // We're still reading the ID3 header.
+ int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_LENGTH - sampleBytesRead);
+ System.arraycopy(data.data, data.getPosition(), id3Header.data, sampleBytesRead,
+ headerBytesAvailable);
+ 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()
+ || '3' != id3Header.readUnsignedByte()) {
+ Log.w(TAG, "Discarding invalid ID3 tag");
+ writingSample = false;
+ return;
+ }
+ id3Header.skipBytes(3); // version (2) + flags (1)
+ sampleSize = ID3_HEADER_LENGTH + id3Header.readSynchSafeInt();
+ }
+ }
+ // Write data to the output.
+ int bytesToWrite = Math.min(bytesAvailable, sampleSize - sampleBytesRead);
+ output.sampleData(data, bytesToWrite);
+ sampleBytesRead += bytesToWrite;
+ }
+
+ @Override
+ public void packetFinished() {
+ if (!writingSample || sampleSize == 0 || sampleBytesRead != sampleSize) {
+ return;
+ }
+ output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ writingSample = false;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/LatmReader.java
new file mode 100644
index 0000000000..1a41adfa69
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<Integer, Integer> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java
new file mode 100644
index 0000000000..6fefab6314
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java
@@ -0,0 +1,223 @@
+/*
+ * 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.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.MpegAudioHeader;
+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.ParsableByteArray;
+
+/**
+ * Parses a continuous MPEG Audio byte stream and extracts individual frames.
+ */
+public final class MpegAudioReader implements ElementaryStreamReader {
+
+ private static final int STATE_FINDING_HEADER = 0;
+ private static final int STATE_READING_HEADER = 1;
+ private static final int STATE_READING_FRAME = 2;
+
+ private static final int HEADER_SIZE = 4;
+
+ private final ParsableByteArray headerScratch;
+ private final MpegAudioHeader header;
+ private final String language;
+
+ private String formatId;
+ private TrackOutput output;
+
+ private int state;
+ private int frameBytesRead;
+ private boolean hasOutputFormat;
+
+ // Used when finding the frame header.
+ private boolean lastByteWasFF;
+
+ // Parsed from the frame header.
+ private long frameDurationUs;
+ private int frameSize;
+
+ // The timestamp to attach to the next sample in the current packet.
+ private long timeUs;
+
+ public MpegAudioReader() {
+ this(null);
+ }
+
+ public MpegAudioReader(String language) {
+ state = STATE_FINDING_HEADER;
+ // The first byte of an MPEG Audio frame header is always 0xFF.
+ headerScratch = new ParsableByteArray(4);
+ headerScratch.data[0] = (byte) 0xFF;
+ header = new MpegAudioHeader();
+ this.language = language;
+ }
+
+ @Override
+ public void seek() {
+ state = STATE_FINDING_HEADER;
+ frameBytesRead = 0;
+ lastByteWasFF = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.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_HEADER:
+ findHeader(data);
+ break;
+ case STATE_READING_HEADER:
+ readHeaderRemainder(data);
+ break;
+ case STATE_READING_FRAME:
+ readFrameRemainder(data);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ /**
+ * Attempts to locate the start of the next frame header.
+ * <p>
+ * If a frame header is located then the state is changed to {@link #STATE_READING_HEADER}, the
+ * first two bytes of the header are written into {@link #headerScratch}, and the position of the
+ * source is advanced to the byte that immediately follows these two bytes.
+ * <p>
+ * If a frame header is not located then the position of the source is advanced to the limit, and
+ * the method should be called again with the next source to continue the search.
+ *
+ * @param source The source from which to read.
+ */
+ private void findHeader(ParsableByteArray source) {
+ byte[] data = source.data;
+ int startOffset = source.getPosition();
+ int endOffset = source.limit();
+ for (int i = startOffset; i < endOffset; i++) {
+ boolean byteIsFF = (data[i] & 0xFF) == 0xFF;
+ boolean found = lastByteWasFF && (data[i] & 0xE0) == 0xE0;
+ lastByteWasFF = byteIsFF;
+ if (found) {
+ source.setPosition(i + 1);
+ // Reset lastByteWasFF for next time.
+ lastByteWasFF = false;
+ headerScratch.data[1] = data[i];
+ frameBytesRead = 2;
+ state = STATE_READING_HEADER;
+ return;
+ }
+ }
+ source.setPosition(endOffset);
+ }
+
+ /**
+ * Attempts to read the remaining two bytes of the frame header.
+ * <p>
+ * If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME},
+ * the media format is output if this has not previously occurred, the four header bytes are
+ * output as sample data, and the position of the source is advanced to the byte that immediately
+ * follows the header.
+ * <p>
+ * If a frame header is read in full but cannot be parsed then the state is changed to
+ * {@link #STATE_READING_HEADER}.
+ * <p>
+ * If a frame header is not read in full then the position of the source is advanced to the limit,
+ * and the method should be called again with the next source to continue the read.
+ *
+ * @param source The source from which to read.
+ */
+ private void readHeaderRemainder(ParsableByteArray source) {
+ int bytesToRead = Math.min(source.bytesLeft(), HEADER_SIZE - frameBytesRead);
+ source.readBytes(headerScratch.data, frameBytesRead, bytesToRead);
+ frameBytesRead += bytesToRead;
+ if (frameBytesRead < HEADER_SIZE) {
+ // We haven't read the whole header yet.
+ return;
+ }
+
+ headerScratch.setPosition(0);
+ boolean parsedHeader = MpegAudioHeader.populateHeader(headerScratch.readInt(), header);
+ if (!parsedHeader) {
+ // We thought we'd located a frame header, but we hadn't.
+ frameBytesRead = 0;
+ state = STATE_READING_HEADER;
+ return;
+ }
+
+ frameSize = header.frameSize;
+ if (!hasOutputFormat) {
+ frameDurationUs = (C.MICROS_PER_SECOND * header.samplesPerFrame) / header.sampleRate;
+ Format format = Format.createAudioSampleFormat(formatId, header.mimeType, null,
+ Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate,
+ null, null, 0, language);
+ output.format(format);
+ hasOutputFormat = true;
+ }
+
+ headerScratch.setPosition(0);
+ output.sampleData(headerScratch, HEADER_SIZE);
+ state = STATE_READING_FRAME;
+ }
+
+ /**
+ * Attempts to read the remainder of the frame.
+ * <p>
+ * If a frame is read in full then true is returned. The frame will have been output, and the
+ * position of the source will have been advanced to the byte that immediately follows the end of
+ * the frame.
+ * <p>
+ * If a frame is not read in full then the position of the source will have been advanced to the
+ * limit, and the method should be called again with the next source to continue the read.
+ *
+ * @param source The source from which to read.
+ */
+ private void readFrameRemainder(ParsableByteArray source) {
+ int bytesToRead = Math.min(source.bytesLeft(), frameSize - frameBytesRead);
+ output.sampleData(source, bytesToRead);
+ frameBytesRead += bytesToRead;
+ if (frameBytesRead < frameSize) {
+ // We haven't read the whole of the frame yet.
+ return;
+ }
+
+ output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, frameSize, 0, null);
+ timeUs += frameDurationUs;
+ frameBytesRead = 0;
+ state = STATE_FINDING_HEADER;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java
new file mode 100644
index 0000000000..4941aa29a0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java
@@ -0,0 +1,109 @@
+/*
+ * 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.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.util.Arrays;
+
+/**
+ * A buffer that fills itself with data corresponding to a specific NAL unit, as it is
+ * encountered in the stream.
+ */
+/* package */ final class NalUnitTargetBuffer {
+
+ private final int targetType;
+
+ private boolean isFilling;
+ private boolean isCompleted;
+
+ public byte[] nalData;
+ public int nalLength;
+
+ public NalUnitTargetBuffer(int targetType, int initialCapacity) {
+ this.targetType = targetType;
+
+ // Initialize data with a start code in the first three bytes.
+ nalData = new byte[3 + initialCapacity];
+ nalData[2] = 1;
+ }
+
+ /**
+ * Resets the buffer, clearing any data that it holds.
+ */
+ public void reset() {
+ isFilling = false;
+ isCompleted = false;
+ }
+
+ /**
+ * Returns whether the buffer currently holds a complete NAL unit of the target type.
+ */
+ public boolean isCompleted() {
+ return isCompleted;
+ }
+
+ /**
+ * Called to indicate that a NAL unit has started.
+ *
+ * @param type The type of the NAL unit.
+ */
+ public void startNalUnit(int type) {
+ Assertions.checkState(!isFilling);
+ isFilling = type == targetType;
+ if (isFilling) {
+ // Skip the three byte start code when writing data.
+ nalLength = 3;
+ isCompleted = false;
+ }
+ }
+
+ /**
+ * Called to pass stream data. The data passed should not include the 3 byte start code.
+ *
+ * @param data Holds the data being passed.
+ * @param offset The offset of the data in {@code data}.
+ * @param limit The limit (exclusive) of the data in {@code data}.
+ */
+ public void appendToNalUnit(byte[] data, int offset, int limit) {
+ if (!isFilling) {
+ return;
+ }
+ int readLength = limit - offset;
+ if (nalData.length < nalLength + readLength) {
+ nalData = Arrays.copyOf(nalData, (nalLength + readLength) * 2);
+ }
+ System.arraycopy(data, offset, nalData, nalLength, readLength);
+ nalLength += readLength;
+ }
+
+ /**
+ * Called to indicate that a NAL unit has ended.
+ *
+ * @param discardPadding The number of excess bytes that were passed to
+ * {@link #appendToNalUnit(byte[], int, int)}, which should be discarded.
+ * @return Whether the ended NAL unit is of the target type.
+ */
+ public boolean endNalUnit(int discardPadding) {
+ if (!isFilling) {
+ return false;
+ }
+ nalLength -= discardPadding;
+ isFilling = false;
+ isCompleted = true;
+ return true;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.java
new file mode 100644
index 0000000000..86afe22563
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.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.extractor.ts;
+
+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;
+
+/**
+ * Parses PES packet data and extracts samples.
+ */
+public final class PesReader implements TsPayloadReader {
+
+ private static final String TAG = "PesReader";
+
+ private static final int STATE_FINDING_HEADER = 0;
+ private static final int STATE_READING_HEADER = 1;
+ private static final int STATE_READING_HEADER_EXTENSION = 2;
+ private static final int STATE_READING_BODY = 3;
+
+ private static final int HEADER_SIZE = 9;
+ private static final int MAX_HEADER_EXTENSION_SIZE = 10;
+ private static final int PES_SCRATCH_SIZE = 10; // max(HEADER_SIZE, MAX_HEADER_EXTENSION_SIZE)
+
+ private final ElementaryStreamReader reader;
+ private final ParsableBitArray pesScratch;
+
+ private int state;
+ private int bytesRead;
+
+ private TimestampAdjuster timestampAdjuster;
+ private boolean ptsFlag;
+ private boolean dtsFlag;
+ private boolean seenFirstDts;
+ private int extendedHeaderLength;
+ private int payloadSize;
+ private boolean dataAlignmentIndicator;
+ private long timeUs;
+
+ public PesReader(ElementaryStreamReader reader) {
+ this.reader = reader;
+ pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]);
+ state = STATE_FINDING_HEADER;
+ }
+
+ @Override
+ public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator) {
+ this.timestampAdjuster = timestampAdjuster;
+ reader.createTracks(extractorOutput, idGenerator);
+ }
+
+ // TsPayloadReader implementation.
+
+ @Override
+ public final void seek() {
+ state = STATE_FINDING_HEADER;
+ bytesRead = 0;
+ seenFirstDts = false;
+ reader.seek();
+ }
+
+ @Override
+ 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:
+ // Expected.
+ break;
+ case STATE_READING_HEADER_EXTENSION:
+ Log.w(TAG, "Unexpected start indicator reading extended header");
+ break;
+ case STATE_READING_BODY:
+ // If payloadSize == -1 then the length of the previous packet was unspecified, and so
+ // we only know that it's finished now that we've seen the start of the next one. This
+ // is expected. If payloadSize != -1, then the length of the previous packet was known,
+ // but we didn't receive that amount of data. This is not expected.
+ if (payloadSize != -1) {
+ Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes");
+ }
+ // Either way, notify the reader that it has now finished.
+ reader.packetFinished();
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ setState(STATE_READING_HEADER);
+ }
+
+ while (data.bytesLeft() > 0) {
+ switch (state) {
+ case STATE_FINDING_HEADER:
+ data.skipBytes(data.bytesLeft());
+ break;
+ case STATE_READING_HEADER:
+ if (continueRead(data, pesScratch.data, HEADER_SIZE)) {
+ setState(parseHeader() ? STATE_READING_HEADER_EXTENSION : STATE_FINDING_HEADER);
+ }
+ break;
+ case STATE_READING_HEADER_EXTENSION:
+ int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength);
+ // Read as much of the extended header as we're interested in, and skip the rest.
+ if (continueRead(data, pesScratch.data, readLength)
+ && continueRead(data, null, extendedHeaderLength)) {
+ parseHeaderExtension();
+ flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0;
+ reader.packetStarted(timeUs, flags);
+ setState(STATE_READING_BODY);
+ }
+ break;
+ case STATE_READING_BODY:
+ readLength = data.bytesLeft();
+ int padding = payloadSize == -1 ? 0 : readLength - payloadSize;
+ if (padding > 0) {
+ readLength -= padding;
+ data.setLimit(data.getPosition() + readLength);
+ }
+ reader.consume(data);
+ if (payloadSize != -1) {
+ payloadSize -= readLength;
+ if (payloadSize == 0) {
+ reader.packetFinished();
+ setState(STATE_READING_HEADER);
+ }
+ }
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ private void setState(int state) {
+ this.state = state;
+ bytesRead = 0;
+ }
+
+ /**
+ * 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, or {@code null} to skip.
+ * @param targetLength The target length of the read.
+ * @return Whether the target length has been reached.
+ */
+ private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {
+ int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);
+ if (bytesToRead <= 0) {
+ return true;
+ } else if (target == null) {
+ source.skipBytes(bytesToRead);
+ } else {
+ source.readBytes(target, bytesRead, bytesToRead);
+ }
+ bytesRead += bytesToRead;
+ return bytesRead == targetLength;
+ }
+
+ private boolean parseHeader() {
+ // Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of
+ // the header.
+ pesScratch.setPosition(0);
+ int startCodePrefix = pesScratch.readBits(24);
+ if (startCodePrefix != 0x000001) {
+ Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix);
+ payloadSize = -1;
+ return false;
+ }
+
+ pesScratch.skipBits(8); // stream_id.
+ int packetLength = pesScratch.readBits(16);
+ pesScratch.skipBits(5); // '10' (2), PES_scrambling_control (2), PES_priority (1)
+ dataAlignmentIndicator = pesScratch.readBit();
+ pesScratch.skipBits(2); // copyright (1), original_or_copy (1)
+ ptsFlag = pesScratch.readBit();
+ dtsFlag = pesScratch.readBit();
+ // ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1),
+ // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1)
+ pesScratch.skipBits(6);
+ extendedHeaderLength = pesScratch.readBits(8);
+
+ if (packetLength == 0) {
+ payloadSize = -1;
+ } else {
+ payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */
+ - HEADER_SIZE - extendedHeaderLength;
+ }
+ return true;
+ }
+
+ private void parseHeaderExtension() {
+ pesScratch.setPosition(0);
+ timeUs = C.TIME_UNSET;
+ if (ptsFlag) {
+ pesScratch.skipBits(4); // '0010' or '0011'
+ long pts = (long) pesScratch.readBits(3) << 30;
+ pesScratch.skipBits(1); // marker_bit
+ pts |= pesScratch.readBits(15) << 15;
+ pesScratch.skipBits(1); // marker_bit
+ pts |= pesScratch.readBits(15);
+ pesScratch.skipBits(1); // marker_bit
+ if (!seenFirstDts && dtsFlag) {
+ pesScratch.skipBits(4); // '0011'
+ long dts = (long) pesScratch.readBits(3) << 30;
+ pesScratch.skipBits(1); // marker_bit
+ dts |= pesScratch.readBits(15) << 15;
+ pesScratch.skipBits(1); // marker_bit
+ dts |= pesScratch.readBits(15);
+ pesScratch.skipBits(1); // marker_bit
+ // Subsequent PES packets may have earlier presentation timestamps than this one, but they
+ // should all be greater than or equal to this packet's decode timestamp. We feed the
+ // decode timestamp to the adjuster here so that in the case that this is the first to be
+ // fed, the adjuster will be able to compute an offset to apply such that the adjusted
+ // presentation timestamps of all future packets are non-negative.
+ timestampAdjuster.adjustTsTimestamp(dts);
+ seenFirstDts = true;
+ }
+ timeUs = timestampAdjuster.adjustTsTimestamp(pts);
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java
new file mode 100644
index 0000000000..acd08a2f12
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsDurationReader.java
new file mode 100644
index 0000000000..a5960fbe15
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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).
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
new file mode 100644
index 0000000000..8dcccbe459
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
@@ -0,0 +1,397 @@
+/*
+ * 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.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;
+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.ParsableBitArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.io.IOException;
+
+/**
+ * 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 Extractor[] {new PsExtractor()};
+
+ /* 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;
+ public static final int AUDIO_STREAM_MASK = 0xE0;
+ public static final int VIDEO_STREAM = 0xE0;
+ public static final int VIDEO_STREAM_MASK = 0xF0;
+
+ private final TimestampAdjuster timestampAdjuster;
+ private final SparseArray<PesReader> 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));
+ }
+
+ public PsExtractor(TimestampAdjuster timestampAdjuster) {
+ this.timestampAdjuster = timestampAdjuster;
+ psPacketBuffer = new ParsableByteArray(4096);
+ psPayloadReaders = new SparseArray<>();
+ durationReader = new PsDurationReader();
+ }
+
+ // Extractor implementation.
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ byte[] scratch = new byte[14];
+ input.peekFully(scratch, 0, 14);
+
+ // Verify the PACK_START_CODE for the first 4 bytes
+ if (PACK_START_CODE != (((scratch[0] & 0xFF) << 24) | ((scratch[1] & 0xFF) << 16)
+ | ((scratch[2] & 0xFF) << 8) | (scratch[3] & 0xFF))) {
+ return false;
+ }
+ // Verify the 01xxx1xx marker on the 5th byte
+ if ((scratch[4] & 0xC4) != 0x44) {
+ return false;
+ }
+ // Verify the xxxxx1xx marker on the 7th byte
+ if ((scratch[6] & 0x04) != 0x04) {
+ return false;
+ }
+ // Verify the xxxxx1xx marker on the 9th byte
+ if ((scratch[8] & 0x04) != 0x04) {
+ return false;
+ }
+ // Verify the xxxxxxx1 marker on the 10th byte
+ if ((scratch[9] & 0x01) != 0x01) {
+ return false;
+ }
+ // Verify the xxxxxx11 marker on the 13th byte
+ if ((scratch[12] & 0x03) != 0x03) {
+ return false;
+ }
+ // Read the stuffing length from the 14th byte (last 3 bits)
+ int packStuffingLength = scratch[13] & 0x07;
+ input.advancePeekPosition(packStuffingLength);
+ // Now check that the next 3 bytes are the beginning of an MPEG start code
+ input.peekFully(scratch, 0, 3);
+ return (PACKET_START_CODE_PREFIX == (((scratch[0] & 0xFF) << 16) | ((scratch[1] & 0xFF) << 8)
+ | (scratch[2] & 0xFF)));
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.output = output;
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ 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();
+ }
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @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;
+ }
+
+ psPacketBuffer.setPosition(0);
+ int nextStartCode = psPacketBuffer.readInt();
+ if (nextStartCode == MPEG_PROGRAM_END_CODE) {
+ return RESULT_END_OF_INPUT;
+ } else if (nextStartCode == PACK_START_CODE) {
+ // Now peek the rest of the pack_header.
+ input.peekFully(psPacketBuffer.data, 0, 10);
+
+ // We only care about the pack_stuffing_length in here, skip the first 77 bits.
+ psPacketBuffer.setPosition(9);
+
+ // Last 3 bits is the length.
+ int packStuffingLength = psPacketBuffer.readUnsignedByte() & 0x07;
+
+ // Now skip the stuffing and the pack header.
+ input.skipFully(packStuffingLength + 14);
+ return RESULT_CONTINUE;
+ } else if (nextStartCode == SYSTEM_HEADER_START_CODE) {
+ // We just skip all this, but we need to get the length first.
+ input.peekFully(psPacketBuffer.data, 0, 2);
+
+ // Length is the next 2 bytes.
+ psPacketBuffer.setPosition(0);
+ int systemHeaderLength = psPacketBuffer.readUnsignedShort();
+ input.skipFully(systemHeaderLength + 6);
+ return RESULT_CONTINUE;
+ } else if (((nextStartCode & 0xFFFFFF00) >> 8) != PACKET_START_CODE_PREFIX) {
+ input.skipFully(1); // Skip bytes until we see a valid start code again.
+ return RESULT_CONTINUE;
+ }
+
+ // We're at the start of a regular PES packet now.
+ // Get the stream ID off the last byte of the start code.
+ int streamId = nextStartCode & 0xFF;
+
+ // Check to see if we have this one in our map yet, and if not, then add it.
+ PesReader payloadReader = psPayloadReaders.get(streamId);
+ if (!foundAllTracks) {
+ if (payloadReader == null) {
+ ElementaryStreamReader elementaryStreamReader = null;
+ 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;
+ lastTrackPosition = input.getPosition();
+ } else if ((streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {
+ elementaryStreamReader = new MpegAudioReader();
+ foundAudioTrack = true;
+ 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);
+ elementaryStreamReader.createTracks(output, idGenerator);
+ payloadReader = new PesReader(elementaryStreamReader, timestampAdjuster);
+ psPayloadReaders.put(streamId, payloadReader);
+ }
+ }
+ long maxSearchPosition =
+ foundAudioTrack && foundVideoTrack
+ ? lastTrackPosition + MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND
+ : MAX_SEARCH_LENGTH;
+ if (input.getPosition() > maxSearchPosition) {
+ foundAllTracks = true;
+ output.endTracks();
+ }
+ }
+
+ // The next 2 bytes are the length. Once we have that we can consume the complete packet.
+ input.peekFully(psPacketBuffer.data, 0, 2);
+ psPacketBuffer.setPosition(0);
+ int payloadLength = psPacketBuffer.readUnsignedShort();
+ int pesLength = payloadLength + 6;
+
+ if (payloadReader == null) {
+ // Just skip this data.
+ input.skipFully(pesLength);
+ } else {
+ psPacketBuffer.reset(pesLength);
+ // Read the whole packet and the header for consumption.
+ input.readFully(psPacketBuffer.data, 0, pesLength);
+ psPacketBuffer.setPosition(6);
+ payloadReader.consume(psPacketBuffer);
+ psPacketBuffer.setLimit(psPacketBuffer.capacity());
+ }
+
+ return RESULT_CONTINUE;
+ }
+
+ // 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.
+ */
+ private static final class PesReader {
+
+ private static final int PES_SCRATCH_SIZE = 64;
+
+ private final ElementaryStreamReader pesPayloadReader;
+ private final TimestampAdjuster timestampAdjuster;
+ private final ParsableBitArray pesScratch;
+
+ private boolean ptsFlag;
+ private boolean dtsFlag;
+ private boolean seenFirstDts;
+ private int extendedHeaderLength;
+ private long timeUs;
+
+ public PesReader(ElementaryStreamReader pesPayloadReader, TimestampAdjuster timestampAdjuster) {
+ this.pesPayloadReader = pesPayloadReader;
+ this.timestampAdjuster = timestampAdjuster;
+ pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]);
+ }
+
+ /**
+ * Notifies the reader that a seek has occurred.
+ * <p>
+ * Following a call to this method, the data passed to the next invocation of
+ * {@link #consume(ParsableByteArray)} will not be a continuation of the data that was
+ * previously passed. Hence the reader should reset any internal state.
+ */
+ public void seek() {
+ seenFirstDts = false;
+ pesPayloadReader.seek();
+ }
+
+ /**
+ * 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) 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, TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR);
+ pesPayloadReader.consume(data);
+ // We always have complete PES packets with program stream.
+ pesPayloadReader.packetFinished();
+ }
+
+ private void parseHeader() {
+ // Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of
+ // the header.
+ // First 8 bits are skipped: '10' (2), PES_scrambling_control (2), PES_priority (1),
+ // data_alignment_indicator (1), copyright (1), original_or_copy (1)
+ pesScratch.skipBits(8);
+ ptsFlag = pesScratch.readBit();
+ dtsFlag = pesScratch.readBit();
+ // ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1),
+ // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1)
+ pesScratch.skipBits(6);
+ extendedHeaderLength = pesScratch.readBits(8);
+ }
+
+ private void parseHeaderExtension() {
+ timeUs = 0;
+ if (ptsFlag) {
+ pesScratch.skipBits(4); // '0010' or '0011'
+ long pts = (long) pesScratch.readBits(3) << 30;
+ pesScratch.skipBits(1); // marker_bit
+ pts |= pesScratch.readBits(15) << 15;
+ pesScratch.skipBits(1); // marker_bit
+ pts |= pesScratch.readBits(15);
+ pesScratch.skipBits(1); // marker_bit
+ if (!seenFirstDts && dtsFlag) {
+ pesScratch.skipBits(4); // '0011'
+ long dts = (long) pesScratch.readBits(3) << 30;
+ pesScratch.skipBits(1); // marker_bit
+ dts |= pesScratch.readBits(15) << 15;
+ pesScratch.skipBits(1); // marker_bit
+ dts |= pesScratch.readBits(15);
+ pesScratch.skipBits(1); // marker_bit
+ // Subsequent PES packets may have earlier presentation timestamps than this one, but they
+ // should all be greater than or equal to this packet's decode timestamp. We feed the
+ // decode timestamp to the adjuster here so that in the case that this is the first to be
+ // fed, the adjuster will be able to compute an offset to apply such that the adjusted
+ // presentation timestamps of all future packets are non-negative.
+ timestampAdjuster.adjustTsTimestamp(dts);
+ seenFirstDts = true;
+ }
+ timeUs = timestampAdjuster.adjustTsTimestamp(pts);
+ }
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java
new file mode 100644
index 0000000000..b5942b8bcc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java
@@ -0,0 +1,49 @@
+/*
+ * 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.ts;
+
+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.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+
+/**
+ * Reads section data.
+ */
+public interface SectionPayloadReader {
+
+ /**
+ * Initializes the section payload reader.
+ *
+ * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
+ * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data.
+ * @param idGenerator A {@link PesReader.TrackIdGenerator} that generates unique track ids for the
+ * {@link TrackOutput}s.
+ */
+ void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator);
+
+ /**
+ * Called by a {@link SectionReader} when a full section is received.
+ *
+ * @param sectionData The data belonging to a section starting from the table_id. If
+ * section_syntax_indicator is set to '1', {@code sectionData} excludes the CRC_32 field.
+ * Otherwise, all bytes belonging to the table section are included.
+ */
+ void consume(ParsableByteArray sectionData);
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java
new file mode 100644
index 0000000000..61b53cfa72
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java
@@ -0,0 +1,134 @@
+/*
+ * 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.ts;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+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;
+
+/**
+ * Reads section data packets and feeds the whole sections to a given {@link SectionPayloadReader}.
+ * Useful information on PSI sections can be found in ISO/IEC 13818-1, section 2.4.4.
+ */
+public final class SectionReader implements TsPayloadReader {
+
+ private static final int SECTION_HEADER_LENGTH = 3;
+ private static final int DEFAULT_SECTION_BUFFER_LENGTH = 32;
+ private static final int MAX_SECTION_LENGTH = 4098;
+
+ private final SectionPayloadReader reader;
+ private final ParsableByteArray sectionData;
+
+ private int totalSectionLength;
+ private int bytesRead;
+ private boolean sectionSyntaxIndicator;
+ private boolean waitingForPayloadStart;
+
+ public SectionReader(SectionPayloadReader reader) {
+ this.reader = reader;
+ sectionData = new ParsableByteArray(DEFAULT_SECTION_BUFFER_LENGTH);
+ }
+
+ @Override
+ public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator) {
+ reader.init(timestampAdjuster, extractorOutput, idGenerator);
+ waitingForPayloadStart = true;
+ }
+
+ @Override
+ public void seek() {
+ waitingForPayloadStart = true;
+ }
+
+ @Override
+ 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();
+ payloadStartPosition = data.getPosition() + payloadStartOffset;
+ }
+
+ if (waitingForPayloadStart) {
+ if (!payloadUnitStartIndicator) {
+ return;
+ }
+ waitingForPayloadStart = false;
+ data.setPosition(payloadStartPosition);
+ bytesRead = 0;
+ }
+
+ while (data.bytesLeft() > 0) {
+ if (bytesRead < SECTION_HEADER_LENGTH) {
+ // Note: see ISO/IEC 13818-1, section 2.4.4.3 for detailed information on the format of
+ // the header.
+ if (bytesRead == 0) {
+ int tableId = data.readUnsignedByte();
+ data.setPosition(data.getPosition() - 1);
+ if (tableId == 0xFF /* forbidden value */) {
+ // No more sections in this ts packet.
+ waitingForPayloadStart = true;
+ return;
+ }
+ }
+ int headerBytesToRead = Math.min(data.bytesLeft(), SECTION_HEADER_LENGTH - bytesRead);
+ data.readBytes(sectionData.data, bytesRead, headerBytesToRead);
+ bytesRead += headerBytesToRead;
+ if (bytesRead == SECTION_HEADER_LENGTH) {
+ sectionData.reset(SECTION_HEADER_LENGTH);
+ sectionData.skipBytes(1); // Skip table id (8).
+ int secondHeaderByte = sectionData.readUnsignedByte();
+ int thirdHeaderByte = sectionData.readUnsignedByte();
+ sectionSyntaxIndicator = (secondHeaderByte & 0x80) != 0;
+ totalSectionLength =
+ (((secondHeaderByte & 0x0F) << 8) | thirdHeaderByte) + SECTION_HEADER_LENGTH;
+ if (sectionData.capacity() < totalSectionLength) {
+ // Ensure there is enough space to keep the whole section.
+ byte[] bytes = sectionData.data;
+ sectionData.reset(
+ Math.min(MAX_SECTION_LENGTH, Math.max(totalSectionLength, bytes.length * 2)));
+ System.arraycopy(bytes, 0, sectionData.data, 0, SECTION_HEADER_LENGTH);
+ }
+ }
+ } else {
+ // Reading the body.
+ int bodyBytesToRead = Math.min(data.bytesLeft(), totalSectionLength - bytesRead);
+ data.readBytes(sectionData.data, bytesRead, bodyBytesToRead);
+ bytesRead += bodyBytesToRead;
+ if (bytesRead == totalSectionLength) {
+ if (sectionSyntaxIndicator) {
+ // This section has common syntax as defined in ISO/IEC 13818-1, section 2.4.4.11.
+ if (Util.crc32(sectionData.data, 0, totalSectionLength, 0xFFFFFFFF) != 0) {
+ // The CRC is invalid so discard the section.
+ waitingForPayloadStart = true;
+ return;
+ }
+ sectionData.reset(totalSectionLength - 4); // Exclude the CRC_32 field.
+ } else {
+ // This is a private section with private defined syntax.
+ sectionData.reset(totalSectionLength);
+ }
+ reader.consume(sectionData);
+ bytesRead = 0;
+ }
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.java
new file mode 100644
index 0000000000..88ea482be4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.java
@@ -0,0 +1,73 @@
+/*
+ * 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.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.extractor.ts.TsPayloadReader.TrackIdGenerator;
+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 SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}. */
+public final class SeiReader {
+
+ private final List<Format> closedCaptionFormats;
+ private final TrackOutput[] outputs;
+
+ /**
+ * @param closedCaptionFormats A list of formats for the closed caption channels to expose.
+ */
+ public SeiReader(List<Format> closedCaptionFormats) {
+ this.closedCaptionFormats = closedCaptionFormats;
+ outputs = new TrackOutput[closedCaptionFormats.size()];
+ }
+
+ public void createTracks(ExtractorOutput extractorOutput, 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);
+ 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;
+ }
+ }
+
+ public void consume(long pesTimeUs, ParsableByteArray seiBuffer) {
+ CeaUtil.consume(pesTimeUs, seiBuffer, outputs);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java
new file mode 100644
index 0000000000..17223bad7c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java
@@ -0,0 +1,62 @@
+/*
+ * 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.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.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+
+/**
+ * Parses splice info sections as defined by SCTE35.
+ */
+public final class SpliceInfoSectionReader implements SectionPayloadReader {
+
+ private TimestampAdjuster timestampAdjuster;
+ private TrackOutput output;
+ private boolean formatDeclared;
+
+ @Override
+ public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TsPayloadReader.TrackIdGenerator idGenerator) {
+ this.timestampAdjuster = timestampAdjuster;
+ idGenerator.generateNewId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);
+ output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_SCTE35,
+ null, Format.NO_VALUE, null));
+ }
+
+ @Override
+ public void consume(ParsableByteArray sectionData) {
+ if (!formatDeclared) {
+ if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) {
+ // There is not enough information to initialize the timestamp adjuster.
+ return;
+ }
+ output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35,
+ timestampAdjuster.getTimestampOffsetUs()));
+ formatDeclared = true;
+ }
+ int sampleSize = sectionData.bytesLeft();
+ output.sampleData(sectionData, sampleSize);
+ output.sampleMetadata(timestampAdjuster.getLastAdjustedTimestampUs(), C.BUFFER_FLAG_KEY_FRAME,
+ sampleSize, 0, null);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java
new file mode 100644
index 0000000000..136691bdaf
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java
new file mode 100644
index 0000000000..ed4b66a7e4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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).
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
new file mode 100644
index 0000000000..a52e56bd32
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
@@ -0,0 +1,698 @@
+/*
+ * 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.ts;
+
+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;
+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.extractor.ts.DefaultTsPayloadReaderFactory.Flags;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.DvbSubtitleInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
+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.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;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 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 Extractor[] {new TsExtractor()};
+
+ /**
+ * Modes for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} or {@link
+ * #MODE_HLS}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @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_MULTI_PMT = 0;
+ /**
+ * Assume only one PMT will be contained in the stream, even if more are declared by the PAT.
+ */
+ public static final int MODE_SINGLE_PMT = 1;
+ /**
+ * Enable single PMT mode, map {@link TrackOutput}s by their type (instead of PID) and ignore
+ * continuity counters.
+ */
+ public static final int MODE_HLS = 2;
+
+ 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_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;
+ public static final int TS_STREAM_TYPE_ID3 = 0x15;
+ public static final int TS_STREAM_TYPE_SPLICE_INFO = 0x86;
+ public static final int TS_STREAM_TYPE_DVBSUBS = 0x59;
+
+ 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 = 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_SIZE = TS_PACKET_SIZE * 50;
+ private static final int SNIFF_TS_PACKET_COUNT = 5;
+
+ private final @Mode int mode;
+ private final List<TimestampAdjuster> timestampAdjusters;
+ private final ParsableByteArray tsPacketBuffer;
+ private final SparseIntArray continuityCounters;
+ private final TsPayloadReader.Factory payloadReaderFactory;
+ private final SparseArray<TsPayloadReader> 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);
+ }
+
+ /**
+ * @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory}
+ * {@code FLAG_*} values that control the behavior of the payload readers.
+ */
+ public TsExtractor(@Flags int defaultTsPayloadReaderFlags) {
+ 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_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,
+ TsPayloadReader.Factory payloadReaderFactory) {
+ this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory);
+ this.mode = mode;
+ if (mode == MODE_SINGLE_PMT || mode == MODE_HLS) {
+ timestampAdjusters = Collections.singletonList(timestampAdjuster);
+ } else {
+ timestampAdjusters = new ArrayList<>();
+ timestampAdjusters.add(timestampAdjuster);
+ }
+ 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();
+ }
+
+ // Extractor implementation.
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ byte[] buffer = tsPacketBuffer.data;
+ 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;
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.output = output;
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ Assertions.checkState(mode != MODE_HLS);
+ int timestampAdjustersCount = timestampAdjusters.size();
+ for (int i = 0; i < timestampAdjustersCount; i++) {
+ 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();
+ for (int i = 0; i < tsPayloadReaders.size(); i++) {
+ tsPayloadReaders.valueAt(i).seek();
+ }
+ bytesSinceLastSync = 0;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ 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);
+ }
+ 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);
+ }
+ }
+
+ if (!fillBufferWithAtLeastOnePacket(input)) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ int endOfPacket = findEndOfFirstTsPacketInBuffer();
+ int limit = tsPacketBuffer.limit();
+ if (endOfPacket > limit) {
+ return RESULT_CONTINUE;
+ }
+
+ @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;
+ }
+ 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.
+ if (mode != MODE_HLS) {
+ int continuityCounter = tsPacketHeader & 0xF;
+ int previousCounter = continuityCounters.get(pid, continuityCounter - 1);
+ continuityCounters.put(pid, continuityCounter);
+ if (previousCounter == continuityCounter) {
+ // 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();
+ 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.
+ 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);
+ return RESULT_CONTINUE;
+ }
+
+ // 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.
+ *
+ * <p>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();
+ SparseArray<TsPayloadReader> initialPayloadReaders =
+ payloadReaderFactory.createInitialPayloadReaders();
+ int initialPayloadReadersSize = initialPayloadReaders.size();
+ for (int i = 0; i < initialPayloadReadersSize; i++) {
+ tsPayloadReaders.put(initialPayloadReaders.keyAt(i), initialPayloadReaders.valueAt(i));
+ }
+ tsPayloadReaders.put(TS_PAT_PID, new SectionReader(new PatReader()));
+ id3Reader = null;
+ }
+
+ /**
+ * Parses Program Association Table data.
+ */
+ private class PatReader implements SectionPayloadReader {
+
+ private final ParsableBitArray patScratch;
+
+ public PatReader() {
+ patScratch = new ParsableBitArray(new byte[4]);
+ }
+
+ @Override
+ public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator) {
+ // Do nothing.
+ }
+
+ @Override
+ public void consume(ParsableByteArray sectionData) {
+ int tableId = sectionData.readUnsignedByte();
+ if (tableId != 0x00 /* program_association_section */) {
+ // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment.
+ return;
+ }
+ // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12),
+ // transport_stream_id (16), reserved (2), version_number (5), current_next_indicator (1),
+ // section_number (8), last_section_number (8)
+ sectionData.skipBytes(7);
+
+ int programCount = sectionData.bytesLeft() / 4;
+ for (int i = 0; i < programCount; i++) {
+ sectionData.readBytes(patScratch, 4);
+ int programNumber = patScratch.readBits(16);
+ patScratch.skipBits(3); // reserved (3)
+ if (programNumber == 0) {
+ patScratch.skipBits(13); // network_PID (13)
+ } else {
+ int pid = patScratch.readBits(13);
+ tsPayloadReaders.put(pid, new SectionReader(new PmtReader(pid)));
+ remainingPmts++;
+ }
+ }
+ if (mode != MODE_HLS) {
+ tsPayloadReaders.remove(TS_PAT_PID);
+ }
+ }
+
+ }
+
+ /**
+ * Parses Program Map Table.
+ */
+ private class PmtReader implements SectionPayloadReader {
+
+ private static final int TS_PMT_DESC_REGISTRATION = 0x05;
+ private static final int TS_PMT_DESC_ISO639_LANG = 0x0A;
+ 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<TsPayloadReader> 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;
+ }
+
+ @Override
+ public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator) {
+ // Do nothing.
+ }
+
+ @Override
+ public void consume(ParsableByteArray sectionData) {
+ int tableId = sectionData.readUnsignedByte();
+ if (tableId != 0x02 /* TS_program_map_section */) {
+ // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment.
+ return;
+ }
+ // TimestampAdjuster assignment.
+ TimestampAdjuster timestampAdjuster;
+ if (mode == MODE_SINGLE_PMT || mode == MODE_HLS || remainingPmts == 1) {
+ timestampAdjuster = timestampAdjusters.get(0);
+ } else {
+ timestampAdjuster = new TimestampAdjuster(
+ timestampAdjusters.get(0).getFirstSampleTimestampUs());
+ timestampAdjusters.add(timestampAdjuster);
+ }
+
+ // 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)
+ 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);
+ pmtScratch.skipBits(4);
+ int programInfoLength = pmtScratch.readBits(12);
+
+ // Skip the descriptors.
+ sectionData.skipBytes(programInfoLength);
+
+ 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, 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);
+ int streamType = pmtScratch.readBits(8);
+ pmtScratch.skipBits(3); // reserved
+ int elementaryPid = pmtScratch.readBits(13);
+ pmtScratch.skipBits(4); // reserved
+ int esInfoLength = pmtScratch.readBits(12); // ES_info_length.
+ EsInfo esInfo = readEsInfo(sectionData, esInfoLength);
+ if (streamType == 0x06) {
+ streamType = esInfo.streamType;
+ }
+ remainingEntriesLength -= esInfoLength + 5;
+
+ int trackId = mode == MODE_HLS ? streamType : elementaryPid;
+ if (trackIds.get(trackId)) {
+ continue;
+ }
+
+ 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));
+ }
+ tsPayloadReaders.put(trackPid, reader);
+ }
+ }
+
+ if (mode == MODE_HLS) {
+ if (!tracksEnded) {
+ output.endTracks();
+ remainingPmts = 0;
+ tracksEnded = true;
+ }
+ } else {
+ tsPayloadReaders.remove(pid);
+ remainingPmts = mode == MODE_SINGLE_PMT ? 0 : remainingPmts - 1;
+ if (remainingPmts == 0) {
+ output.endTracks();
+ tracksEnded = true;
+ }
+ }
+ }
+
+ /**
+ * Returns the stream info read from the available descriptors. Sets {@code data}'s position to
+ * the end of the descriptors.
+ *
+ * @param data A buffer with its position set to the start of the first descriptor.
+ * @param length The length of descriptors to read from the current position in {@code data}.
+ * @return The stream info read from the available descriptors.
+ */
+ private EsInfo readEsInfo(ParsableByteArray data, int length) {
+ int descriptorsStartPosition = data.getPosition();
+ int descriptorsEndPosition = descriptorsStartPosition + length;
+ int streamType = -1;
+ String language = null;
+ List<DvbSubtitleInfo> dvbSubtitleInfos = null;
+ while (data.getPosition() < descriptorsEndPosition) {
+ int descriptorTag = data.readUnsignedByte();
+ int descriptorLength = data.readUnsignedByte();
+ int positionOfNextDescriptor = data.getPosition() + descriptorLength;
+ if (descriptorTag == TS_PMT_DESC_REGISTRATION) { // registration_descriptor
+ long formatIdentifier = data.readUnsignedInt();
+ if (formatIdentifier == AC3_FORMAT_IDENTIFIER) {
+ 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;
+ }
+ } else if (descriptorTag == TS_PMT_DESC_AC3) { // AC-3_descriptor in DVB (ETSI EN 300 468)
+ 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) {
+ language = data.readString(3).trim();
+ // Audio type is ignored.
+ } else if (descriptorTag == TS_PMT_DESC_DVBSUBS) {
+ streamType = TS_STREAM_TYPE_DVBSUBS;
+ dvbSubtitleInfos = new ArrayList<>();
+ while (data.getPosition() < positionOfNextDescriptor) {
+ String dvbLanguage = data.readString(3).trim();
+ int dvbSubtitlingType = data.readUnsignedByte();
+ byte[] initializationData = new byte[4];
+ data.readBytes(initializationData, 0, 4);
+ dvbSubtitleInfos.add(new DvbSubtitleInfo(dvbLanguage, dvbSubtitlingType,
+ initializationData));
+ }
+ }
+ // Skip unused bytes of current descriptor.
+ data.skipBytes(positionOfNextDescriptor - data.getPosition());
+ }
+ data.setPosition(descriptorsEndPosition);
+ return new EsInfo(streamType, language, dvbSubtitleInfos,
+ Arrays.copyOfRange(data.data, descriptorsStartPosition, descriptorsEndPosition));
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java
new file mode 100644
index 0000000000..940c1c7937
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java
@@ -0,0 +1,232 @@
+/*
+ * 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.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;
+
+/**
+ * Parses TS packet payload data.
+ */
+public interface TsPayloadReader {
+
+ /**
+ * Factory of {@link TsPayloadReader} instances.
+ */
+ interface Factory {
+
+ /**
+ * Returns the initial mapping from PIDs to payload readers.
+ * <p>
+ * This method allows the injection of payload readers for reserved PIDs, excluding PID 0.
+ *
+ * @return A {@link SparseArray} that maps PIDs to payload readers.
+ */
+ SparseArray<TsPayloadReader> createInitialPayloadReaders();
+
+ /**
+ * Returns a {@link TsPayloadReader} for a given stream type and elementary stream information.
+ * May return null if the stream type is not supported.
+ *
+ * @param streamType Stream type value as defined in the PMT entry or associated descriptors.
+ * @param esInfo Information associated to the elementary stream provided in the PMT.
+ * @return A {@link TsPayloadReader} for the packet stream carried by the provided pid.
+ * {@code null} if the stream is not supported.
+ */
+ TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo);
+
+ }
+
+ /**
+ * Holds information associated with a PMT entry.
+ */
+ final class EsInfo {
+
+ public final int streamType;
+ public final String language;
+ public final List<DvbSubtitleInfo> dvbSubtitleInfos;
+ public final byte[] descriptorBytes;
+
+ /**
+ * @param streamType The type of the stream as defined by the
+ * {@link TsExtractor}{@code .TS_STREAM_TYPE_*}.
+ * @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18.
+ * @param dvbSubtitleInfos Information about DVB subtitles associated to the stream.
+ * @param descriptorBytes The descriptor bytes associated to the stream.
+ */
+ public EsInfo(int streamType, String language, List<DvbSubtitleInfo> dvbSubtitleInfos,
+ byte[] descriptorBytes) {
+ this.streamType = streamType;
+ this.language = language;
+ this.dvbSubtitleInfos =
+ dvbSubtitleInfos == null
+ ? Collections.emptyList()
+ : Collections.unmodifiableList(dvbSubtitleInfos);
+ this.descriptorBytes = descriptorBytes;
+ }
+
+ }
+
+ /**
+ * Holds information about a DVB subtitle, as defined in ETSI EN 300 468 V1.11.1 section 6.2.41.
+ */
+ final class DvbSubtitleInfo {
+
+ public final String language;
+ public final int type;
+ public final byte[] initializationData;
+
+ /**
+ * @param language The ISO 639-2 three-letter language code.
+ * @param type The subtitling type.
+ * @param initializationData The composition and ancillary page ids.
+ */
+ public DvbSubtitleInfo(String language, int type, byte[] initializationData) {
+ this.language = language;
+ this.type = type;
+ this.initializationData = initializationData;
+ }
+
+ }
+
+ /**
+ * Generates track ids for initializing {@link TsPayloadReader}s' {@link TrackOutput}s.
+ */
+ final class TrackIdGenerator {
+
+ private static final int ID_UNSET = Integer.MIN_VALUE;
+
+ private final String formatIdPrefix;
+ private final int firstTrackId;
+ private final int trackIdIncrement;
+ private int trackId;
+ private String formatId;
+
+ public TrackIdGenerator(int firstTrackId, int trackIdIncrement) {
+ this(ID_UNSET, firstTrackId, trackIdIncrement);
+ }
+
+ public TrackIdGenerator(int programNumber, int firstTrackId, int trackIdIncrement) {
+ this.formatIdPrefix = programNumber != ID_UNSET ? programNumber + "/" : "";
+ this.firstTrackId = firstTrackId;
+ this.trackIdIncrement = trackIdIncrement;
+ trackId = ID_UNSET;
+ }
+
+ /**
+ * Generates a new set of track and track format ids. Must be called before {@code get*}
+ * methods.
+ */
+ public void generateNewId() {
+ trackId = trackId == ID_UNSET ? firstTrackId : trackId + trackIdIncrement;
+ formatId = formatIdPrefix + trackId;
+ }
+
+ /**
+ * Returns the last generated track id. Must be called after the first {@link #generateNewId()}
+ * call.
+ *
+ * @return The last generated track id.
+ */
+ public int getTrackId() {
+ maybeThrowUninitializedError();
+ return trackId;
+ }
+
+ /**
+ * Returns the last generated format id, with the format {@code "programNumber/trackId"}. If no
+ * {@code programNumber} was provided, the {@code trackId} alone is used as format id. Must be
+ * called after the first {@link #generateNewId()} call.
+ *
+ * @return The last generated format id, with the format {@code "programNumber/trackId"}. If no
+ * {@code programNumber} was provided, the {@code trackId} alone is used as
+ * format id.
+ */
+ public String getFormatId() {
+ maybeThrowUninitializedError();
+ return formatId;
+ }
+
+ private void maybeThrowUninitializedError() {
+ if (trackId == ID_UNSET) {
+ throw new IllegalStateException("generateNewId() must be called before retrieving ids.");
+ }
+ }
+
+ }
+
+ /**
+ * 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.
+ *
+ * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
+ * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data.
+ * @param idGenerator A {@link PesReader.TrackIdGenerator} that generates unique track ids for the
+ * {@link TrackOutput}s.
+ */
+ void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator);
+
+ /**
+ * Notifies the reader that a seek has occurred.
+ *
+ * <p>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();
+
+ /**
+ * Consumes the payload of a TS packet.
+ *
+ * @param data The TS packet. The position will be set to the start of the payload.
+ * @param flags See {@link Flags}.
+ * @throws ParserException If the payload could not be parsed.
+ */
+ void consume(ParsableByteArray data, @Flags int flags) throws ParserException;
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/TsUtil.java
new file mode 100644
index 0000000000..8cd24ff1e9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/UserDataReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ts/UserDataReader.java
new file mode 100644
index 0000000000..fb56fe379c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<Format> closedCaptionFormats;
+ private final TrackOutput[] outputs;
+
+ public UserDataReader(List<Format> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.java
new file mode 100644
index 0000000000..d4ac3ef8e1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.java
@@ -0,0 +1,562 @@
+/*
+ * 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.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.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;
+
+/**
+ * Extracts data from WAV byte streams.
+ */
+public final class WavExtractor implements Extractor {
+
+ /**
+ * 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.
+ */
+ private static final int TARGET_SAMPLES_PER_SECOND = 10;
+
+ /** 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;
+
+ public WavExtractor() {
+ dataStartPosition = C.POSITION_UNSET;
+ dataEndPosition = C.POSITION_UNSET;
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ return WavHeaderReader.peek(input) != null;
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ extractorOutput = output;
+ trackOutput = output.track(0, C.TRACK_TYPE_AUDIO);
+ output.endTracks();
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ if (outputWriter != null) {
+ outputWriter.reset(timeUs);
+ }
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ 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.");
+ }
+
+ 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<Long, Long> 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.
+ *
+ * <p>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.
+ *
+ * <p>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);
+ }
+
+ @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;
+ }
+ }
+
+ // 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;
+ }
+
+ return bytesLeft <= 0;
+ }
+ }
+
+ private static final class ImaAdPcmOutputWriter implements OutputWriter {
+
+ private static final int[] INDEX_TABLE = {
+ -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8
+ };
+
+ 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
+ };
+
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.java
new file mode 100644
index 0000000000..bc6cf8999b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.java
@@ -0,0 +1,55 @@
+/*
+ * 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.wav;
+
+/** Header for a WAV file. */
+/* package */ final class WavHeader {
+
+ /**
+ * 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;
+
+ public WavHeader(
+ int formatType,
+ int numChannels,
+ int frameRateHz,
+ int averageBytesPerSecond,
+ int blockSize,
+ int bitsPerSample,
+ byte[] extraData) {
+ this.formatType = formatType;
+ this.numChannels = numChannels;
+ this.frameRateHz = frameRateHz;
+ this.averageBytesPerSecond = averageBytesPerSecond;
+ this.blockSize = blockSize;
+ this.bitsPerSample = bitsPerSample;
+ this.extraData = extraData;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java
new file mode 100644
index 0000000000..1c36aaa3c3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java
@@ -0,0 +1,191 @@
+/*
+ * 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.wav;
+
+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 {
+
+ private static final String TAG = "WavHeaderReader";
+
+ /**
+ * Peeks and returns a {@code WavHeader}.
+ *
+ * @param input Input stream to peek the WAV header from.
+ * @throws ParserException If the input file is an incorrect RIFF WAV.
+ * @throws IOException If peeking from the input fails.
+ * @throws InterruptedException If interrupted while peeking from input.
+ * @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);
+
+ // Allocate a scratch buffer large enough to store the format chunk.
+ ParsableByteArray scratch = new ParsableByteArray(16);
+
+ // Attempt to read the RIFF chunk.
+ ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch);
+ if (chunkHeader.id != WavUtil.RIFF_FOURCC) {
+ return null;
+ }
+
+ input.peekFully(scratch.data, 0, 4);
+ scratch.setPosition(0);
+ int riffFormat = scratch.readInt();
+ 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 != WavUtil.FMT_FOURCC) {
+ input.advancePeekPosition((int) chunkHeader.size);
+ chunkHeader = ChunkHeader.peek(input, scratch);
+ }
+
+ Assertions.checkState(chunkHeader.size >= 16);
+ input.peekFully(scratch.data, 0, 16);
+ scratch.setPosition(0);
+ int audioFormatType = scratch.readLittleEndianUnsignedShort();
+ int numChannels = scratch.readLittleEndianUnsignedShort();
+ int frameRateHz = scratch.readLittleEndianUnsignedIntToInt();
+ int averageBytesPerSecond = scratch.readLittleEndianUnsignedIntToInt();
+ int blockSize = scratch.readLittleEndianUnsignedShort();
+ int bitsPerSample = scratch.readLittleEndianUnsignedShort();
+
+ 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;
+ }
+
+ return new WavHeader(
+ audioFormatType,
+ numChannels,
+ frameRateHz,
+ averageBytesPerSecond,
+ blockSize,
+ bitsPerSample,
+ extraData);
+ }
+
+ /**
+ * 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 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 Pair<Long, Long> skipToData(ExtractorInput input)
+ throws IOException, InterruptedException {
+ Assertions.checkNotNull(input);
+
+ // Make sure the peek position is set to the read position before we peek the first header.
+ input.resetPeekPosition();
+
+ 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 != 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 == WavUtil.RIFF_FOURCC) {
+ bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4;
+ }
+ if (bytesToSkip > Integer.MAX_VALUE) {
+ throw new ParserException("Chunk is too large (~2GB+) to skip; id: " + chunkHeader.id);
+ }
+ input.skipFully((int) bytesToSkip);
+ chunkHeader = ChunkHeader.peek(input, scratch);
+ }
+ // Skip past the "data" header.
+ input.skipFully(ChunkHeader.SIZE_IN_BYTES);
+
+ long dataStartPosition = input.getPosition();
+ long dataEndPosition = dataStartPosition + chunkHeader.size;
+ long inputLength = input.getLength();
+ if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) {
+ Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength);
+ dataEndPosition = inputLength;
+ }
+ return Pair.create(dataStartPosition, dataEndPosition);
+ }
+
+ private WavHeaderReader() {
+ // Prevent instantiation.
+ }
+
+ /** Container for a WAV chunk header. */
+ private static final class ChunkHeader {
+
+ /** Size in bytes of a WAV chunk header. */
+ public static final int SIZE_IN_BYTES = 8;
+
+ /** 4-character identifier, stored as an integer, for this chunk. */
+ public final int id;
+ /** Size of this chunk in bytes. */
+ public final long size;
+
+ private ChunkHeader(int id, long size) {
+ this.id = id;
+ this.size = size;
+ }
+
+ /**
+ * Peeks and returns a {@link ChunkHeader}.
+ *
+ * @param input Input stream to peek the chunk header from.
+ * @param scratch Buffer for temporary use.
+ * @throws IOException If peeking from the input fails.
+ * @throws InterruptedException If interrupted while peeking from input.
+ * @return A new {@code ChunkHeader} peeked from {@code input}.
+ */
+ public static ChunkHeader peek(ExtractorInput input, ParsableByteArray scratch)
+ throws IOException, InterruptedException {
+ input.peekFully(scratch.data, /* offset= */ 0, /* length= */ SIZE_IN_BYTES);
+ scratch.setPosition(0);
+
+ int id = scratch.readInt();
+ long size = scratch.readLittleEndianUnsignedInt();
+
+ return new ChunkHeader(id, size);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java
new file mode 100644
index 0000000000..d14268d120
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
new file mode 100644
index 0000000000..7e38c9a173
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
@@ -0,0 +1,617 @@
+/*
+ * 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.mediacodec;
+
+import android.annotation.TargetApi;
+import android.graphics.Point;
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo.AudioCapabilities;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecInfo.CodecProfileLevel;
+import android.media.MediaCodecInfo.VideoCapabilities;
+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. */
+@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.
+ * <p>
+ * May be passed to {@link MediaCodec#createByCodecName(String)} to create an instance of the
+ * decoder.
+ */
+ 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.
+ *
+ * @see CodecCapabilities#isFeatureSupported(String)
+ * @see CodecCapabilities#FEATURE_AdaptivePlayback
+ */
+ public final boolean adaptive;
+
+ /**
+ * Whether the decoder supports tunneling.
+ *
+ * @see CodecCapabilities#isFeatureSupported(String)
+ * @see CodecCapabilities#FEATURE_TunneledPlayback
+ */
+ public final boolean tunneling;
+
+ /**
+ * 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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * @param name The name of the {@link MediaCodec}.
+ * @return The created instance.
+ */
+ public static MediaCodecInfo newPassthroughInstance(String name) {
+ return new MediaCodecInfo(
+ name,
+ /* mimeType= */ null,
+ /* codecMimeType= */ null,
+ /* capabilities= */ null,
+ /* passthrough= */ true,
+ /* hardwareAccelerated= */ false,
+ /* softwareOnly= */ true,
+ /* vendor= */ false,
+ /* forceDisableAdaptive= */ false,
+ /* forceSecure= */ false);
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param name The name of the {@link MediaCodec}.
+ * @param mimeType A mime type supported by the {@link MediaCodec}.
+ * @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,
+ 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);
+ }
+
+ 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;
+ 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;
+ }
+
+ /**
+ * The profile levels supported by the decoder.
+ *
+ * @return The profile levels supported by the decoder.
+ */
+ public CodecProfileLevel[] getProfileLevels() {
+ return capabilities == null || capabilities.profileLevels == null ? new CodecProfileLevel[0]
+ : capabilities.profileLevels;
+ }
+
+ /**
+ * 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.
+ *
+ * @see CodecCapabilities#getMaxSupportedInstances()
+ */
+ 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(format.codecs);
+ if (codecMimeType == null) {
+ return true;
+ }
+ if (!mimeType.equals(codecMimeType)) {
+ logNoSupport("codec.mime " + format.codecs + ", " + codecMimeType);
+ return false;
+ }
+ Pair<Integer, Integer> 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 == profile && capabilities.level >= level) {
+ return true;
+ }
+ }
+ 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<Integer, Integer> 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<Integer, Integer> oldCodecProfileLevel =
+ MediaCodecUtil.getCodecProfileAndLevel(oldFormat);
+ Pair<Integer, Integer> 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.
+ *
+ * <p>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.
+ * @return Whether the decoder supports video with the given width, height and frame rate.
+ */
+ @TargetApi(21)
+ public boolean isVideoSizeAndRateSupportedV21(int width, int height, double frameRate) {
+ if (capabilities == null) {
+ logNoSupport("sizeAndRate.caps");
+ return false;
+ }
+ VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities();
+ if (videoCapabilities == null) {
+ logNoSupport("sizeAndRate.vCaps");
+ return false;
+ }
+ if (!areSizeAndRateSupportedV21(videoCapabilities, width, height, frameRate)) {
+ if (width >= height
+ || !enableRotatedVerticalResolutionWorkaround(name)
+ || !areSizeAndRateSupportedV21(videoCapabilities, height, width, frameRate)) {
+ logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate);
+ return false;
+ }
+ logAssumedSupport("sizeAndRate.rotated, " + width + "x" + height + "x" + frameRate);
+ }
+ return true;
+ }
+
+ /**
+ * Returns the smallest video size greater than or equal to a specified size that also satisfies
+ * the {@link MediaCodec}'s width and height alignment requirements.
+ * <p>
+ * Must not be called if the device SDK version is less than 21.
+ *
+ * @param width Width in pixels.
+ * @param height Height in pixels.
+ * @return The smallest video size greater than or equal to the specified size that also satisfies
+ * the {@link MediaCodec}'s width and height alignment requirements, or null if not a video
+ * codec.
+ */
+ @TargetApi(21)
+ public Point alignVideoSizeV21(int width, int height) {
+ if (capabilities == null) {
+ return null;
+ }
+ VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities();
+ if (videoCapabilities == null) {
+ return null;
+ }
+ return alignVideoSizeV21(videoCapabilities, width, height);
+ }
+
+ /**
+ * Whether the decoder supports audio with a given sample rate.
+ * <p>
+ * Must not be called if the device SDK version is less than 21.
+ *
+ * @param sampleRate The sample rate in Hz.
+ * @return Whether the decoder supports audio with the given sample rate.
+ */
+ @TargetApi(21)
+ public boolean isAudioSampleRateSupportedV21(int sampleRate) {
+ if (capabilities == null) {
+ logNoSupport("sampleRate.caps");
+ return false;
+ }
+ AudioCapabilities audioCapabilities = capabilities.getAudioCapabilities();
+ if (audioCapabilities == null) {
+ logNoSupport("sampleRate.aCaps");
+ return false;
+ }
+ if (!audioCapabilities.isSampleRateSupported(sampleRate)) {
+ logNoSupport("sampleRate.support, " + sampleRate);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Whether the decoder supports audio with a given channel count.
+ * <p>
+ * Must not be called if the device SDK version is less than 21.
+ *
+ * @param channelCount The channel count.
+ * @return Whether the decoder supports audio with the given channel count.
+ */
+ @TargetApi(21)
+ public boolean isAudioChannelCountSupportedV21(int channelCount) {
+ if (capabilities == null) {
+ logNoSupport("channelCount.caps");
+ return false;
+ }
+ AudioCapabilities audioCapabilities = capabilities.getAudioCapabilities();
+ if (audioCapabilities == null) {
+ logNoSupport("channelCount.aCaps");
+ return false;
+ }
+ int maxInputChannelCount = adjustMaxInputChannelCount(name, mimeType,
+ audioCapabilities.getMaxInputChannelCount());
+ if (maxInputChannelCount < channelCount) {
+ logNoSupport("channelCount.support, " + channelCount);
+ return false;
+ }
+ return true;
+ }
+
+ private void logNoSupport(String message) {
+ Log.d(TAG, "NoSupport [" + message + "] [" + name + ", " + mimeType + "] ["
+ + Util.DEVICE_DEBUG_INFO + "]");
+ }
+
+ private void logAssumedSupport(String message) {
+ Log.d(TAG, "AssumedSupport [" + message + "] [" + name + ", " + mimeType + "] ["
+ + 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);
+ }
+
+ @TargetApi(19)
+ private static boolean isAdaptiveV19(CodecCapabilities capabilities) {
+ return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback);
+ }
+
+ private static boolean isTunneling(CodecCapabilities capabilities) {
+ return Util.SDK_INT >= 21 && isTunnelingV21(capabilities);
+ }
+
+ @TargetApi(21)
+ private static boolean isTunnelingV21(CodecCapabilities capabilities) {
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
new file mode 100644
index 0000000000..8d2f4574fd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
@@ -0,0 +1,2014 @@
+/*
+ * 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.mediacodec;
+
+import android.annotation.TargetApi;
+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.Bundle;
+import android.os.SystemClock;
+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;
+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.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.
+ */
+public abstract class MediaCodecRenderer extends BaseRenderer {
+
+ /** Thrown when a failure occurs instantiating a decoder. */
+ public static class DecoderInitializationException extends Exception {
+
+ private static final int CUSTOM_ERROR_CODE_BASE = -50000;
+ private static final int NO_SUITABLE_DECODER_ERROR = CUSTOM_ERROR_CODE_BASE + 1;
+ private static final int DECODER_QUERY_ERROR = CUSTOM_ERROR_CODE_BASE + 2;
+
+ /**
+ * The mime type for which a decoder was being initialized.
+ */
+ public final String mimeType;
+
+ /**
+ * Whether it was required that the decoder support a secure output path.
+ */
+ public final boolean secureDecoderRequired;
+
+ /**
+ * The {@link MediaCodecInfo} of the decoder that failed to initialize. Null if no suitable
+ * decoder was found.
+ */
+ @Nullable public final MediaCodecInfo codecInfo;
+
+ /** An optional developer-readable diagnostic information string. May be null. */
+ @Nullable public final String diagnosticInfo;
+
+ /**
+ * 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.
+ */
+ @Nullable public final DecoderInitializationException fallbackDecoderInitializationException;
+
+ public DecoderInitializationException(Format format, Throwable cause,
+ boolean secureDecoderRequired, int 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,
+ 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.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)
+ private static String getDiagnosticInfoV21(Throwable cause) {
+ if (cause instanceof CodecException) {
+ return ((CodecException) cause).getDiagnosticInfo();
+ }
+ return null;
+ }
+
+ private static String buildCustomDiagnosticInfo(int errorCode) {
+ String sign = errorCode < 0 ? "neg_" : "";
+ 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";
+
+ /**
+ * If the {@link MediaCodec} is hotswapped (i.e. replaced during playback), this is the period of
+ * time during which {@link #isReady()} will report true regardless of whether the new codec has
+ * output frames that are ready to be rendered.
+ * <p>
+ * This allows codec hotswapping to be performed seamlessly, without interrupting the playback of
+ * other renderers, provided the new codec is able to decode some frames within this time period.
+ */
+ 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.
+ */
+ private static final int RECONFIGURATION_STATE_NONE = 0;
+ /**
+ * Codec configuration data needs to be written into the next buffer.
+ */
+ private static final int RECONFIGURATION_STATE_WRITE_PENDING = 1;
+ /**
+ * Codec configuration data has been written into the next buffer, but that buffer still needs to
+ * be returned to the codec.
+ */
+ 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 adaptation workaround is never used.
+ */
+ private static final int ADAPTATION_WORKAROUND_MODE_NEVER = 0;
+ /**
+ * The adaptation workaround is used when adapting between formats of the same resolution only.
+ */
+ private static final int ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION = 1;
+ /**
+ * The adaptation workaround is always used when adapting between formats.
+ */
+ private static final int ADAPTATION_WORKAROUND_MODE_ALWAYS = 2;
+
+ /**
+ * 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 =
+ 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;
+ @Nullable private final DrmSessionManager<FrameworkMediaCrypto> drmSessionManager;
+ private final boolean playClearSamplesWithoutKeys;
+ private final boolean enableDecoderFallback;
+ private final float assumedMinimumCodecOperatingRate;
+ private final DecoderInputBuffer buffer;
+ private final DecoderInputBuffer flagsOnlyBuffer;
+ private final TimedValueQueue<Format> formatQueue;
+ private final ArrayList<Long> decodeOnlyPresentationTimestamps;
+ private final MediaCodec.BufferInfo outputBufferInfo;
+
+ private boolean drmResourcesAcquired;
+ @Nullable private Format inputFormat;
+ private Format outputFormat;
+ @Nullable private DrmSession<FrameworkMediaCrypto> codecDrmSession;
+ @Nullable private DrmSession<FrameworkMediaCrypto> 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<MediaCodecInfo> availableCodecInfos;
+ @Nullable private DecoderInitializationException preferredDecoderInitializationException;
+ @Nullable private MediaCodecInfo codecInfo;
+ @AdaptationWorkaroundMode private int codecAdaptationWorkaroundMode;
+ private boolean codecNeedsReconfigureWorkaround;
+ private boolean codecNeedsDiscardToSpsWorkaround;
+ private boolean codecNeedsFlushWorkaround;
+ 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 ByteBuffer outputBuffer;
+ private boolean isDecodeOnlyOutputBuffer;
+ private boolean isLastOutputBuffer;
+ private boolean codecReconfigured;
+ @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 waitingForFirstSyncSample;
+ private boolean waitingForFirstSampleInFormat;
+ private boolean skipMediaCodecStopOnRelease;
+ private boolean pendingOutputEndOfStream;
+
+ protected DecoderCounters decoderCounters;
+
+ /**
+ * @param trackType The track type that the renderer handles. One of the {@code C.TRACK_TYPE_*}
+ * constants defined in {@link C}.
+ * @param mediaCodecSelector A decoder selector.
+ * @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 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,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ boolean playClearSamplesWithoutKeys,
+ boolean enableDecoderFallback,
+ float assumedMinimumCodecOperatingRate) {
+ super(trackType);
+ 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();
+ formatQueue = new TimedValueQueue<>();
+ decodeOnlyPresentationTimestamps = new ArrayList<>();
+ outputBufferInfo = new MediaCodec.BufferInfo();
+ codecReconfigurationState = RECONFIGURATION_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.
+ *
+ * <p>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.
+ *
+ * <p>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()}.
+ *
+ * <p>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
+ @AdaptiveSupport
+ public final int supportsMixedMimeTypeAdaptation() {
+ return ADAPTIVE_NOT_SEAMLESS;
+ }
+
+ @Override
+ @Capabilities
+ public final int supportsFormat(Format format) throws ExoPlaybackException {
+ try {
+ return supportsFormat(mediaCodecSelector, drmSessionManager, format);
+ } catch (DecoderQueryException e) {
+ throw createRendererException(e, format);
+ }
+ }
+
+ /**
+ * Returns the {@link Capabilities} for the given {@link Format}.
+ *
+ * @param mediaCodecSelector The decoder selector.
+ * @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.
+ */
+ @Capabilities
+ protected abstract int supportsFormat(
+ MediaCodecSelector mediaCodecSelector,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ Format format)
+ throws DecoderQueryException;
+
+ /**
+ * Returns a list of decoders that can decode media in the specified format, in priority order.
+ *
+ * @param mediaCodecSelector The decoder selector.
+ * @param format The {@link Format} for which a decoder is required.
+ * @param requiresSecureDecoder Whether a secure decoder is required.
+ * @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty.
+ * @throws DecoderQueryException Thrown if there was an error querying decoders.
+ */
+ protected abstract List<MediaCodecInfo> 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 {@link Format} for which the codec is being configured.
+ * @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption.
+ * @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,
+ @Nullable MediaCrypto crypto,
+ float codecOperatingRate);
+
+ protected final void maybeInitCodec() throws ExoPlaybackException {
+ if (codec != null || inputFormat == null) {
+ // We have a codec already, or we don't have a format with which to instantiate one.
+ return;
+ }
+
+ setCodecDrmSession(sourceDrmSession);
+
+ 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;
+ }
+ }
+ }
+
+ try {
+ maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder);
+ } catch (DecoderInitializationException e) {
+ throw createRendererException(e, inputFormat);
+ }
+ }
+
+ protected boolean shouldInitCodec(MediaCodecInfo codecInfo) {
+ return true;
+ }
+
+ /**
+ * 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();
+ }
+
+ @Override
+ protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+ inputStreamEnded = false;
+ outputStreamEnded = false;
+ 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() {
+ inputFormat = null;
+ if (sourceDrmSession != null || codecDrmSession != null) {
+ // TODO: Do something better with this case.
+ onReset();
+ } else {
+ flushOrReleaseCodec();
+ }
+ }
+
+ @Override
+ protected void onReset() {
+ try {
+ releaseCodec();
+ } finally {
+ setSourceDrmSession(null);
+ }
+ if (drmSessionManager != null && drmResourcesAcquired) {
+ drmResourcesAcquired = false;
+ drmSessionManager.release();
+ }
+ }
+
+ protected void releaseCodec() {
+ 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 {
+ if (!skipMediaCodecStopOnRelease) {
+ codec.stop();
+ }
+ } finally {
+ codec.release();
+ }
+ }
+ } finally {
+ codec = null;
+ try {
+ if (mediaCrypto != null) {
+ mediaCrypto.release();
+ }
+ } finally {
+ mediaCrypto = null;
+ mediaCryptoRequiresSecureDecoder = false;
+ setCodecDrmSession(null);
+ }
+ }
+ }
+
+ @Override
+ protected void onStarted() {
+ // Do nothing. Overridden to remove throws clause.
+ }
+
+ @Override
+ protected void onStopped() {
+ // Do nothing. Overridden to remove throws clause.
+ }
+
+ @Override
+ public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
+ if (pendingOutputEndOfStream) {
+ pendingOutputEndOfStream = false;
+ processEndOfStream();
+ }
+ try {
+ if (outputStreamEnded) {
+ renderToEndOfStream();
+ return;
+ }
+ 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) {
+ 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;
+ }
+ }
+
+ /**
+ * 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}.
+ *
+ * <p>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;
+ codecReceivedEos = false;
+ codecReceivedBuffers = false;
+ waitingForFirstSyncSample = true;
+ codecNeedsAdaptationWorkaroundBuffer = false;
+ shouldSkipAdaptationWorkaroundOutputBuffer = false;
+ 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<MediaCodecInfo> 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<MediaCodecInfo> getAvailableCodecInfos(boolean mediaCryptoRequiresSecureDecoder)
+ throws DecoderQueryException {
+ List<MediaCodecInfo> 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 {
+ return inputBuffers[inputIndex];
+ }
+ }
+
+ 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<FrameworkMediaCrypto> session) {
+ DrmSession.replaceSession(sourceDrmSession, session);
+ sourceDrmSession = session;
+ }
+
+ private void setCodecDrmSession(@Nullable DrmSession<FrameworkMediaCrypto> 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 || codecDrainState == DRAIN_STATE_WAIT_END_OF_STREAM || inputStreamEnded) {
+ return false;
+ }
+
+ if (inputIndex < 0) {
+ inputIndex = codec.dequeueInputBuffer(0);
+ if (inputIndex < 0) {
+ return false;
+ }
+ buffer.data = getInputBuffer(inputIndex);
+ buffer.clear();
+ }
+
+ 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 (codecNeedsEosPropagation) {
+ // Do nothing.
+ } else {
+ codecReceivedEos = true;
+ codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ resetInputBuffer();
+ }
+ codecDrainState = DRAIN_STATE_WAIT_END_OF_STREAM;
+ return false;
+ }
+
+ if (codecNeedsAdaptationWorkaroundBuffer) {
+ codecNeedsAdaptationWorkaroundBuffer = false;
+ buffer.data.put(ADAPTATION_WORKAROUND_BUFFER);
+ codec.queueInputBuffer(inputIndex, 0, ADAPTATION_WORKAROUND_BUFFER.length, 0, 0);
+ 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.
+ result = C.RESULT_BUFFER_READ;
+ } else {
+ // 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 < codecFormat.initializationData.size(); i++) {
+ byte[] data = codecFormat.initializationData.get(i);
+ buffer.data.put(data);
+ }
+ codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING;
+ }
+ adaptiveReconfigurationBytes = buffer.data.position();
+ 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;
+ }
+ if (result == C.RESULT_FORMAT_READ) {
+ if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
+ // We received two formats in a row. Clear the current buffer of any reconfiguration data
+ // associated with the first format.
+ buffer.clear();
+ codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+ }
+ onInputFormatChanged(formatHolder);
+ return true;
+ }
+
+ // We've read a buffer.
+ if (buffer.isEndOfStream()) {
+ if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
+ // We received a new format immediately before the end of the stream. We need to clear
+ // the corresponding reconfiguration data from the current buffer, but re-write it into
+ // a subsequent buffer if there are any (e.g. if the user seeks backwards).
+ buffer.clear();
+ codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+ }
+ inputStreamEnded = true;
+ if (!codecReceivedBuffers) {
+ processEndOfStream();
+ return false;
+ }
+ try {
+ if (codecNeedsEosPropagation) {
+ // Do nothing.
+ } else {
+ codecReceivedEos = true;
+ codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ resetInputBuffer();
+ }
+ } catch (CryptoException e) {
+ throw createRendererException(e, inputFormat);
+ }
+ return false;
+ }
+ 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
+ // data into a subsequent buffer (if there is one).
+ codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+ }
+ return true;
+ }
+ waitingForFirstSyncSample = false;
+ boolean bufferEncrypted = buffer.isEncrypted();
+ waitingForKeys = shouldWaitForKeys(bufferEncrypted);
+ if (waitingForKeys) {
+ return false;
+ }
+ if (codecNeedsDiscardToSpsWorkaround && !bufferEncrypted) {
+ NalUnitUtil.discardToSps(buffer.data);
+ if (buffer.data.position() == 0) {
+ return true;
+ }
+ codecNeedsDiscardToSpsWorkaround = false;
+ }
+ try {
+ long presentationTimeUs = buffer.timeUs;
+ 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) {
+ MediaCodec.CryptoInfo cryptoInfo = getFrameworkCryptoInfo(buffer,
+ adaptiveReconfigurationBytes);
+ codec.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, presentationTimeUs, 0);
+ } else {
+ codec.queueInputBuffer(inputIndex, 0, buffer.data.limit(), presentationTimeUs, 0);
+ }
+ resetInputBuffer();
+ codecReceivedBuffers = true;
+ codecReconfigurationState = RECONFIGURATION_STATE_NONE;
+ decoderCounters.inputBufferCount++;
+ } catch (CryptoException e) {
+ throw createRendererException(e, inputFormat);
+ }
+ return true;
+ }
+
+ private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
+ if (codecDrmSession == null
+ || (!bufferEncrypted
+ && (playClearSamplesWithoutKeys || codecDrmSession.playClearSamplesWithoutKeys()))) {
+ return false;
+ }
+ @DrmSession.State int drmSessionState = codecDrmSession.getState();
+ if (drmSessionState == DrmSession.STATE_ERROR) {
+ throw createRendererException(codecDrmSession.getError(), inputFormat);
+ }
+ return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
+ }
+
+ /**
+ * Called when a {@link MediaCodec} has been created and configured.
+ * <p>
+ * The default implementation is a no-op.
+ *
+ * @param name The name of the codec that was initialized.
+ * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization
+ * finished.
+ * @param initializationDurationMs The time taken to initialize the codec in milliseconds.
+ */
+ protected void onCodecInitialized(String name, long initializedTimestampMs,
+ long initializationDurationMs) {
+ // Do nothing.
+ }
+
+ /**
+ * Called when a new {@link Format} is read from the upstream {@link MediaPeriod}.
+ *
+ * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}.
+ * @throws ExoPlaybackException If an error occurs re-initializing the {@link MediaCodec}.
+ */
+ @SuppressWarnings("unchecked")
+ protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {
+ waitingForFirstSampleInFormat = true;
+ Format newFormat = Assertions.checkNotNull(formatHolder.format);
+ if (formatHolder.includesDrmSession) {
+ setSourceDrmSession((DrmSession<FrameworkMediaCrypto>) formatHolder.drmSession);
+ } else {
+ sourceDrmSession =
+ getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession);
+ }
+ inputFormat = newFormat;
+
+ if (codec == null) {
+ maybeInitCodec();
+ return;
+ }
+
+ // 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 {@link MediaFormat} of the {@link MediaCodec} changes.
+ *
+ * <p>The default implementation is a no-op.
+ *
+ * @param codec The {@link MediaCodec} instance.
+ * @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 outputMediaFormat)
+ throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ /**
+ * Handles supplemental data associated with an input buffer.
+ *
+ * <p>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.
+ *
+ * <p>The default implementation is a no-op.
+ *
+ * @param buffer The buffer to be queued.
+ */
+ protected void onQueueInputBuffer(DecoderInputBuffer buffer) {
+ // Do nothing.
+ }
+
+ /**
+ * Called when an output buffer is successfully processed.
+ * <p>
+ * The default implementation is a no-op.
+ *
+ * @param presentationTimeUs The timestamp associated with the output buffer.
+ */
+ protected void onProcessedOutputBuffer(long presentationTimeUs) {
+ // Do nothing.
+ }
+
+ /**
+ * Determines whether the existing {@link MediaCodec} can be kept for a new {@link Format}, and if
+ * it can whether it requires reconfiguration.
+ *
+ * <p>The default implementation returns {@link #KEEP_CODEC_RESULT_NO}.
+ *
+ * @param codec The existing {@link MediaCodec} instance.
+ * @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 @KeepCodecResult int canKeepCodec(
+ MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) {
+ return KEEP_CODEC_RESULT_NO;
+ }
+
+ @Override
+ public boolean isEnded() {
+ return outputStreamEnded;
+ }
+
+ @Override
+ public boolean isReady() {
+ return inputFormat != null
+ && !waitingForKeys
+ && (isSourceReady()
+ || hasOutputBuffer()
+ || (codecHotswapDeadlineMs != C.TIME_UNSET
+ && SystemClock.elapsedRealtime() < codecHotswapDeadlineMs));
+ }
+
+ /**
+ * Returns the maximum time to block whilst waiting for a decoded output buffer.
+ *
+ * @return The maximum time to block, in microseconds.
+ */
+ protected long getDequeueOutputBufferTimeoutUs() {
+ 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.
+ *
+ * <p>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.
+ */
+ private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
+ throws ExoPlaybackException {
+ if (!hasOutputBuffer()) {
+ int outputIndex;
+ if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) {
+ try {
+ outputIndex =
+ codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs());
+ } catch (IllegalStateException e) {
+ processEndOfStream();
+ if (outputStreamEnded) {
+ // Release the codec, as it's in an error state.
+ releaseCodec();
+ }
+ return false;
+ }
+ } else {
+ outputIndex =
+ codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs());
+ }
+
+ 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;
+ }
+ /* 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,
+ outputBuffer,
+ outputIndex,
+ outputBufferInfo.flags,
+ outputBufferInfo.presentationTimeUs,
+ isDecodeOnlyOutputBuffer,
+ isLastOutputBuffer,
+ outputFormat);
+ } catch (IllegalStateException e) {
+ processEndOfStream();
+ if (outputStreamEnded) {
+ // Release the codec, as it's in an error state.
+ releaseCodec();
+ }
+ return false;
+ }
+ } else {
+ processedOutputBuffer =
+ processOutputBuffer(
+ positionUs,
+ elapsedRealtimeUs,
+ codec,
+ outputBuffer,
+ outputIndex,
+ outputBufferInfo.flags,
+ outputBufferInfo.presentationTimeUs,
+ isDecodeOnlyOutputBuffer,
+ isLastOutputBuffer,
+ outputFormat);
+ }
+
+ if (processedOutputBuffer) {
+ onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs);
+ boolean isEndOfStream = (outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+ resetOutputBuffer();
+ if (!isEndOfStream) {
+ return true;
+ }
+ processEndOfStream();
+ }
+
+ return false;
+ }
+
+ /** Processes a new output {@link MediaFormat}. */
+ private void processOutputFormat() throws ExoPlaybackException {
+ 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) {
+ mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
+ }
+ onOutputFormatChanged(codec, mediaFormat);
+ }
+
+ /**
+ * Processes a change in the output buffers.
+ */
+ private void processOutputBuffersChanged() {
+ if (Util.SDK_INT < 21) {
+ outputBuffers = codec.getOutputBuffers();
+ }
+ }
+
+ /**
+ * Processes an output media buffer.
+ *
+ * <p>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.
+ *
+ * <p>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 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 isDecodeOnlyBuffer,
+ boolean isLastBuffer,
+ Format format)
+ throws ExoPlaybackException;
+
+ /**
+ * Incrementally renders any remaining output.
+ * <p>
+ * The default implementation is a no-op.
+ *
+ * @throws ExoPlaybackException Thrown if an error occurs rendering remaining output.
+ */
+ protected void renderToEndOfStream() throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ /**
+ * Processes an end of stream signal.
+ *
+ * @throws ExoPlaybackException If an error occurs processing the signal.
+ */
+ private void processEndOfStream() throws ExoPlaybackException {
+ 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;
+ }
+ }
+
+ /**
+ * 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();
+ for (int i = 0; i < size; i++) {
+ if (decodeOnlyPresentationTimestamps.get(i) == presentationTimeUs) {
+ decodeOnlyPresentationTimestamps.remove(i);
+ return true;
+ }
+ }
+ 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<FrameworkMediaCrypto> 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.
+ * <p>
+ * If true is returned, the renderer will work around the issue by releasing the decoder and
+ * instantiating a new one rather than flushing the current instance.
+ * <p>
+ * See [Internal: b/8347958, b/8543366].
+ *
+ * @param name The name of the decoder.
+ * @return True if the decoder is known to fail when flushed.
+ */
+ private static boolean codecNeedsFlushWorkaround(String name) {
+ return Util.SDK_INT < 18
+ || (Util.SDK_INT == 18
+ && ("OMX.SEC.avc.dec".equals(name) || "OMX.SEC.avc.dec.secure".equals(name)))
+ || (Util.SDK_INT == 19 && Util.MODEL.startsWith("SM-G800")
+ && ("OMX.Exynos.avc.dec".equals(name) || "OMX.Exynos.avc.dec.secure".equals(name)));
+ }
+
+ /**
+ * Returns a mode that specifies when the adaptation workaround should be enabled.
+ *
+ * <p>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.
+ *
+ * <p>See [Internal: b/27807182]. See <a
+ * href="https://github.com/google/ExoPlayer/issues/3257">GitHub issue #3257</a>.
+ *
+ * @param name The name of the decoder.
+ * @return The mode specifying when the adaptation workaround should be enabled.
+ */
+ 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))) {
+ 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.
+ *
+ * <p>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.
+ *
+ * <p>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 {@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) {
+ return Util.SDK_INT < 21 && format.initializationData.isEmpty()
+ && "OMX.MTK.VIDEO.DECODER.AVC".equals(name);
+ }
+
+ /**
+ * 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.
+ *
+ * <p>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 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(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);
+ }
+
+ /**
+ * Returns whether the decoder is known to behave incorrectly if flushed after receiving an input
+ * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set.
+ * <p>
+ * If true is returned, the renderer will work around the issue by instantiating a new decoder
+ * when this case occurs.
+ * <p>
+ * See [Internal: b/8578467, b/23361053].
+ *
+ * @param name The name of the decoder.
+ * @return True if the decoder is known to behave incorrectly if flushed after receiving an input
+ * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set. False otherwise.
+ */
+ 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) || "stvm8".equals(Util.DEVICE))
+ && ("OMX.amlogic.avc.decoder.awesome".equals(name)
+ || "OMX.amlogic.avc.decoder.awesome.secure".equals(name)));
+ }
+
+ /**
+ * Returns whether the decoder may throw an {@link IllegalStateException} from
+ * {@link MediaCodec#dequeueOutputBuffer(MediaCodec.BufferInfo, long)} or
+ * {@link MediaCodec#releaseOutputBuffer(int, boolean)} after receiving an input
+ * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set.
+ * <p>
+ * See [Internal: b/17933838].
+ *
+ * @param name The name of the decoder.
+ * @return True if the decoder may throw an exception after receiving an end-of-stream buffer.
+ */
+ private static boolean codecNeedsEosOutputExceptionWorkaround(String name) {
+ return Util.SDK_INT == 21 && "OMX.google.aac.decoder".equals(name);
+ }
+
+ /**
+ * 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.
+ *
+ * <p>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 {@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
+ && "OMX.MTK.AUDIO.DECODER.MP3".equals(name);
+ }
+
+ /**
+ * Returns whether the decoder is known to behave incorrectly if flushed prior to having output a
+ * {@link MediaFormat}.
+ *
+ * <p>If true is returned, the renderer will work around the issue by instantiating a new decoder
+ * when this case occurs.
+ *
+ * <p>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 codecNeedsSosFlushWorkaround(String name) {
+ return Util.SDK_INT == 29 && "c2.android.aac.decoder".equals(name);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java
new file mode 100644
index 0000000000..3f90c3a105
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java
@@ -0,0 +1,71 @@
+/*
+ * 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.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.
+ */
+public interface MediaCodecSelector {
+
+ /**
+ * Default implementation of {@link MediaCodecSelector}, which returns the preferred decoder for
+ * the given format.
+ */
+ MediaCodecSelector DEFAULT =
+ new MediaCodecSelector() {
+ @Override
+ public List<MediaCodecInfo> getDecoderInfos(
+ String mimeType, boolean requiresSecureDecoder, boolean requiresTunnelingDecoder)
+ throws DecoderQueryException {
+ return MediaCodecUtil.getDecoderInfos(
+ mimeType, requiresSecureDecoder, requiresTunnelingDecoder);
+ }
+
+ @Override
+ @Nullable
+ public MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException {
+ return MediaCodecUtil.getPassthroughDecoderInfo();
+ }
+ };
+
+ /**
+ * 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 requiresSecureDecoder Whether a secure decoder is required.
+ * @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.
+ */
+ List<MediaCodecInfo> 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.
+ * @throws DecoderQueryException Thrown if there was an error querying decoders.
+ */
+ @Nullable
+ MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException;
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
new file mode 100644
index 0000000000..11fe931305
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
@@ -0,0 +1,1232 @@
+/*
+ * 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.mediacodec;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecInfo.CodecProfileLevel;
+import android.media.MediaCodecList;
+import android.text.TextUtils;
+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;
+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.
+ */
+@SuppressLint("InlinedApi")
+public final class MediaCodecUtil {
+
+ /**
+ * Thrown when an error occurs querying the device for its underlying media capabilities.
+ * <p>
+ * Such failures are not expected in normal operation and are normally temporary (e.g. if the
+ * mediaserver process has crashed and is yet to restart).
+ */
+ public static class DecoderQueryException extends Exception {
+
+ private DecoderQueryException(Throwable cause) {
+ super("Failed to query underlying media codecs", cause);
+ }
+
+ }
+
+ private static final String TAG = "MediaCodecUtil";
+ private static final Pattern PROFILE_PATTERN = Pattern.compile("^\\D?(\\d+)$");
+
+ private static final HashMap<CodecKey, List<MediaCodecInfo>> decoderInfosCache = new HashMap<>();
+
+ // Codecs to constant mappings.
+ // AVC.
+ private static final SparseIntArray AVC_PROFILE_NUMBER_TO_CONST;
+ 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<String, Integer> 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<String, Integer> DOLBY_VISION_STRING_TO_PROFILE;
+ private static final Map<String, Integer> 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;
+
+ private MediaCodecUtil() {}
+
+ /**
+ * Optional call to warm the codec cache for a given mime type.
+ *
+ * <p>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, boolean tunneling) {
+ try {
+ 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);
+ }
+ }
+
+ /**
+ * Returns information about a decoder suitable for audio passthrough.
+ *
+ * @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.
+ */
+ @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 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.
+ * @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.
+ */
+ @Nullable
+ public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure, boolean tunneling)
+ throws DecoderQueryException {
+ List<MediaCodecInfo> 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}.
+ *
+ * @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.
+ * @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<MediaCodecInfo> getDecoderInfos(
+ String mimeType, boolean secure, boolean tunneling) throws DecoderQueryException {
+ CodecKey key = new CodecKey(mimeType, secure, tunneling);
+ @Nullable List<MediaCodecInfo> cachedDecoderInfos = decoderInfosCache.get(key);
+ if (cachedDecoderInfos != null) {
+ return cachedDecoderInfos;
+ }
+ MediaCodecListCompat mediaCodecList =
+ Util.SDK_INT >= 21
+ ? new MediaCodecListCompatV21(secure, tunneling)
+ : new MediaCodecListCompatV16();
+ ArrayList<MediaCodecInfo> 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.
+ mediaCodecList = new MediaCodecListCompatV16();
+ decoderInfos = getDecoderInfosInternal(key, mediaCodecList);
+ if (!decoderInfos.isEmpty()) {
+ Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType
+ + ". Assuming: " + decoderInfos.get(0).name);
+ }
+ }
+ applyWorkarounds(mimeType, decoderInfos);
+ List<MediaCodecInfo> 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<MediaCodecInfo> getDecoderInfosSortedByFormatSupport(
+ List<MediaCodecInfo> decoderInfos, Format format) {
+ decoderInfos = new ArrayList<>(decoderInfos);
+ sortByScore(
+ decoderInfos,
+ decoderInfo -> {
+ try {
+ return decoderInfo.isFormatSupported(format) ? 1 : 0;
+ } catch (DecoderQueryException e) {
+ return -1;
+ }
+ });
+ return decoderInfos;
+ }
+
+ /**
+ * 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<Integer, Integer> 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<MediaCodecInfo> getDecoderInfosInternal(
+ CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException {
+ try {
+ ArrayList<MediaCodecInfo> 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);
+ 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;
+ }
+ }
+ }
+ return decoderInfos;
+ } catch (Exception e) {
+ // If the underlying mediaserver is in a bad state, we may catch an IllegalStateException
+ // or an IllegalArgumentException here.
+ throw new DecoderQueryException(e);
+ }
+ }
+
+ /**
+ * 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.
+ */
+ @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;
+ }
+
+ // Work around broken audio decoders.
+ if (Util.SDK_INT < 21
+ && ("CIPAACDecoder".equals(name)
+ || "CIPMP3Decoder".equals(name)
+ || "CIPVorbisDecoder".equals(name)
+ || "CIPAMRNBDecoder".equals(name)
+ || "AACDecoder".equals(name)
+ || "MP3Decoder".equals(name))) {
+ return false;
+ }
+
+ // 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;
+ }
+
+ // Work around an issue where querying/creating a particular MP3 decoder on some devices on
+ // platform API version 16 fails.
+ if (Util.SDK_INT == 16
+ && "OMX.qcom.audio.decoder.mp3".equals(name)
+ && ("dlxu".equals(Util.DEVICE) // HTC Butterfly
+ || "protou".equals(Util.DEVICE) // HTC Desire X
+ || "ville".equals(Util.DEVICE) // HTC One S
+ || "villeplus".equals(Util.DEVICE)
+ || "villec2".equals(Util.DEVICE)
+ || Util.DEVICE.startsWith("gee") // LGE Optimus G
+ || "C6602".equals(Util.DEVICE) // Sony Xperia Z
+ || "C6603".equals(Util.DEVICE)
+ || "C6606".equals(Util.DEVICE)
+ || "C6616".equals(Util.DEVICE)
+ || "L36h".equals(Util.DEVICE)
+ || "SO-02E".equals(Util.DEVICE))) {
+ return false;
+ }
+
+ // Work around an issue where large timestamps are not propagated correctly.
+ if (Util.SDK_INT == 16
+ && "OMX.qcom.audio.decoder.aac".equals(name)
+ && ("C1504".equals(Util.DEVICE) // Sony Xperia E
+ || "C1505".equals(Util.DEVICE)
+ || "C1604".equals(Util.DEVICE) // Sony Xperia E dual
+ || "C1605".equals(Util.DEVICE))) {
+ return false;
+ }
+
+ // 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")
+ || Util.DEVICE.startsWith("t0"))) {
+ return false;
+ }
+
+ // VP8 decoder on Samsung Galaxy S4 cannot be queried.
+ if (Util.SDK_INT <= 19 && Util.DEVICE.startsWith("jflte")
+ && "OMX.qcom.video.decoder.vp8".equals(name)) {
+ 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;
+ }
+
+ /**
+ * Modifies a list of {@link MediaCodecInfo}s to apply workarounds where we know better than the
+ * platform.
+ *
+ * @param mimeType The MIME type of input media.
+ * @param decoderInfos The list to modify.
+ */
+ private static void applyWorkarounds(String mimeType, List<MediaCodecInfo> 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 <a href="https://github.com/google/ExoPlayer/issues/5782">Issue #5782</a>.
+ 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));
+ }
+ // 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;
+ });
+ }
+
+ 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();
+ }
+
+ /**
+ * The result of {@link android.media.MediaCodecInfo#isHardwareAccelerated()} for API levels 29+,
+ * or a best-effort approximation for lower levels.
+ */
+ 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<Integer, Integer> 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<Integer, Integer> getHevcProfileAndLevel(String codec, String[] parts) {
+ if (parts.length < 4) {
+ // The codec has fewer parts than required by the HEVC codec string format.
+ Log.w(TAG, "Ignoring malformed HEVC 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 HEVC codec string: " + codec);
+ return null;
+ }
+ @Nullable String profileString = matcher.group(1);
+ int profile;
+ if ("1".equals(profileString)) {
+ profile = CodecProfileLevel.HEVCProfileMain;
+ } else if ("2".equals(profileString)) {
+ profile = CodecProfileLevel.HEVCProfileMain10;
+ } else {
+ Log.w(TAG, "Unknown HEVC profile string: " + profileString);
+ return null;
+ }
+ @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: " + levelString);
+ return null;
+ }
+ return new Pair<>(profile, level);
+ }
+
+ @Nullable
+ private static Pair<Integer, Integer> 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;
+ }
+ int profileInteger;
+ int levelInteger;
+ try {
+ if (parts[1].length() == 6) {
+ // Format: avc1.xxccyy, where xx is profile and yy level, both hexadecimal.
+ 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(parts[1]);
+ levelInteger = Integer.parseInt(parts[2]);
+ } else {
+ // We don't recognize the format.
+ Log.w(TAG, "Ignoring malformed AVC codec string: " + codec);
+ return null;
+ }
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring malformed AVC codec string: " + codec);
+ return null;
+ }
+
+ int profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger, -1);
+ if (profile == -1) {
+ Log.w(TAG, "Unknown AVC profile: " + profileInteger);
+ return 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<Integer, Integer> 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<Integer, Integer> 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.
+ *
+ * @param avcLevel one of CodecProfileLevel.AVCLevel* constants.
+ * @return maximum frame size that can be decoded by a decoder with the specified avc level
+ * (or {@code -1} if the level is not recognized)
+ */
+ private static int avcLevelToMaxFrameSize(int avcLevel) {
+ switch (avcLevel) {
+ 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<Integer, Integer> 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 <T> void sortByScore(List<T> list, ScoreProvider<T> scoreProvider) {
+ Collections.sort(list, (a, b) -> scoreProvider.getScore(b) - scoreProvider.getScore(a));
+ }
+
+ /** Interface for providers of item scores. */
+ private interface ScoreProvider<T> {
+ /** Returns the score of the provided item. */
+ int getScore(T t);
+ }
+
+ private interface MediaCodecListCompat {
+
+ /**
+ * The number of codecs in the list.
+ */
+ int getCodecCount();
+
+ /**
+ * The info at the specified index in the list.
+ *
+ * @param index The index.
+ */
+ android.media.MediaCodecInfo getCodecInfoAt(int index);
+
+ /**
+ * Returns whether secure decoders are explicitly listed, if present.
+ */
+ boolean secureDecodersExplicit();
+
+ /** 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)
+ private static final class MediaCodecListCompatV21 implements MediaCodecListCompat {
+
+ private final int codecKind;
+
+ @Nullable private android.media.MediaCodecInfo[] mediaCodecInfos;
+
+ // 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
+ public int getCodecCount() {
+ ensureMediaCodecInfosInitialized();
+ return mediaCodecInfos.length;
+ }
+
+ // incompatible types in return.
+ @SuppressWarnings("nullness:return.type.incompatible")
+ @Override
+ public android.media.MediaCodecInfo getCodecInfoAt(int index) {
+ ensureMediaCodecInfosInitialized();
+ return mediaCodecInfos[index];
+ }
+
+ @Override
+ public boolean secureDecodersExplicit() {
+ return true;
+ }
+
+ @Override
+ 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();
+ }
+ }
+
+ }
+
+ private static final class MediaCodecListCompatV16 implements MediaCodecListCompat {
+
+ @Override
+ public int getCodecCount() {
+ return MediaCodecList.getCodecCount();
+ }
+
+ @Override
+ public android.media.MediaCodecInfo getCodecInfoAt(int index) {
+ return MediaCodecList.getCodecInfoAt(index);
+ }
+
+ @Override
+ public boolean secureDecodersExplicit() {
+ return false;
+ }
+
+ @Override
+ 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 CodecCapabilities.FEATURE_SecurePlayback.equals(feature)
+ && MimeTypes.VIDEO_H264.equals(mimeType);
+ }
+
+ @Override
+ public boolean isFeatureRequired(
+ String feature, String mimeType, CodecCapabilities capabilities) {
+ return false;
+ }
+
+ }
+
+ private static final class CodecKey {
+
+ public final String mimeType;
+ public final boolean secure;
+ public final boolean tunneling;
+
+ 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.hashCode();
+ result = prime * result + (secure ? 1231 : 1237);
+ result = prime * result + (tunneling ? 1231 : 1237);
+ return result;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != CodecKey.class) {
+ return false;
+ }
+ CodecKey other = (CodecKey) obj;
+ return TextUtils.equals(mimeType, other.mimeType)
+ && secure == other.secure
+ && tunneling == other.tunneling;
+ }
+
+ }
+
+ static {
+ AVC_PROFILE_NUMBER_TO_CONST = new SparseIntArray();
+ AVC_PROFILE_NUMBER_TO_CONST.put(66, CodecProfileLevel.AVCProfileBaseline);
+ 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);
+ // TODO: Find int for CodecProfileLevel.AVCLevel1b.
+ AVC_LEVEL_NUMBER_TO_CONST.put(11, CodecProfileLevel.AVCLevel11);
+ AVC_LEVEL_NUMBER_TO_CONST.put(12, CodecProfileLevel.AVCLevel12);
+ AVC_LEVEL_NUMBER_TO_CONST.put(13, CodecProfileLevel.AVCLevel13);
+ AVC_LEVEL_NUMBER_TO_CONST.put(20, CodecProfileLevel.AVCLevel2);
+ AVC_LEVEL_NUMBER_TO_CONST.put(21, CodecProfileLevel.AVCLevel21);
+ AVC_LEVEL_NUMBER_TO_CONST.put(22, CodecProfileLevel.AVCLevel22);
+ AVC_LEVEL_NUMBER_TO_CONST.put(30, CodecProfileLevel.AVCLevel3);
+ AVC_LEVEL_NUMBER_TO_CONST.put(31, CodecProfileLevel.AVCLevel31);
+ AVC_LEVEL_NUMBER_TO_CONST.put(32, CodecProfileLevel.AVCLevel32);
+ AVC_LEVEL_NUMBER_TO_CONST.put(40, CodecProfileLevel.AVCLevel4);
+ AVC_LEVEL_NUMBER_TO_CONST.put(41, CodecProfileLevel.AVCLevel41);
+ AVC_LEVEL_NUMBER_TO_CONST.put(42, CodecProfileLevel.AVCLevel42);
+ AVC_LEVEL_NUMBER_TO_CONST.put(50, CodecProfileLevel.AVCLevel5);
+ 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);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L63", CodecProfileLevel.HEVCMainTierLevel21);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L90", CodecProfileLevel.HEVCMainTierLevel3);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L93", CodecProfileLevel.HEVCMainTierLevel31);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L120", CodecProfileLevel.HEVCMainTierLevel4);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L123", CodecProfileLevel.HEVCMainTierLevel41);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L150", CodecProfileLevel.HEVCMainTierLevel5);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L153", CodecProfileLevel.HEVCMainTierLevel51);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L156", CodecProfileLevel.HEVCMainTierLevel52);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L180", CodecProfileLevel.HEVCMainTierLevel6);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L183", CodecProfileLevel.HEVCMainTierLevel61);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L186", CodecProfileLevel.HEVCMainTierLevel62);
+
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H30", CodecProfileLevel.HEVCHighTierLevel1);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H60", CodecProfileLevel.HEVCHighTierLevel2);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H63", CodecProfileLevel.HEVCHighTierLevel21);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H90", CodecProfileLevel.HEVCHighTierLevel3);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H93", CodecProfileLevel.HEVCHighTierLevel31);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H120", CodecProfileLevel.HEVCHighTierLevel4);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H123", CodecProfileLevel.HEVCHighTierLevel41);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H150", CodecProfileLevel.HEVCHighTierLevel5);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H153", CodecProfileLevel.HEVCHighTierLevel51);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H156", CodecProfileLevel.HEVCHighTierLevel52);
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java
new file mode 100644
index 0000000000..cafaaa7c83
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<byte[]> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/package-info.java
new file mode 100644
index 0000000000..c8dd17d0df
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/Metadata.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/Metadata.java
new file mode 100644
index 0000000000..16f01c4627
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/Metadata.java
@@ -0,0 +1,171 @@
+/*
+ * 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.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;
+
+/**
+ * A collection of metadata entries.
+ */
+public final class Metadata implements 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;
+
+ /**
+ * @param entries The metadata entries.
+ */
+ public Metadata(Entry... entries) {
+ this.entries = entries;
+ }
+
+ /**
+ * @param entries The metadata entries.
+ */
+ public Metadata(List<? extends Entry> entries) {
+ this.entries = new Entry[entries.size()];
+ entries.toArray(this.entries);
+ }
+
+ /* package */ Metadata(Parcel in) {
+ entries = new Metadata.Entry[in.readInt()];
+ for (int i = 0; i < entries.length; i++) {
+ entries[i] = in.readParcelable(Entry.class.getClassLoader());
+ }
+ }
+
+ /**
+ * Returns the number of metadata entries.
+ */
+ public int length() {
+ return entries.length;
+ }
+
+ /**
+ * Returns the entry at the specified index.
+ *
+ * @param index The index of the entry.
+ * @return The entry at the specified index.
+ */
+ public Metadata.Entry get(int index) {
+ 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(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ Metadata other = (Metadata) obj;
+ return Arrays.equals(entries, other.entries);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(entries);
+ }
+
+ @Override
+ public String toString() {
+ return "entries=" + Arrays.toString(entries);
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(entries.length);
+ for (Entry entry : entries) {
+ dest.writeParcelable(entry, 0);
+ }
+ }
+
+ public static final Parcelable.Creator<Metadata> CREATOR =
+ new Parcelable.Creator<Metadata>() {
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoder.java
new file mode 100644
index 0000000000..1bc1c7dc06
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoder.java
@@ -0,0 +1,33 @@
+/*
+ * 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.metadata;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Decodes metadata from binary data.
+ */
+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, or null if the metadata could not be decoded.
+ */
+ @Nullable
+ Metadata decode(MetadataInputBuffer inputBuffer);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java
new file mode 100644
index 0000000000..30f6aad4a9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java
@@ -0,0 +1,94 @@
+/*
+ * 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;
+
+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;
+
+/**
+ * A factory for {@link MetadataDecoder} instances.
+ */
+public interface MetadataDecoderFactory {
+
+ /**
+ * Returns whether the factory is able to instantiate a {@link MetadataDecoder} for the given
+ * {@link Format}.
+ *
+ * @param format The {@link Format}.
+ * @return Whether the factory can instantiate a suitable {@link MetadataDecoder}.
+ */
+ boolean supportsFormat(Format format);
+
+ /**
+ * Creates a {@link MetadataDecoder} for the given {@link Format}.
+ *
+ * @param format The {@link Format}.
+ * @return A new {@link MetadataDecoder}.
+ * @throws IllegalArgumentException If the {@link Format} is not supported.
+ */
+ MetadataDecoder createDecoder(Format format);
+
+ /**
+ * Default {@link MetadataDecoder} implementation.
+ *
+ * <p>The formats supported by this factory are:
+ *
+ * <ul>
+ * <li>ID3 ({@link Id3Decoder})
+ * <li>EMSG ({@link EventMessageDecoder})
+ * <li>SCTE-35 ({@link SpliceInfoDecoder})
+ * <li>ICY ({@link IcyDecoder})
+ * </ul>
+ */
+ MetadataDecoderFactory DEFAULT =
+ new MetadataDecoderFactory() {
+
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java
new file mode 100644
index 0000000000..9a265744ec
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java
@@ -0,0 +1,36 @@
+/*
+ * 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.metadata;
+
+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 MetadataDecoder}.
+ */
+public final class MetadataInputBuffer extends DecoderInputBuffer {
+
+ /**
+ * An offset that must be added to the metadata's timestamps after it's been decoded, or
+ * {@link Format#OFFSET_SAMPLE_RELATIVE} if {@link #timeUs} should be added.
+ */
+ public long subsampleOffsetUs;
+
+ public MetadataInputBuffer() {
+ super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataOutput.java
new file mode 100644
index 0000000000..025f9f01bc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataOutput.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.metadata;
+
+/**
+ * Receives metadata output.
+ */
+public interface MetadataOutput {
+
+ /**
+ * Called when there is metadata associated with current playback time.
+ *
+ * @param metadata The metadata.
+ */
+ void onMetadata(Metadata metadata);
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataRenderer.java
new file mode 100644
index 0000000000..329f9ffa7d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataRenderer.java
@@ -0,0 +1,236 @@
+/*
+ * 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.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.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 {
+
+ 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.
+ private static final int MAX_PENDING_METADATA_COUNT = 5;
+
+ private final MetadataDecoderFactory decoderFactory;
+ private final MetadataOutput output;
+ @Nullable private final Handler outputHandler;
+ private final MetadataInputBuffer buffer;
+ private final @NullableType Metadata[] pendingMetadata;
+ private final long[] pendingMetadataTimestamps;
+
+ private int pendingMetadataIndex;
+ private int pendingMetadataCount;
+ @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.
+ */
+ public MetadataRenderer(MetadataOutput output, @Nullable Looper outputLooper) {
+ this(output, outputLooper, MetadataDecoderFactory.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 decoderFactory A factory from which to obtain {@link MetadataDecoder} instances.
+ */
+ public MetadataRenderer(
+ MetadataOutput output, @Nullable Looper outputLooper, MetadataDecoderFactory decoderFactory) {
+ super(C.TRACK_TYPE_METADATA);
+ this.output = Assertions.checkNotNull(output);
+ this.outputHandler =
+ outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this);
+ this.decoderFactory = Assertions.checkNotNull(decoderFactory);
+ buffer = new MetadataInputBuffer();
+ pendingMetadata = new Metadata[MAX_PENDING_METADATA_COUNT];
+ pendingMetadataTimestamps = new long[MAX_PENDING_METADATA_COUNT];
+ }
+
+ @Override
+ @Capabilities
+ public int supportsFormat(Format format) {
+ 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, long offsetUs) {
+ decoder = decoderFactory.createDecoder(formats[0]);
+ }
+
+ @Override
+ protected void onPositionReset(long positionUs, boolean joining) {
+ flushPendingMetadata();
+ inputStreamEnded = false;
+ }
+
+ @Override
+ 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()) {
+ inputStreamEnded = true;
+ } else if (buffer.isDecodeOnly()) {
+ // Do nothing. Note this assumes that all metadata buffers can be decoded independently.
+ // 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 = subsampleOffsetUs;
+ buffer.flip();
+ @Nullable Metadata metadata = castNonNull(decoder).decode(buffer);
+ if (metadata != null) {
+ List<Metadata.Entry> 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) {
+ 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<Metadata.Entry> 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;
+ }
+
+ @Override
+ public boolean isEnded() {
+ return inputStreamEnded;
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ private void invokeRenderer(Metadata metadata) {
+ if (outputHandler != null) {
+ outputHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget();
+ } else {
+ invokeRendererInternal(metadata);
+ }
+ }
+
+ private void flushPendingMetadata() {
+ Arrays.fill(pendingMetadata, null);
+ pendingMetadataIndex = 0;
+ pendingMetadataCount = 0;
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_INVOKE_RENDERER:
+ invokeRendererInternal((Metadata) msg.obj);
+ return true;
+ default:
+ // Should never happen.
+ throw new IllegalStateException();
+ }
+ }
+
+ private void invokeRendererInternal(Metadata metadata) {
+ output.onMetadata(metadata);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
new file mode 100644
index 0000000000..01aac27a27
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
@@ -0,0 +1,202 @@
+/*
+ * 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 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. */
+public final class EventMessage implements Metadata.Entry {
+
+ /**
+ * emsg scheme_id_uri from the <a href="https://aomediacodec.github.io/av1-id3/#semantics">CMAF
+ * spec</a>.
+ */
+ @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 <a
+ * href="https://www.scte.org/SCTEDocs/Standards/ANSI_SCTE%20214-3%202015.pdf">SCTE 214-3
+ * 2015</a>.
+ */
+ @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;
+
+ /**
+ * The value for the event.
+ */
+ public final String value;
+
+ /**
+ * The duration of the event in milliseconds.
+ */
+ public final long durationMs;
+
+ /**
+ * The instance identifier.
+ */
+ public final long id;
+
+ /**
+ * The body of the message.
+ */
+ public final byte[] messageData;
+
+ // Lazily initialized hashcode.
+ 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) {
+ this.schemeIdUri = schemeIdUri;
+ this.value = value;
+ this.durationMs = durationMs;
+ this.id = id;
+ this.messageData = messageData;
+ }
+
+ /* package */ EventMessage(Parcel in) {
+ schemeIdUri = castNonNull(in.readString());
+ value = castNonNull(in.readString());
+ durationMs = in.readLong();
+ id = in.readLong();
+ 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
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = 17;
+ result = 31 * result + (schemeIdUri != null ? schemeIdUri.hashCode() : 0);
+ result = 31 * result + (value != null ? value.hashCode() : 0);
+ result = 31 * result + (int) (durationMs ^ (durationMs >>> 32));
+ result = 31 * result + (int) (id ^ (id >>> 32));
+ result = 31 * result + Arrays.hashCode(messageData);
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ EventMessage other = (EventMessage) obj;
+ 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
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(schemeIdUri);
+ dest.writeString(value);
+ dest.writeLong(durationMs);
+ dest.writeLong(id);
+ dest.writeByteArray(messageData);
+ }
+
+ public static final Parcelable.Creator<EventMessage> CREATOR =
+ new Parcelable.Creator<EventMessage>() {
+
+ @Override
+ public EventMessage createFromParcel(Parcel in) {
+ return new EventMessage(in);
+ }
+
+ @Override
+ public EventMessage[] newArray(int size) {
+ return new EventMessage[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java
new file mode 100644
index 0000000000..09b0a69395
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java
@@ -0,0 +1,47 @@
+/*
+ * 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 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 data encoded by {@link EventMessageEncoder}. */
+public final class EventMessageDecoder implements MetadataDecoder {
+
+ @SuppressWarnings("ByteBufferBackingArray")
+ @Override
+ public Metadata decode(MetadataInputBuffer inputBuffer) {
+ ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data);
+ byte[] data = buffer.array();
+ int size = buffer.limit();
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java
new file mode 100644
index 0000000000..261e39ae70
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/package-info.java
new file mode 100644
index 0000000000..3e54b59a8c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/PictureFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/PictureFrame.java
new file mode 100644
index 0000000000..8a7ffbd976
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<PictureFrame> CREATOR =
+ new Parcelable.Creator<PictureFrame>() {
+
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/VorbisComment.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/VorbisComment.java
new file mode 100644
index 0000000000..b777582b5d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<VorbisComment> CREATOR =
+ new Parcelable.Creator<VorbisComment>() {
+
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/package-info.java
new file mode 100644
index 0000000000..02353ec303
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java
new file mode 100644
index 0000000000..1d44219eda
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java
new file mode 100644
index 0000000000..638e7594eb
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<String, List<String>> 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<String> 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<IcyHeaders> CREATOR =
+ new Parcelable.Creator<IcyHeaders>() {
+
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyInfo.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyInfo.java
new file mode 100644
index 0000000000..4104e41c64
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<IcyInfo> CREATOR =
+ new Parcelable.Creator<IcyInfo>() {
+
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/package-info.java
new file mode 100644
index 0000000000..a8a45e2ef1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.java
new file mode 100644
index 0000000000..f151707e4b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.java
@@ -0,0 +1,108 @@
+/*
+ * 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.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;
+
+/**
+ * APIC (Attached Picture) ID3 frame.
+ */
+public final class ApicFrame extends Id3Frame {
+
+ public static final String ID = "APIC";
+
+ public final String mimeType;
+ @Nullable public final String description;
+ public final int pictureType;
+ public final byte[] pictureData;
+
+ public ApicFrame(
+ String mimeType, @Nullable String description, int pictureType, byte[] pictureData) {
+ super(ID);
+ this.mimeType = mimeType;
+ this.description = description;
+ this.pictureType = pictureType;
+ this.pictureData = pictureData;
+ }
+
+ /* package */ ApicFrame(Parcel in) {
+ super(ID);
+ mimeType = castNonNull(in.readString());
+ description = in.readString();
+ pictureType = in.readInt();
+ pictureData = castNonNull(in.createByteArray());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ ApicFrame other = (ApicFrame) obj;
+ return pictureType == other.pictureType && Util.areEqual(mimeType, other.mimeType)
+ && Util.areEqual(description, other.description)
+ && Arrays.equals(pictureData, other.pictureData);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + pictureType;
+ result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + Arrays.hashCode(pictureData);
+ 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);
+ dest.writeString(description);
+ dest.writeInt(pictureType);
+ dest.writeByteArray(pictureData);
+ }
+
+ public static final Parcelable.Creator<ApicFrame> CREATOR = new Parcelable.Creator<ApicFrame>() {
+
+ @Override
+ public ApicFrame createFromParcel(Parcel in) {
+ return new ApicFrame(in);
+ }
+
+ @Override
+ public ApicFrame[] newArray(int size) {
+ return new ApicFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java
new file mode 100644
index 0000000000..adc66ccdfe
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java
@@ -0,0 +1,83 @@
+/*
+ * 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.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;
+
+/**
+ * Binary ID3 frame.
+ */
+public final class BinaryFrame extends Id3Frame {
+
+ public final byte[] data;
+
+ public BinaryFrame(String id, byte[] data) {
+ super(id);
+ this.data = data;
+ }
+
+ /* package */ BinaryFrame(Parcel in) {
+ super(castNonNull(in.readString()));
+ data = castNonNull(in.createByteArray());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ BinaryFrame other = (BinaryFrame) obj;
+ return id.equals(other.id) && Arrays.equals(data, other.data);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + id.hashCode();
+ result = 31 * result + Arrays.hashCode(data);
+ return result;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeByteArray(data);
+ }
+
+ public static final Parcelable.Creator<BinaryFrame> CREATOR =
+ new Parcelable.Creator<BinaryFrame>() {
+
+ @Override
+ public BinaryFrame createFromParcel(Parcel in) {
+ return new BinaryFrame(in);
+ }
+
+ @Override
+ public BinaryFrame[] newArray(int size) {
+ return new BinaryFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
new file mode 100644
index 0000000000..348781dddf
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
@@ -0,0 +1,145 @@
+/*
+ * 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.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;
+
+/**
+ * Chapter information ID3 frame.
+ */
+public final class ChapterFrame extends Id3Frame {
+
+ public static final String ID = "CHAP";
+
+ public final String chapterId;
+ public final int startTimeMs;
+ public final int endTimeMs;
+ /**
+ * The byte offset of the start of the chapter, or {@link C#POSITION_UNSET} if not set.
+ */
+ public final long startOffset;
+ /**
+ * The byte offset of the end of the chapter, or {@link C#POSITION_UNSET} if not set.
+ */
+ public final long endOffset;
+ private final Id3Frame[] subFrames;
+
+ public ChapterFrame(String chapterId, int startTimeMs, int endTimeMs, long startOffset,
+ long endOffset, Id3Frame[] subFrames) {
+ super(ID);
+ this.chapterId = chapterId;
+ this.startTimeMs = startTimeMs;
+ this.endTimeMs = endTimeMs;
+ this.startOffset = startOffset;
+ this.endOffset = endOffset;
+ this.subFrames = subFrames;
+ }
+
+ /* package */ ChapterFrame(Parcel in) {
+ super(ID);
+ this.chapterId = castNonNull(in.readString());
+ this.startTimeMs = in.readInt();
+ this.endTimeMs = in.readInt();
+ this.startOffset = in.readLong();
+ this.endOffset = in.readLong();
+ int subFrameCount = in.readInt();
+ subFrames = new Id3Frame[subFrameCount];
+ for (int i = 0; i < subFrameCount; i++) {
+ subFrames[i] = in.readParcelable(Id3Frame.class.getClassLoader());
+ }
+ }
+
+ /**
+ * Returns the number of sub-frames.
+ */
+ public int getSubFrameCount() {
+ return subFrames.length;
+ }
+
+ /**
+ * Returns the sub-frame at {@code index}.
+ */
+ public Id3Frame getSubFrame(int index) {
+ return subFrames[index];
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ ChapterFrame other = (ChapterFrame) obj;
+ return startTimeMs == other.startTimeMs
+ && endTimeMs == other.endTimeMs
+ && startOffset == other.startOffset
+ && endOffset == other.endOffset
+ && Util.areEqual(chapterId, other.chapterId)
+ && Arrays.equals(subFrames, other.subFrames);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + startTimeMs;
+ result = 31 * result + endTimeMs;
+ result = 31 * result + (int) startOffset;
+ result = 31 * result + (int) endOffset;
+ result = 31 * result + (chapterId != null ? chapterId.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(chapterId);
+ dest.writeInt(startTimeMs);
+ dest.writeInt(endTimeMs);
+ dest.writeLong(startOffset);
+ dest.writeLong(endOffset);
+ dest.writeInt(subFrames.length);
+ for (Id3Frame subFrame : subFrames) {
+ dest.writeParcelable(subFrame, 0);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<ChapterFrame> CREATOR = new Creator<ChapterFrame>() {
+
+ @Override
+ public ChapterFrame createFromParcel(Parcel in) {
+ return new ChapterFrame(in);
+ }
+
+ @Override
+ public ChapterFrame[] newArray(int size) {
+ return new ChapterFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
new file mode 100644
index 0000000000..9451151c16
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
@@ -0,0 +1,127 @@
+/*
+ * 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.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;
+
+/**
+ * Chapter table of contents ID3 frame.
+ */
+public final class ChapterTocFrame extends Id3Frame {
+
+ public static final String ID = "CTOC";
+
+ public final String elementId;
+ public final boolean isRoot;
+ public final boolean isOrdered;
+ public final String[] children;
+ private final Id3Frame[] subFrames;
+
+ public ChapterTocFrame(String elementId, boolean isRoot, boolean isOrdered, String[] children,
+ Id3Frame[] subFrames) {
+ super(ID);
+ this.elementId = elementId;
+ this.isRoot = isRoot;
+ this.isOrdered = isOrdered;
+ this.children = children;
+ this.subFrames = subFrames;
+ }
+
+ /* package */
+ ChapterTocFrame(Parcel in) {
+ super(ID);
+ this.elementId = castNonNull(in.readString());
+ this.isRoot = in.readByte() != 0;
+ this.isOrdered = in.readByte() != 0;
+ this.children = castNonNull(in.createStringArray());
+ int subFrameCount = in.readInt();
+ subFrames = new Id3Frame[subFrameCount];
+ for (int i = 0; i < subFrameCount; i++) {
+ subFrames[i] = in.readParcelable(Id3Frame.class.getClassLoader());
+ }
+ }
+
+ /**
+ * Returns the number of sub-frames.
+ */
+ public int getSubFrameCount() {
+ return subFrames.length;
+ }
+
+ /**
+ * Returns the sub-frame at {@code index}.
+ */
+ public Id3Frame getSubFrame(int index) {
+ return subFrames[index];
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ ChapterTocFrame other = (ChapterTocFrame) obj;
+ return isRoot == other.isRoot
+ && isOrdered == other.isOrdered
+ && Util.areEqual(elementId, other.elementId)
+ && Arrays.equals(children, other.children)
+ && Arrays.equals(subFrames, other.subFrames);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (isRoot ? 1 : 0);
+ result = 31 * result + (isOrdered ? 1 : 0);
+ result = 31 * result + (elementId != null ? elementId.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(elementId);
+ dest.writeByte((byte) (isRoot ? 1 : 0));
+ dest.writeByte((byte) (isOrdered ? 1 : 0));
+ dest.writeStringArray(children);
+ dest.writeInt(subFrames.length);
+ for (Id3Frame subFrame : subFrames) {
+ dest.writeParcelable(subFrame, 0);
+ }
+ }
+
+ public static final Creator<ChapterTocFrame> CREATOR = new Creator<ChapterTocFrame>() {
+
+ @Override
+ public ChapterTocFrame createFromParcel(Parcel in) {
+ return new ChapterTocFrame(in);
+ }
+
+ @Override
+ public ChapterTocFrame[] newArray(int size) {
+ return new ChapterTocFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.java
new file mode 100644
index 0000000000..98b8c79a96
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.java
@@ -0,0 +1,101 @@
+/*
+ * 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.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;
+
+/**
+ * Comment ID3 frame.
+ */
+public final class CommentFrame extends Id3Frame {
+
+ public static final String ID = "COMM";
+
+ public final String language;
+ public final String description;
+ public final String text;
+
+ public CommentFrame(String language, String description, String text) {
+ super(ID);
+ this.language = language;
+ this.description = description;
+ this.text = text;
+ }
+
+ /* package */ CommentFrame(Parcel in) {
+ super(ID);
+ language = 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;
+ }
+ CommentFrame other = (CommentFrame) obj;
+ return Util.areEqual(description, other.description) && Util.areEqual(language, other.language)
+ && Util.areEqual(text, other.text);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (language != null ? language.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 + ": language=" + language + ", description=" + description;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeString(language);
+ dest.writeString(text);
+ }
+
+ public static final Parcelable.Creator<CommentFrame> CREATOR =
+ new Parcelable.Creator<CommentFrame>() {
+
+ @Override
+ public CommentFrame createFromParcel(Parcel in) {
+ return new CommentFrame(in);
+ }
+
+ @Override
+ public CommentFrame[] newArray(int size) {
+ return new CommentFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.java
new file mode 100644
index 0000000000..58a208a76a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.java
@@ -0,0 +1,112 @@
+/*
+ * 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.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;
+
+/**
+ * GEOB (General Encapsulated Object) ID3 frame.
+ */
+public final class GeobFrame extends Id3Frame {
+
+ public static final String ID = "GEOB";
+
+ public final String mimeType;
+ public final String filename;
+ public final String description;
+ public final byte[] data;
+
+ public GeobFrame(String mimeType, String filename, String description, byte[] data) {
+ super(ID);
+ this.mimeType = mimeType;
+ this.filename = filename;
+ this.description = description;
+ this.data = data;
+ }
+
+ /* package */ GeobFrame(Parcel in) {
+ super(ID);
+ mimeType = castNonNull(in.readString());
+ filename = castNonNull(in.readString());
+ description = castNonNull(in.readString());
+ data = castNonNull(in.createByteArray());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ GeobFrame other = (GeobFrame) obj;
+ return Util.areEqual(mimeType, other.mimeType) && Util.areEqual(filename, other.filename)
+ && Util.areEqual(description, other.description) && Arrays.equals(data, other.data);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);
+ result = 31 * result + (filename != null ? filename.hashCode() : 0);
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + Arrays.hashCode(data);
+ 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);
+ dest.writeString(filename);
+ dest.writeString(description);
+ dest.writeByteArray(data);
+ }
+
+ public static final Parcelable.Creator<GeobFrame> CREATOR = new Parcelable.Creator<GeobFrame>() {
+
+ @Override
+ public GeobFrame createFromParcel(Parcel in) {
+ return new GeobFrame(in);
+ }
+
+ @Override
+ public GeobFrame[] newArray(int size) {
+ return new GeobFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
new file mode 100644
index 0000000000..36e004ed52
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
@@ -0,0 +1,842 @@
+/*
+ * 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.metadata.id3;
+
+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;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Decodes ID3 tags.
+ */
+public final class Id3Decoder implements MetadataDecoder {
+
+ /**
+ * A predicate for determining whether individual frames should be decoded.
+ */
+ public interface FramePredicate {
+
+ /**
+ * Returns whether a frame with the specified parameters should be decoded.
+ *
+ * @param majorVersion The major version of the ID3 tag.
+ * @param id0 The first byte of the frame ID.
+ * @param id1 The second byte of the frame ID.
+ * @param id2 The third byte of the frame ID.
+ * @param id3 The fourth byte of the frame ID.
+ * @return Whether the frame should be decoded.
+ */
+ boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3);
+
+ }
+
+ /** 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 = 0x00494433;
+ /**
+ * Length of an ID3 tag header.
+ */
+ public static final int ID3_HEADER_LENGTH = 10;
+
+ private static final int FRAME_FLAG_V3_IS_COMPRESSED = 0x0080;
+ private static final int FRAME_FLAG_V3_IS_ENCRYPTED = 0x0040;
+ private static final int FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER = 0x0020;
+ private static final int FRAME_FLAG_V4_IS_COMPRESSED = 0x0008;
+ private static final int FRAME_FLAG_V4_IS_ENCRYPTED = 0x0004;
+ private static final int FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER = 0x0040;
+ private static final int FRAME_FLAG_V4_IS_UNSYNCHRONIZED = 0x0002;
+ private static final int FRAME_FLAG_V4_HAS_DATA_LENGTH = 0x0001;
+
+ private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0;
+ private static final int ID3_TEXT_ENCODING_UTF_16 = 1;
+ private static final int ID3_TEXT_ENCODING_UTF_16BE = 2;
+ private static final int ID3_TEXT_ENCODING_UTF_8 = 3;
+
+ @Nullable private final FramePredicate framePredicate;
+
+ public Id3Decoder() {
+ this(null);
+ }
+
+ /**
+ * @param framePredicate Determines which frames are decoded. May be null to decode all frames.
+ */
+ public Id3Decoder(@Nullable FramePredicate framePredicate) {
+ this.framePredicate = framePredicate;
+ }
+
+ @SuppressWarnings("ByteBufferBackingArray")
+ @Override
+ @Nullable
+ public Metadata decode(MetadataInputBuffer inputBuffer) {
+ ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data);
+ return decode(buffer.array(), buffer.limit());
+ }
+
+ /**
+ * Decodes ID3 tags.
+ *
+ * @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, or null if the data could
+ * not be decoded.
+ */
+ @Nullable
+ public Metadata decode(byte[] data, int size) {
+ List<Id3Frame> id3Frames = new ArrayList<>();
+ ParsableByteArray id3Data = new ParsableByteArray(data, size);
+
+ Id3Header id3Header = decodeHeader(id3Data);
+ if (id3Header == null) {
+ return null;
+ }
+
+ int startPosition = id3Data.getPosition();
+ int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10;
+ int framesSize = id3Header.framesSize;
+ if (id3Header.isUnsynchronized) {
+ framesSize = removeUnsynchronization(id3Data, id3Header.framesSize);
+ }
+ id3Data.setLimit(startPosition + framesSize);
+
+ boolean unsignedIntFrameSizeHack = false;
+ if (!validateFrames(id3Data, id3Header.majorVersion, frameHeaderSize, false)) {
+ if (id3Header.majorVersion == 4 && validateFrames(id3Data, 4, frameHeaderSize, true)) {
+ unsignedIntFrameSizeHack = true;
+ } else {
+ Log.w(TAG, "Failed to validate ID3 tag with majorVersion=" + id3Header.majorVersion);
+ return null;
+ }
+ }
+
+ while (id3Data.bytesLeft() >= frameHeaderSize) {
+ Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack,
+ frameHeaderSize, framePredicate);
+ if (frame != null) {
+ id3Frames.add(frame);
+ }
+ }
+
+ return new Metadata(id3Frames);
+ }
+
+ /**
+ * @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");
+ return null;
+ }
+
+ int id = data.readUnsignedInt24();
+ if (id != ID3_TAG) {
+ Log.w(TAG, "Unexpected first three bytes of ID3 tag header: 0x" + String.format("%06X", id));
+ return null;
+ }
+
+ int majorVersion = data.readUnsignedByte();
+ data.skipBytes(1); // Skip minor version.
+ int flags = data.readUnsignedByte();
+ int framesSize = data.readSynchSafeInt();
+
+ if (majorVersion == 2) {
+ boolean isCompressed = (flags & 0x40) != 0;
+ if (isCompressed) {
+ Log.w(TAG, "Skipped ID3 tag with majorVersion=2 and undefined compression scheme");
+ return null;
+ }
+ } else if (majorVersion == 3) {
+ boolean hasExtendedHeader = (flags & 0x40) != 0;
+ if (hasExtendedHeader) {
+ int extendedHeaderSize = data.readInt(); // Size excluding size field.
+ data.skipBytes(extendedHeaderSize);
+ framesSize -= (extendedHeaderSize + 4);
+ }
+ } else if (majorVersion == 4) {
+ boolean hasExtendedHeader = (flags & 0x40) != 0;
+ if (hasExtendedHeader) {
+ int extendedHeaderSize = data.readSynchSafeInt(); // Size including size field.
+ data.skipBytes(extendedHeaderSize - 4);
+ framesSize -= extendedHeaderSize;
+ }
+ boolean hasFooter = (flags & 0x10) != 0;
+ if (hasFooter) {
+ framesSize -= 10;
+ }
+ } else {
+ Log.w(TAG, "Skipped ID3 tag with unsupported majorVersion=" + majorVersion);
+ return null;
+ }
+
+ // isUnsynchronized is advisory only in version 4. Frame level flags are used instead.
+ boolean isUnsynchronized = majorVersion < 4 && (flags & 0x80) != 0;
+ return new Id3Header(majorVersion, isUnsynchronized, framesSize);
+ }
+
+ private static boolean validateFrames(ParsableByteArray id3Data, int majorVersion,
+ int frameHeaderSize, boolean unsignedIntFrameSizeHack) {
+ int startPosition = id3Data.getPosition();
+ try {
+ while (id3Data.bytesLeft() >= frameHeaderSize) {
+ // Read the next frame header.
+ int id;
+ long frameSize;
+ int flags;
+ if (majorVersion >= 3) {
+ id = id3Data.readInt();
+ frameSize = id3Data.readUnsignedInt();
+ flags = id3Data.readUnsignedShort();
+ } else {
+ id = id3Data.readUnsignedInt24();
+ frameSize = id3Data.readUnsignedInt24();
+ flags = 0;
+ }
+ // Validate the frame header and skip to the next one.
+ if (id == 0 && frameSize == 0 && flags == 0) {
+ // We've reached zero padding after the end of the final frame.
+ return true;
+ } else {
+ if (majorVersion == 4 && !unsignedIntFrameSizeHack) {
+ // Parse the data size as a synchsafe integer, as per the spec.
+ if ((frameSize & 0x808080L) != 0) {
+ return false;
+ }
+ frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7)
+ | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21);
+ }
+ boolean hasGroupIdentifier = false;
+ boolean hasDataLength = false;
+ if (majorVersion == 4) {
+ hasGroupIdentifier = (flags & FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER) != 0;
+ hasDataLength = (flags & FRAME_FLAG_V4_HAS_DATA_LENGTH) != 0;
+ } else if (majorVersion == 3) {
+ hasGroupIdentifier = (flags & FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER) != 0;
+ // A V3 frame has data length if and only if it's compressed.
+ hasDataLength = (flags & FRAME_FLAG_V3_IS_COMPRESSED) != 0;
+ }
+ int minimumFrameSize = 0;
+ if (hasGroupIdentifier) {
+ minimumFrameSize++;
+ }
+ if (hasDataLength) {
+ minimumFrameSize += 4;
+ }
+ if (frameSize < minimumFrameSize) {
+ return false;
+ }
+ if (id3Data.bytesLeft() < frameSize) {
+ return false;
+ }
+ id3Data.skipBytes((int) frameSize); // flags
+ }
+ }
+ return true;
+ } finally {
+ id3Data.setPosition(startPosition);
+ }
+ }
+
+ @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();
+ int frameId3 = majorVersion >= 3 ? id3Data.readUnsignedByte() : 0;
+
+ int frameSize;
+ if (majorVersion == 4) {
+ frameSize = id3Data.readUnsignedIntToInt();
+ if (!unsignedIntFrameSizeHack) {
+ frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7)
+ | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21);
+ }
+ } else if (majorVersion == 3) {
+ frameSize = id3Data.readUnsignedIntToInt();
+ } else /* id3Header.majorVersion == 2 */ {
+ frameSize = id3Data.readUnsignedInt24();
+ }
+
+ int flags = majorVersion >= 3 ? id3Data.readUnsignedShort() : 0;
+ if (frameId0 == 0 && frameId1 == 0 && frameId2 == 0 && frameId3 == 0 && frameSize == 0
+ && flags == 0) {
+ // We must be reading zero padding at the end of the tag.
+ id3Data.setPosition(id3Data.limit());
+ return null;
+ }
+
+ int nextFramePosition = id3Data.getPosition() + frameSize;
+ if (nextFramePosition > id3Data.limit()) {
+ Log.w(TAG, "Frame size exceeds remaining tag data");
+ id3Data.setPosition(id3Data.limit());
+ return null;
+ }
+
+ if (framePredicate != null
+ && !framePredicate.evaluate(majorVersion, frameId0, frameId1, frameId2, frameId3)) {
+ // Filtered by the predicate.
+ id3Data.setPosition(nextFramePosition);
+ return null;
+ }
+
+ // Frame flags.
+ boolean isCompressed = false;
+ boolean isEncrypted = false;
+ boolean isUnsynchronized = false;
+ boolean hasDataLength = false;
+ boolean hasGroupIdentifier = false;
+ if (majorVersion == 3) {
+ isCompressed = (flags & FRAME_FLAG_V3_IS_COMPRESSED) != 0;
+ isEncrypted = (flags & FRAME_FLAG_V3_IS_ENCRYPTED) != 0;
+ hasGroupIdentifier = (flags & FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER) != 0;
+ // A V3 frame has data length if and only if it's compressed.
+ hasDataLength = isCompressed;
+ } else if (majorVersion == 4) {
+ hasGroupIdentifier = (flags & FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER) != 0;
+ isCompressed = (flags & FRAME_FLAG_V4_IS_COMPRESSED) != 0;
+ isEncrypted = (flags & FRAME_FLAG_V4_IS_ENCRYPTED) != 0;
+ isUnsynchronized = (flags & FRAME_FLAG_V4_IS_UNSYNCHRONIZED) != 0;
+ hasDataLength = (flags & FRAME_FLAG_V4_HAS_DATA_LENGTH) != 0;
+ }
+
+ if (isCompressed || isEncrypted) {
+ Log.w(TAG, "Skipping unsupported compressed or encrypted frame");
+ id3Data.setPosition(nextFramePosition);
+ return null;
+ }
+
+ if (hasGroupIdentifier) {
+ frameSize--;
+ id3Data.skipBytes(1);
+ }
+ if (hasDataLength) {
+ frameSize -= 4;
+ id3Data.skipBytes(4);
+ }
+ if (isUnsynchronized) {
+ frameSize = removeUnsynchronization(id3Data, frameSize);
+ }
+
+ try {
+ Id3Frame frame;
+ if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X'
+ && (majorVersion == 2 || frameId3 == 'X')) {
+ frame = decodeTxxxFrame(id3Data, frameSize);
+ } else if (frameId0 == 'T') {
+ String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3);
+ frame = decodeTextInformationFrame(id3Data, frameSize, id);
+ } else if (frameId0 == 'W' && frameId1 == 'X' && frameId2 == 'X'
+ && (majorVersion == 2 || frameId3 == 'X')) {
+ frame = decodeWxxxFrame(id3Data, frameSize);
+ } else if (frameId0 == 'W') {
+ String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3);
+ frame = decodeUrlLinkFrame(id3Data, frameSize, id);
+ } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
+ frame = decodePrivFrame(id3Data, frameSize);
+ } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O'
+ && (frameId3 == 'B' || majorVersion == 2)) {
+ frame = decodeGeobFrame(id3Data, frameSize);
+ } else if (majorVersion == 2 ? (frameId0 == 'P' && frameId1 == 'I' && frameId2 == 'C')
+ : (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C')) {
+ frame = decodeApicFrame(id3Data, frameSize, majorVersion);
+ } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M'
+ && (frameId3 == 'M' || majorVersion == 2)) {
+ frame = decodeCommentFrame(id3Data, frameSize);
+ } else if (frameId0 == 'C' && frameId1 == 'H' && frameId2 == 'A' && frameId3 == 'P') {
+ frame = decodeChapterFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack,
+ frameHeaderSize, framePredicate);
+ } 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);
+ }
+ if (frame == null) {
+ Log.w(TAG, "Failed to decode frame: id="
+ + getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3) + ", frameSize="
+ + frameSize);
+ }
+ return frame;
+ } catch (UnsupportedEncodingException e) {
+ Log.w(TAG, "Unsupported character encoding");
+ return null;
+ } finally {
+ id3Data.setPosition(nextFramePosition);
+ }
+ }
+
+ @Nullable
+ private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize)
+ throws UnsupportedEncodingException {
+ if (frameSize < 1) {
+ // Frame is malformed.
+ return null;
+ }
+
+ int encoding = id3Data.readUnsignedByte();
+ String charset = getCharsetName(encoding);
+
+ byte[] data = new byte[frameSize - 1];
+ id3Data.readBytes(data, 0, frameSize - 1);
+
+ int descriptionEndIndex = indexOfEos(data, 0, encoding);
+ String description = new String(data, 0, descriptionEndIndex, charset);
+
+ int valueStartIndex = descriptionEndIndex + delimiterLength(encoding);
+ int valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
+ String value = decodeStringIfValid(data, valueStartIndex, valueEndIndex, charset);
+
+ return new TextInformationFrame("TXXX", description, value);
+ }
+
+ @Nullable
+ private static TextInformationFrame decodeTextInformationFrame(
+ ParsableByteArray id3Data, int frameSize, String id) throws UnsupportedEncodingException {
+ if (frameSize < 1) {
+ // Frame is malformed.
+ return null;
+ }
+
+ int encoding = id3Data.readUnsignedByte();
+ String charset = getCharsetName(encoding);
+
+ byte[] data = new byte[frameSize - 1];
+ id3Data.readBytes(data, 0, frameSize - 1);
+
+ int valueEndIndex = indexOfEos(data, 0, encoding);
+ String value = new String(data, 0, valueEndIndex, charset);
+
+ return new TextInformationFrame(id, null, value);
+ }
+
+ @Nullable
+ private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize)
+ throws UnsupportedEncodingException {
+ if (frameSize < 1) {
+ // Frame is malformed.
+ return null;
+ }
+
+ int encoding = id3Data.readUnsignedByte();
+ String charset = getCharsetName(encoding);
+
+ byte[] data = new byte[frameSize - 1];
+ id3Data.readBytes(data, 0, frameSize - 1);
+
+ int descriptionEndIndex = indexOfEos(data, 0, encoding);
+ String description = new String(data, 0, descriptionEndIndex, charset);
+
+ int urlStartIndex = descriptionEndIndex + delimiterLength(encoding);
+ int urlEndIndex = indexOfZeroByte(data, urlStartIndex);
+ String url = decodeStringIfValid(data, urlStartIndex, urlEndIndex, "ISO-8859-1");
+
+ return new UrlLinkFrame("WXXX", description, url);
+ }
+
+ private static UrlLinkFrame decodeUrlLinkFrame(ParsableByteArray id3Data, int frameSize,
+ String id) throws UnsupportedEncodingException {
+ byte[] data = new byte[frameSize];
+ id3Data.readBytes(data, 0, frameSize);
+
+ int urlEndIndex = indexOfZeroByte(data, 0);
+ String url = new String(data, 0, urlEndIndex, "ISO-8859-1");
+
+ return new UrlLinkFrame(id, null, url);
+ }
+
+ private static PrivFrame decodePrivFrame(ParsableByteArray id3Data, int frameSize)
+ throws UnsupportedEncodingException {
+ byte[] data = new byte[frameSize];
+ id3Data.readBytes(data, 0, frameSize);
+
+ int ownerEndIndex = indexOfZeroByte(data, 0);
+ String owner = new String(data, 0, ownerEndIndex, "ISO-8859-1");
+
+ int privateDataStartIndex = ownerEndIndex + 1;
+ byte[] privateData = copyOfRangeIfValid(data, privateDataStartIndex, data.length);
+
+ return new PrivFrame(owner, privateData);
+ }
+
+ private static GeobFrame decodeGeobFrame(ParsableByteArray id3Data, int frameSize)
+ throws UnsupportedEncodingException {
+ int encoding = id3Data.readUnsignedByte();
+ String charset = getCharsetName(encoding);
+
+ byte[] data = new byte[frameSize - 1];
+ id3Data.readBytes(data, 0, frameSize - 1);
+
+ int mimeTypeEndIndex = indexOfZeroByte(data, 0);
+ String mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1");
+
+ int filenameStartIndex = mimeTypeEndIndex + 1;
+ int filenameEndIndex = indexOfEos(data, filenameStartIndex, encoding);
+ String filename = decodeStringIfValid(data, filenameStartIndex, filenameEndIndex, charset);
+
+ int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding);
+ int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding);
+ String description =
+ decodeStringIfValid(data, descriptionStartIndex, descriptionEndIndex, charset);
+
+ int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding);
+ byte[] objectData = copyOfRangeIfValid(data, objectDataStartIndex, data.length);
+
+ return new GeobFrame(mimeType, filename, description, objectData);
+ }
+
+ private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSize,
+ int majorVersion) throws UnsupportedEncodingException {
+ int encoding = id3Data.readUnsignedByte();
+ String charset = getCharsetName(encoding);
+
+ byte[] data = new byte[frameSize - 1];
+ id3Data.readBytes(data, 0, frameSize - 1);
+
+ String mimeType;
+ int mimeTypeEndIndex;
+ if (majorVersion == 2) {
+ mimeTypeEndIndex = 2;
+ mimeType = "image/" + Util.toLowerInvariant(new String(data, 0, 3, "ISO-8859-1"));
+ if ("image/jpg".equals(mimeType)) {
+ mimeType = "image/jpeg";
+ }
+ } else {
+ mimeTypeEndIndex = indexOfZeroByte(data, 0);
+ mimeType = Util.toLowerInvariant(new String(data, 0, mimeTypeEndIndex, "ISO-8859-1"));
+ if (mimeType.indexOf('/') == -1) {
+ mimeType = "image/" + mimeType;
+ }
+ }
+
+ int pictureType = data[mimeTypeEndIndex + 1] & 0xFF;
+
+ int descriptionStartIndex = mimeTypeEndIndex + 2;
+ int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding);
+ String description = new String(data, descriptionStartIndex,
+ descriptionEndIndex - descriptionStartIndex, charset);
+
+ int pictureDataStartIndex = descriptionEndIndex + delimiterLength(encoding);
+ 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) {
+ // Frame is malformed.
+ return null;
+ }
+
+ int encoding = id3Data.readUnsignedByte();
+ String charset = getCharsetName(encoding);
+
+ byte[] data = new byte[3];
+ id3Data.readBytes(data, 0, 3);
+ String language = new String(data, 0, 3);
+
+ data = new byte[frameSize - 4];
+ id3Data.readBytes(data, 0, frameSize - 4);
+
+ int descriptionEndIndex = indexOfEos(data, 0, encoding);
+ String description = new String(data, 0, descriptionEndIndex, charset);
+
+ int textStartIndex = descriptionEndIndex + delimiterLength(encoding);
+ 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,
+ @Nullable FramePredicate framePredicate)
+ throws UnsupportedEncodingException {
+ int framePosition = id3Data.getPosition();
+ int chapterIdEndIndex = indexOfZeroByte(id3Data.data, framePosition);
+ String chapterId = new String(id3Data.data, framePosition, chapterIdEndIndex - framePosition,
+ "ISO-8859-1");
+ id3Data.setPosition(chapterIdEndIndex + 1);
+
+ int startTime = id3Data.readInt();
+ int endTime = id3Data.readInt();
+ long startOffset = id3Data.readUnsignedInt();
+ if (startOffset == 0xFFFFFFFFL) {
+ startOffset = C.POSITION_UNSET;
+ }
+ long endOffset = id3Data.readUnsignedInt();
+ if (endOffset == 0xFFFFFFFFL) {
+ endOffset = C.POSITION_UNSET;
+ }
+
+ ArrayList<Id3Frame> subFrames = new ArrayList<>();
+ int limit = framePosition + frameSize;
+ while (id3Data.getPosition() < limit) {
+ Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack,
+ frameHeaderSize, framePredicate);
+ if (frame != null) {
+ subFrames.add(frame);
+ }
+ }
+
+ Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()];
+ subFrames.toArray(subFrameArray);
+ return new ChapterFrame(chapterId, startTime, endTime, startOffset, endOffset, subFrameArray);
+ }
+
+ 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,
+ "ISO-8859-1");
+ id3Data.setPosition(elementIdEndIndex + 1);
+
+ int ctocFlags = id3Data.readUnsignedByte();
+ boolean isRoot = (ctocFlags & 0x0002) != 0;
+ boolean isOrdered = (ctocFlags & 0x0001) != 0;
+
+ int childCount = id3Data.readUnsignedByte();
+ String[] children = new String[childCount];
+ for (int i = 0; i < childCount; i++) {
+ int startIndex = id3Data.getPosition();
+ int endIndex = indexOfZeroByte(id3Data.data, startIndex);
+ children[i] = new String(id3Data.data, startIndex, endIndex - startIndex, "ISO-8859-1");
+ id3Data.setPosition(endIndex + 1);
+ }
+
+ ArrayList<Id3Frame> subFrames = new ArrayList<>();
+ int limit = framePosition + frameSize;
+ while (id3Data.getPosition() < limit) {
+ Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack,
+ frameHeaderSize, framePredicate);
+ if (frame != null) {
+ subFrames.add(frame);
+ }
+ }
+
+ Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()];
+ subFrames.toArray(subFrameArray);
+ 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];
+ id3Data.readBytes(frame, 0, frameSize);
+
+ return new BinaryFrame(id, frame);
+ }
+
+ /**
+ * Performs in-place removal of unsynchronization for {@code length} bytes starting from
+ * {@link ParsableByteArray#getPosition()}
+ *
+ * @param data Contains the data to be processed.
+ * @param length The length of the data to be processed.
+ * @return The length of the data after processing.
+ */
+ private static int removeUnsynchronization(ParsableByteArray data, int length) {
+ byte[] bytes = data.data;
+ int startPosition = data.getPosition();
+ for (int i = startPosition; i + 1 < startPosition + length; i++) {
+ if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) {
+ int relativePosition = i - startPosition;
+ System.arraycopy(bytes, i + 2, bytes, i + 1, length - relativePosition - 2);
+ length--;
+ }
+ }
+ return length;
+ }
+
+ /**
+ * Maps encoding byte from ID3v2 frame to a Charset.
+ *
+ * @param encodingByte The value of encoding byte from ID3v2 frame.
+ * @return Charset name.
+ */
+ private static String getCharsetName(int encodingByte) {
+ switch (encodingByte) {
+ 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";
+ }
+ }
+
+ private static String getFrameId(int majorVersion, int frameId0, int frameId1, int frameId2,
+ int frameId3) {
+ return majorVersion == 2 ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
+ : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
+ }
+
+ private static int indexOfEos(byte[] data, int fromIndex, int encoding) {
+ int terminationPos = indexOfZeroByte(data, fromIndex);
+
+ // For single byte encoding charsets, we're done.
+ if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) {
+ return terminationPos;
+ }
+
+ // Otherwise ensure an even index and look for a second zero byte.
+ while (terminationPos < data.length - 1) {
+ if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) {
+ return terminationPos;
+ }
+ terminationPos = indexOfZeroByte(data, terminationPos + 1);
+ }
+
+ return data.length;
+ }
+
+ private static int indexOfZeroByte(byte[] data, int fromIndex) {
+ for (int i = fromIndex; i < data.length; i++) {
+ if (data[i] == (byte) 0) {
+ return i;
+ }
+ }
+ return data.length;
+ }
+
+ private static int delimiterLength(int encodingByte) {
+ return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8)
+ ? 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;
+ private final boolean isUnsynchronized;
+ private final int framesSize;
+
+ public Id3Header(int majorVersion, boolean isUnsynchronized, int framesSize) {
+ this.majorVersion = majorVersion;
+ this.isUnsynchronized = isUnsynchronized;
+ this.framesSize = framesSize;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java
new file mode 100644
index 0000000000..f96b5e752c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java
@@ -0,0 +1,44 @@
+/*
+ * 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.metadata.id3;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+
+/**
+ * Base class for ID3 frames.
+ */
+public abstract class Id3Frame implements Metadata.Entry {
+
+ /**
+ * The frame ID.
+ */
+ public final String id;
+
+ public Id3Frame(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public String toString() {
+ return id;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/InternalFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/InternalFrame.java
new file mode 100644
index 0000000000..ab8ccff343
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<InternalFrame> CREATOR =
+ new Creator<InternalFrame>() {
+
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/MlltFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/MlltFrame.java
new file mode 100644
index 0000000000..441235d7c9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<MlltFrame> CREATOR =
+ new Creator<MlltFrame>() {
+
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.java
new file mode 100644
index 0000000000..248d9996dd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.java
@@ -0,0 +1,94 @@
+/*
+ * 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.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;
+
+/**
+ * PRIV (Private) ID3 frame.
+ */
+public final class PrivFrame extends Id3Frame {
+
+ public static final String ID = "PRIV";
+
+ public final String owner;
+ public final byte[] privateData;
+
+ public PrivFrame(String owner, byte[] privateData) {
+ super(ID);
+ this.owner = owner;
+ this.privateData = privateData;
+ }
+
+ /* package */ PrivFrame(Parcel in) {
+ super(ID);
+ owner = castNonNull(in.readString());
+ privateData = castNonNull(in.createByteArray());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ PrivFrame other = (PrivFrame) obj;
+ return Util.areEqual(owner, other.owner) && Arrays.equals(privateData, other.privateData);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (owner != null ? owner.hashCode() : 0);
+ result = 31 * result + Arrays.hashCode(privateData);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return id + ": owner=" + owner;
+ }
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(owner);
+ dest.writeByteArray(privateData);
+ }
+
+ public static final Parcelable.Creator<PrivFrame> CREATOR = new Parcelable.Creator<PrivFrame>() {
+
+ @Override
+ public PrivFrame createFromParcel(Parcel in) {
+ return new PrivFrame(in);
+ }
+
+ @Override
+ public PrivFrame[] newArray(int size) {
+ return new PrivFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
new file mode 100644
index 0000000000..c0bd36ccf7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
@@ -0,0 +1,96 @@
+/*
+ * 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.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;
+
+/**
+ * Text information ID3 frame.
+ */
+public final class TextInformationFrame extends Id3Frame {
+
+ @Nullable public final String description;
+ public final String value;
+
+ public TextInformationFrame(String id, @Nullable String description, String value) {
+ super(id);
+ this.description = description;
+ this.value = value;
+ }
+
+ /* package */ TextInformationFrame(Parcel in) {
+ super(castNonNull(in.readString()));
+ description = in.readString();
+ value = castNonNull(in.readString());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ TextInformationFrame other = (TextInformationFrame) obj;
+ return id.equals(other.id) && Util.areEqual(description, other.description)
+ && Util.areEqual(value, other.value);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + id.hashCode();
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + (value != null ? value.hashCode() : 0);
+ 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);
+ dest.writeString(description);
+ dest.writeString(value);
+ }
+
+ public static final Parcelable.Creator<TextInformationFrame> CREATOR =
+ new Parcelable.Creator<TextInformationFrame>() {
+
+ @Override
+ public TextInformationFrame createFromParcel(Parcel in) {
+ return new TextInformationFrame(in);
+ }
+
+ @Override
+ public TextInformationFrame[] newArray(int size) {
+ return new TextInformationFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
new file mode 100644
index 0000000000..ced474960e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
@@ -0,0 +1,96 @@
+/*
+ * 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.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;
+
+/**
+ * Url link ID3 frame.
+ */
+public final class UrlLinkFrame extends Id3Frame {
+
+ @Nullable public final String description;
+ public final String url;
+
+ public UrlLinkFrame(String id, @Nullable String description, String url) {
+ super(id);
+ this.description = description;
+ this.url = url;
+ }
+
+ /* package */ UrlLinkFrame(Parcel in) {
+ super(castNonNull(in.readString()));
+ description = in.readString();
+ url = castNonNull(in.readString());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ UrlLinkFrame other = (UrlLinkFrame) obj;
+ return id.equals(other.id) && Util.areEqual(description, other.description)
+ && Util.areEqual(url, other.url);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + id.hashCode();
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + (url != null ? url.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return id + ": url=" + url;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeString(description);
+ dest.writeString(url);
+ }
+
+ public static final Parcelable.Creator<UrlLinkFrame> CREATOR =
+ new Parcelable.Creator<UrlLinkFrame>() {
+
+ @Override
+ public UrlLinkFrame createFromParcel(Parcel in) {
+ return new UrlLinkFrame(in);
+ }
+
+ @Override
+ public UrlLinkFrame[] newArray(int size) {
+ return new UrlLinkFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/package-info.java
new file mode 100644
index 0000000000..87b20161df
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/package-info.java
new file mode 100644
index 0000000000..e5775f7acc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java
new file mode 100644
index 0000000000..3437c8dd73
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java
@@ -0,0 +1,85 @@
+/*
+ * 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.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) {
+ this.ptsAdjustment = ptsAdjustment;
+ this.identifier = identifier;
+ this.commandBytes = commandBytes;
+ }
+
+ private PrivateCommand(Parcel in) {
+ ptsAdjustment = in.readLong();
+ identifier = in.readLong();
+ commandBytes = Util.castNonNull(in.createByteArray());
+ }
+
+ /* package */ static PrivateCommand parseFromSection(ParsableByteArray sectionData,
+ int commandLength, long ptsAdjustment) {
+ long identifier = sectionData.readUnsignedInt();
+ byte[] privateBytes = new byte[commandLength - 4 /* identifier size */];
+ sectionData.readBytes(privateBytes, 0, privateBytes.length);
+ return new PrivateCommand(identifier, privateBytes, ptsAdjustment);
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(ptsAdjustment);
+ dest.writeLong(identifier);
+ dest.writeByteArray(commandBytes);
+ }
+
+ public static final Parcelable.Creator<PrivateCommand> CREATOR =
+ new Parcelable.Creator<PrivateCommand>() {
+
+ @Override
+ public PrivateCommand createFromParcel(Parcel in) {
+ return new PrivateCommand(in);
+ }
+
+ @Override
+ public PrivateCommand[] newArray(int size) {
+ return new PrivateCommand[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java
new file mode 100644
index 0000000000..866a7ec8bc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java
@@ -0,0 +1,37 @@
+/*
+ * 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.metadata.scte35;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+
+/**
+ * Superclass for SCTE35 splice commands.
+ */
+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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java
new file mode 100644
index 0000000000..a90bddb078
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java
@@ -0,0 +1,102 @@
+/*
+ * 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.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.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.
+ */
+public final class SpliceInfoDecoder implements MetadataDecoder {
+
+ private static final int TYPE_SPLICE_NULL = 0x00;
+ private static final int TYPE_SPLICE_SCHEDULE = 0x04;
+ private static final int TYPE_SPLICE_INSERT = 0x05;
+ private static final int TYPE_TIME_SIGNAL = 0x06;
+ private static final int TYPE_PRIVATE_COMMAND = 0xFF;
+
+ private final ParsableByteArray sectionData;
+ private final ParsableBitArray sectionHeader;
+
+ @MonotonicNonNull private TimestampAdjuster timestampAdjuster;
+
+ public SpliceInfoDecoder() {
+ sectionData = new ParsableByteArray();
+ sectionHeader = new ParsableBitArray();
+ }
+
+ @SuppressWarnings("ByteBufferBackingArray")
+ @Override
+ public Metadata decode(MetadataInputBuffer inputBuffer) {
+ ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data);
+
+ // Internal timestamps adjustment.
+ if (timestampAdjuster == null
+ || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) {
+ timestampAdjuster = new TimestampAdjuster(inputBuffer.timeUs);
+ timestampAdjuster.adjustSampleTimestamp(inputBuffer.timeUs - inputBuffer.subsampleOffsetUs);
+ }
+
+ byte[] data = buffer.array();
+ int size = buffer.limit();
+ sectionData.reset(data, size);
+ sectionHeader.reset(data, size);
+ // table_id(8), section_syntax_indicator(1), private_indicator(1), reserved(2),
+ // section_length(12), protocol_version(8), encrypted_packet(1), encryption_algorithm(6).
+ sectionHeader.skipBits(39);
+ long ptsAdjustment = sectionHeader.readBits(1);
+ ptsAdjustment = (ptsAdjustment << 32) | sectionHeader.readBits(32);
+ // cw_index(8), tier(12).
+ sectionHeader.skipBits(20);
+ int spliceCommandLength = sectionHeader.readBits(12);
+ int spliceCommandType = sectionHeader.readBits(8);
+ @Nullable SpliceCommand command = null;
+ // Go to the start of the command by skipping all fields up to command_type.
+ sectionData.skipBytes(14);
+ switch (spliceCommandType) {
+ case TYPE_SPLICE_NULL:
+ command = new SpliceNullCommand();
+ break;
+ case TYPE_SPLICE_SCHEDULE:
+ command = SpliceScheduleCommand.parseFromSection(sectionData);
+ break;
+ case TYPE_SPLICE_INSERT:
+ command = SpliceInsertCommand.parseFromSection(sectionData, ptsAdjustment,
+ timestampAdjuster);
+ break;
+ case TYPE_TIME_SIGNAL:
+ command = TimeSignalCommand.parseFromSection(sectionData, ptsAdjustment, timestampAdjuster);
+ break;
+ case TYPE_PRIVATE_COMMAND:
+ command = PrivateCommand.parseFromSection(sectionData, spliceCommandLength, ptsAdjustment);
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+ return command == null ? new Metadata() : new Metadata(command);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java
new file mode 100644
index 0000000000..5993efb10f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java
@@ -0,0 +1,254 @@
+/*
+ * 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.metadata.scte35;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a splice insert command defined in SCTE35, Section 9.3.3.
+ */
+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<ComponentSplice> 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;
+ /**
+ * 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<ComponentSplice> componentSpliceList, boolean autoReturn, long breakDurationUs,
+ int uniqueProgramId, int availNum, int availsExpected) {
+ this.spliceEventId = spliceEventId;
+ this.spliceEventCancelIndicator = spliceEventCancelIndicator;
+ this.outOfNetworkIndicator = outOfNetworkIndicator;
+ this.programSpliceFlag = programSpliceFlag;
+ this.spliceImmediateFlag = spliceImmediateFlag;
+ this.programSplicePts = programSplicePts;
+ this.programSplicePlaybackPositionUs = programSplicePlaybackPositionUs;
+ this.componentSpliceList = Collections.unmodifiableList(componentSpliceList);
+ this.autoReturn = autoReturn;
+ this.breakDurationUs = breakDurationUs;
+ this.uniqueProgramId = uniqueProgramId;
+ this.availNum = availNum;
+ this.availsExpected = availsExpected;
+ }
+
+ private SpliceInsertCommand(Parcel in) {
+ spliceEventId = in.readLong();
+ spliceEventCancelIndicator = in.readByte() == 1;
+ outOfNetworkIndicator = in.readByte() == 1;
+ programSpliceFlag = in.readByte() == 1;
+ spliceImmediateFlag = in.readByte() == 1;
+ programSplicePts = in.readLong();
+ programSplicePlaybackPositionUs = in.readLong();
+ int componentSpliceListSize = in.readInt();
+ List<ComponentSplice> componentSpliceList = new ArrayList<>(componentSpliceListSize);
+ for (int i = 0; i < componentSpliceListSize; i++) {
+ componentSpliceList.add(ComponentSplice.createFromParcel(in));
+ }
+ this.componentSpliceList = Collections.unmodifiableList(componentSpliceList);
+ autoReturn = in.readByte() == 1;
+ breakDurationUs = in.readLong();
+ uniqueProgramId = in.readInt();
+ availNum = in.readInt();
+ availsExpected = in.readInt();
+ }
+
+ /* package */ static SpliceInsertCommand parseFromSection(ParsableByteArray sectionData,
+ long ptsAdjustment, TimestampAdjuster timestampAdjuster) {
+ long spliceEventId = sectionData.readUnsignedInt();
+ // splice_event_cancel_indicator(1), reserved(7).
+ boolean spliceEventCancelIndicator = (sectionData.readUnsignedByte() & 0x80) != 0;
+ boolean outOfNetworkIndicator = false;
+ boolean programSpliceFlag = false;
+ boolean spliceImmediateFlag = false;
+ long programSplicePts = C.TIME_UNSET;
+ List<ComponentSplice> componentSplices = Collections.emptyList();
+ int uniqueProgramId = 0;
+ int availNum = 0;
+ int availsExpected = 0;
+ boolean autoReturn = false;
+ long breakDurationUs = C.TIME_UNSET;
+ if (!spliceEventCancelIndicator) {
+ int headerByte = sectionData.readUnsignedByte();
+ outOfNetworkIndicator = (headerByte & 0x80) != 0;
+ programSpliceFlag = (headerByte & 0x40) != 0;
+ boolean durationFlag = (headerByte & 0x20) != 0;
+ spliceImmediateFlag = (headerByte & 0x10) != 0;
+ if (programSpliceFlag && !spliceImmediateFlag) {
+ programSplicePts = TimeSignalCommand.parseSpliceTime(sectionData, ptsAdjustment);
+ }
+ if (!programSpliceFlag) {
+ int componentCount = sectionData.readUnsignedByte();
+ componentSplices = new ArrayList<>(componentCount);
+ for (int i = 0; i < componentCount; i++) {
+ int componentTag = sectionData.readUnsignedByte();
+ long componentSplicePts = C.TIME_UNSET;
+ if (!spliceImmediateFlag) {
+ componentSplicePts = TimeSignalCommand.parseSpliceTime(sectionData, ptsAdjustment);
+ }
+ componentSplices.add(new ComponentSplice(componentTag, componentSplicePts,
+ timestampAdjuster.adjustTsTimestamp(componentSplicePts)));
+ }
+ }
+ if (durationFlag) {
+ long firstByte = sectionData.readUnsignedByte();
+ autoReturn = (firstByte & 0x80) != 0;
+ long breakDuration90khz = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt();
+ breakDurationUs = breakDuration90khz * 1000 / 90;
+ }
+ uniqueProgramId = sectionData.readUnsignedShort();
+ availNum = sectionData.readUnsignedByte();
+ availsExpected = sectionData.readUnsignedByte();
+ }
+ return new SpliceInsertCommand(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator,
+ programSpliceFlag, spliceImmediateFlag, programSplicePts,
+ timestampAdjuster.adjustTsTimestamp(programSplicePts), componentSplices, autoReturn,
+ breakDurationUs, uniqueProgramId, availNum, availsExpected);
+ }
+
+ /**
+ * Holds splicing information for specific splice insert command components.
+ */
+ public static final class ComponentSplice {
+
+ public final int componentTag;
+ public final long componentSplicePts;
+ public final long componentSplicePlaybackPositionUs;
+
+ private ComponentSplice(int componentTag, long componentSplicePts,
+ long componentSplicePlaybackPositionUs) {
+ this.componentTag = componentTag;
+ this.componentSplicePts = componentSplicePts;
+ this.componentSplicePlaybackPositionUs = componentSplicePlaybackPositionUs;
+ }
+
+ public void writeToParcel(Parcel dest) {
+ dest.writeInt(componentTag);
+ dest.writeLong(componentSplicePts);
+ dest.writeLong(componentSplicePlaybackPositionUs);
+ }
+
+ public static ComponentSplice createFromParcel(Parcel in) {
+ return new ComponentSplice(in.readInt(), in.readLong(), in.readLong());
+ }
+
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(spliceEventId);
+ dest.writeByte((byte) (spliceEventCancelIndicator ? 1 : 0));
+ dest.writeByte((byte) (outOfNetworkIndicator ? 1 : 0));
+ dest.writeByte((byte) (programSpliceFlag ? 1 : 0));
+ dest.writeByte((byte) (spliceImmediateFlag ? 1 : 0));
+ dest.writeLong(programSplicePts);
+ dest.writeLong(programSplicePlaybackPositionUs);
+ int componentSpliceListSize = componentSpliceList.size();
+ dest.writeInt(componentSpliceListSize);
+ for (int i = 0; i < componentSpliceListSize; i++) {
+ componentSpliceList.get(i).writeToParcel(dest);
+ }
+ dest.writeByte((byte) (autoReturn ? 1 : 0));
+ dest.writeLong(breakDurationUs);
+ dest.writeInt(uniqueProgramId);
+ dest.writeInt(availNum);
+ dest.writeInt(availsExpected);
+ }
+
+ public static final Parcelable.Creator<SpliceInsertCommand> CREATOR =
+ new Parcelable.Creator<SpliceInsertCommand>() {
+
+ @Override
+ public SpliceInsertCommand createFromParcel(Parcel in) {
+ return new SpliceInsertCommand(in);
+ }
+
+ @Override
+ public SpliceInsertCommand[] newArray(int size) {
+ return new SpliceInsertCommand[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java
new file mode 100644
index 0000000000..afc88bbeab
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java
@@ -0,0 +1,47 @@
+/*
+ * 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.metadata.scte35;
+
+import android.os.Parcel;
+
+/**
+ * Represents a splice null command as defined in SCTE35, Section 9.3.1.
+ */
+public final class SpliceNullCommand extends SpliceCommand {
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ // Do nothing.
+ }
+
+ public static final Creator<SpliceNullCommand> CREATOR =
+ new Creator<SpliceNullCommand>() {
+
+ @Override
+ public SpliceNullCommand createFromParcel(Parcel in) {
+ return new SpliceNullCommand();
+ }
+
+ @Override
+ public SpliceNullCommand[] newArray(int size) {
+ return new SpliceNullCommand[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java
new file mode 100644
index 0000000000..e1d369bc87
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java
@@ -0,0 +1,270 @@
+/*
+ * 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.metadata.scte35;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a splice schedule command as defined in SCTE35, Section 9.3.2.
+ */
+public final class SpliceScheduleCommand extends SpliceCommand {
+
+ /**
+ * Represents a splice event as contained in a {@link SpliceScheduleCommand}.
+ */
+ 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<ComponentSplice> 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;
+ /**
+ * 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<ComponentSplice> componentSpliceList, long utcSpliceTime, boolean autoReturn,
+ long breakDurationUs, int uniqueProgramId, int availNum, int availsExpected) {
+ this.spliceEventId = spliceEventId;
+ this.spliceEventCancelIndicator = spliceEventCancelIndicator;
+ this.outOfNetworkIndicator = outOfNetworkIndicator;
+ this.programSpliceFlag = programSpliceFlag;
+ this.componentSpliceList = Collections.unmodifiableList(componentSpliceList);
+ this.utcSpliceTime = utcSpliceTime;
+ this.autoReturn = autoReturn;
+ this.breakDurationUs = breakDurationUs;
+ this.uniqueProgramId = uniqueProgramId;
+ this.availNum = availNum;
+ this.availsExpected = availsExpected;
+ }
+
+ private Event(Parcel in) {
+ this.spliceEventId = in.readLong();
+ this.spliceEventCancelIndicator = in.readByte() == 1;
+ this.outOfNetworkIndicator = in.readByte() == 1;
+ this.programSpliceFlag = in.readByte() == 1;
+ int componentSpliceListLength = in.readInt();
+ ArrayList<ComponentSplice> componentSpliceList = new ArrayList<>(componentSpliceListLength);
+ for (int i = 0; i < componentSpliceListLength; i++) {
+ componentSpliceList.add(ComponentSplice.createFromParcel(in));
+ }
+ this.componentSpliceList = Collections.unmodifiableList(componentSpliceList);
+ this.utcSpliceTime = in.readLong();
+ this.autoReturn = in.readByte() == 1;
+ this.breakDurationUs = in.readLong();
+ this.uniqueProgramId = in.readInt();
+ this.availNum = in.readInt();
+ this.availsExpected = in.readInt();
+ }
+
+ private static Event parseFromSection(ParsableByteArray sectionData) {
+ long spliceEventId = sectionData.readUnsignedInt();
+ // splice_event_cancel_indicator(1), reserved(7).
+ boolean spliceEventCancelIndicator = (sectionData.readUnsignedByte() & 0x80) != 0;
+ boolean outOfNetworkIndicator = false;
+ boolean programSpliceFlag = false;
+ long utcSpliceTime = C.TIME_UNSET;
+ ArrayList<ComponentSplice> componentSplices = new ArrayList<>();
+ int uniqueProgramId = 0;
+ int availNum = 0;
+ int availsExpected = 0;
+ boolean autoReturn = false;
+ long breakDurationUs = C.TIME_UNSET;
+ if (!spliceEventCancelIndicator) {
+ int headerByte = sectionData.readUnsignedByte();
+ outOfNetworkIndicator = (headerByte & 0x80) != 0;
+ programSpliceFlag = (headerByte & 0x40) != 0;
+ boolean durationFlag = (headerByte & 0x20) != 0;
+ if (programSpliceFlag) {
+ utcSpliceTime = sectionData.readUnsignedInt();
+ }
+ if (!programSpliceFlag) {
+ int componentCount = sectionData.readUnsignedByte();
+ componentSplices = new ArrayList<>(componentCount);
+ for (int i = 0; i < componentCount; i++) {
+ int componentTag = sectionData.readUnsignedByte();
+ long componentUtcSpliceTime = sectionData.readUnsignedInt();
+ componentSplices.add(new ComponentSplice(componentTag, componentUtcSpliceTime));
+ }
+ }
+ if (durationFlag) {
+ long firstByte = sectionData.readUnsignedByte();
+ autoReturn = (firstByte & 0x80) != 0;
+ 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, breakDurationUs,
+ uniqueProgramId, availNum, availsExpected);
+ }
+
+ private void writeToParcel(Parcel dest) {
+ dest.writeLong(spliceEventId);
+ dest.writeByte((byte) (spliceEventCancelIndicator ? 1 : 0));
+ dest.writeByte((byte) (outOfNetworkIndicator ? 1 : 0));
+ dest.writeByte((byte) (programSpliceFlag ? 1 : 0));
+ int componentSpliceListSize = componentSpliceList.size();
+ dest.writeInt(componentSpliceListSize);
+ for (int i = 0; i < componentSpliceListSize; i++) {
+ componentSpliceList.get(i).writeToParcel(dest);
+ }
+ dest.writeLong(utcSpliceTime);
+ dest.writeByte((byte) (autoReturn ? 1 : 0));
+ dest.writeLong(breakDurationUs);
+ dest.writeInt(uniqueProgramId);
+ dest.writeInt(availNum);
+ dest.writeInt(availsExpected);
+ }
+
+ private static Event createFromParcel(Parcel in) {
+ return new Event(in);
+ }
+
+ }
+
+ /**
+ * Holds splicing information for specific splice schedule command components.
+ */
+ public static final class ComponentSplice {
+
+ public final int componentTag;
+ public final long utcSpliceTime;
+
+ private ComponentSplice(int componentTag, long utcSpliceTime) {
+ this.componentTag = componentTag;
+ this.utcSpliceTime = utcSpliceTime;
+ }
+
+ private static ComponentSplice createFromParcel(Parcel in) {
+ return new ComponentSplice(in.readInt(), in.readLong());
+ }
+
+ private void writeToParcel(Parcel dest) {
+ dest.writeInt(componentTag);
+ dest.writeLong(utcSpliceTime);
+ }
+
+ }
+
+ /**
+ * The list of scheduled events.
+ */
+ public final List<Event> events;
+
+ private SpliceScheduleCommand(List<Event> events) {
+ this.events = Collections.unmodifiableList(events);
+ }
+
+ private SpliceScheduleCommand(Parcel in) {
+ int eventsSize = in.readInt();
+ ArrayList<Event> events = new ArrayList<>(eventsSize);
+ for (int i = 0; i < eventsSize; i++) {
+ events.add(Event.createFromParcel(in));
+ }
+ this.events = Collections.unmodifiableList(events);
+ }
+
+ /* package */ static SpliceScheduleCommand parseFromSection(ParsableByteArray sectionData) {
+ int spliceCount = sectionData.readUnsignedByte();
+ ArrayList<Event> events = new ArrayList<>(spliceCount);
+ for (int i = 0; i < spliceCount; i++) {
+ events.add(Event.parseFromSection(sectionData));
+ }
+ return new SpliceScheduleCommand(events);
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ int eventsSize = events.size();
+ dest.writeInt(eventsSize);
+ for (int i = 0; i < eventsSize; i++) {
+ events.get(i).writeToParcel(dest);
+ }
+ }
+
+ public static final Parcelable.Creator<SpliceScheduleCommand> CREATOR =
+ new Parcelable.Creator<SpliceScheduleCommand>() {
+
+ @Override
+ public SpliceScheduleCommand createFromParcel(Parcel in) {
+ return new SpliceScheduleCommand(in);
+ }
+
+ @Override
+ public SpliceScheduleCommand[] newArray(int size) {
+ return new SpliceScheduleCommand[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java
new file mode 100644
index 0000000000..f50a029f1b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java
@@ -0,0 +1,93 @@
+/*
+ * 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.metadata.scte35;
+
+import android.os.Parcel;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+
+/**
+ * Represents a time signal command as defined in SCTE35, Section 9.3.4.
+ */
+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) {
+ this.ptsTime = ptsTime;
+ this.playbackPositionUs = playbackPositionUs;
+ }
+
+ /* package */ static TimeSignalCommand parseFromSection(ParsableByteArray sectionData,
+ long ptsAdjustment, TimestampAdjuster timestampAdjuster) {
+ long ptsTime = parseSpliceTime(sectionData, ptsAdjustment);
+ long playbackPositionUs = timestampAdjuster.adjustTsTimestamp(ptsTime);
+ return new TimeSignalCommand(ptsTime, playbackPositionUs);
+ }
+
+ /**
+ * Parses pts_time from splice_time(), defined in Section 9.4.1. Returns {@link C#TIME_UNSET}, if
+ * time_specified_flag is false.
+ *
+ * @param sectionData The section data from which the pts_time is parsed.
+ * @param ptsAdjustment The pts adjustment provided by the splice info section header.
+ * @return The pts_time defined by splice_time(), or {@link C#TIME_UNSET}, if time_specified_flag
+ * is false.
+ */
+ /* package */ static long parseSpliceTime(ParsableByteArray sectionData, long ptsAdjustment) {
+ long firstByte = sectionData.readUnsignedByte();
+ long ptsTime = C.TIME_UNSET;
+ if ((firstByte & 0x80) != 0 /* time_specified_flag */) {
+ // See SCTE35 9.2.1 for more information about pts adjustment.
+ ptsTime = (firstByte & 0x01) << 32 | sectionData.readUnsignedInt();
+ ptsTime += ptsAdjustment;
+ ptsTime &= 0x1FFFFFFFFL;
+ }
+ return ptsTime;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(ptsTime);
+ dest.writeLong(playbackPositionUs);
+ }
+
+ public static final Creator<TimeSignalCommand> CREATOR =
+ new Creator<TimeSignalCommand>() {
+
+ @Override
+ public TimeSignalCommand createFromParcel(Parcel in) {
+ return new TimeSignalCommand(in.readLong(), in.readLong());
+ }
+
+ @Override
+ public TimeSignalCommand[] newArray(int size) {
+ return new TimeSignalCommand[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/package-info.java
new file mode 100644
index 0000000000..17ce76bb9f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ActionFile.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ActionFile.java
new file mode 100644
index 0000000000..5451ea5530
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<DownloadRequest> 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<StreamKey> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java
new file mode 100644
index 0000000000..aa66c73e6b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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}.
+ *
+ * <p>This method must not be called while the {@link DefaultDownloadIndex} is being used by a
+ * {@link DownloadManager}.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java
new file mode 100644
index 0000000000..cc1a2873f5
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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}.
+ *
+ * <p>Equivalent to calling {@link #DefaultDownloadIndex(DatabaseProvider, String)} with {@code
+ * name=""}.
+ *
+ * <p>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<StreamKey> 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<StreamKey> decodeStreamKeys(String encodedStreamKeys) {
+ ArrayList<StreamKey> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java
new file mode 100644
index 0000000000..6391af8a95
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<? extends Downloader> DASH_DOWNLOADER_CONSTRUCTOR;
+ @Nullable private static final Constructor<? extends Downloader> HLS_DOWNLOADER_CONSTRUCTOR;
+ @Nullable private static final Constructor<? extends Downloader> SS_DOWNLOADER_CONSTRUCTOR;
+
+ static {
+ Constructor<? extends Downloader> 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<? extends Downloader> 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<? extends Downloader> 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<? extends Downloader> 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<? extends Downloader> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/Download.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/Download.java
new file mode 100644
index 0000000000..a3bc253a6e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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}
+ *
+ * <ul>
+ * <li>Is {@link DownloadManager#getDownloadsPaused() paused}
+ * <li>Has {@link DownloadManager#getRequirements() Requirements} that are not met
+ * <li>Has already started {@link DownloadManager#getMaxParallelDownloads()
+ * maxParallelDownloads}
+ * </ul>
+ */
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadCursor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadCursor.java
new file mode 100644
index 0000000000..9693e43002
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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 &lt;= position &lt;=
+ * count.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadException.java
new file mode 100644
index 0000000000..cd95b5f922
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadHelper.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadHelper.java
new file mode 100644
index 0000000000..6070b3a80f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>The helper extracts track information from the media, selects tracks for downloading, and
+ * creates {@link DownloadRequest download requests} based on the selected tracks.
+ *
+ * <p>A typical usage of DownloadHelper follows these steps:
+ *
+ * <ol>
+ * <li>Build the helper using one of the {@code forXXX} methods.
+ * <li>Prepare the helper using {@link #prepare(Callback)} and wait for the callback.
+ * <li>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)}.
+ * <li>Create a download request for the selected track using {@link #getDownloadRequest(byte[])}.
+ * <li>Release the helper using {@link #release()}.
+ * </ol>
+ */
+public final class DownloadHelper {
+
+ /**
+ * Default track selection parameters for downloading, but without any {@link Context}
+ * constraints.
+ *
+ * <p>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<? extends MediaSourceFactory> DASH_FACTORY_CONSTRUCTOR =
+ getConstructor("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory");
+
+ @Nullable
+ private static final Constructor<? extends MediaSourceFactory> SS_FACTORY_CONSTRUCTOR =
+ getConstructor("com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory");
+
+ @Nullable
+ private static final Constructor<? extends MediaSourceFactory> 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<FrameworkMediaCrypto> 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<FrameworkMediaCrypto> 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<FrameworkMediaCrypto> 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<? extends MediaSourceFactory> 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<TrackSelection> @MonotonicNonNull [][] trackSelectionsByPeriodAndRenderer;
+ private List<TrackSelection> @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.
+ *
+ * <p>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<TrackSelection> 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<SelectionOverride> 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<StreamKey> streamKeys = new ArrayList<>();
+ List<TrackSelection> 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<TrackSelection>[][]) new List<?>[periodCount][rendererCount];
+ immutableTrackSelectionsByPeriodAndRenderer =
+ (List<TrackSelection>[][]) 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<TrackSelection> 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<? extends MediaSourceFactory> getConstructor(String className) {
+ try {
+ // LINT.IfChange
+ Class<? extends MediaSourceFactory> 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<? extends MediaSourceFactory> constructor,
+ Uri uri,
+ Factory dataSourceFactory,
+ @Nullable DrmSessionManager<?> drmSessionManager,
+ @Nullable List<StreamKey> 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<MediaPeriod> 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<? extends MediaChunk> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadIndex.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadIndex.java
new file mode 100644
index 0000000000..5fbb3e7c0b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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}.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadManager.java
new file mode 100644
index 0000000000..a6ace12343
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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()}.
+ *
+ * <p>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<Listener> 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<Download> 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:
+ *
+ * <ul>
+ * <li>The {@link #getRequirements() Requirements} are not met.
+ * <li>The downloads are not paused (i.e. {@link #getDownloadsPaused()} is {@code false}).
+ * <li>There are downloads in the {@link Download#STATE_QUEUED queued state}.
+ * </ul>
+ */
+ 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<Download> getCurrentDownloads() {
+ return downloads;
+ }
+
+ /** Returns whether downloads are currently paused. */
+ public boolean getDownloadsPaused() {
+ return downloadsPaused;
+ }
+
+ /**
+ * Resumes downloads.
+ *
+ * <p>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<Download> downloads = (List<Download>) 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<Download> 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<Download> downloads;
+ private final HashMap<String, Task> 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<Download> 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<Download> 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<Download> 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<Download> downloads;
+
+ public DownloadUpdate(Download download, boolean isRemove, List<Download> downloads) {
+ this.download = download;
+ this.isRemove = isRemove;
+ this.downloads = downloads;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadProgress.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadProgress.java
new file mode 100644
index 0000000000..177698ec1e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadRequest.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadRequest.java
new file mode 100644
index 0000000000..31a441aa2d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<StreamKey> 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<StreamKey> 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<StreamKey> 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<StreamKey> 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}.
+ *
+ * <p>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<StreamKey> 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<DownloadRequest> CREATOR =
+ new Parcelable.Creator<DownloadRequest>() {
+
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadService.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloadService.java
new file mode 100644
index 0000000000..a2d7d82438
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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:
+ *
+ * <ul>
+ * <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.
+ * </ul>
+ */
+ 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:
+ *
+ * <ul>
+ * <li>{@link #KEY_DOWNLOAD_REQUEST} - A {@link DownloadRequest} defining the download to be
+ * added.
+ * <li>{@link #KEY_STOP_REASON} - An initial stop reason for the download. If omitted {@link
+ * Download#STOP_REASON_NONE} is used.
+ * <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.
+ * </ul>
+ */
+ public static final String ACTION_ADD_DOWNLOAD =
+ "com.google.android.exoplayer.downloadService.action.ADD_DOWNLOAD";
+
+ /**
+ * Removes a download. Extras:
+ *
+ * <ul>
+ * <li>{@link #KEY_CONTENT_ID} - The content id of a download to remove.
+ * <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.
+ * </ul>
+ */
+ public static final String ACTION_REMOVE_DOWNLOAD =
+ "com.google.android.exoplayer.downloadService.action.REMOVE_DOWNLOAD";
+
+ /**
+ * Removes all downloads. Extras:
+ *
+ * <ul>
+ * <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.
+ * </ul>
+ */
+ 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:
+ *
+ * <ul>
+ * <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.
+ * </ul>
+ */
+ public static final String ACTION_RESUME_DOWNLOADS =
+ "com.google.android.exoplayer.downloadService.action.RESUME_DOWNLOADS";
+
+ /**
+ * Pauses all downloads. Extras:
+ *
+ * <ul>
+ * <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.
+ * </ul>
+ */
+ 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:
+ *
+ * <ul>
+ * <li>{@link #KEY_CONTENT_ID} - The content id of a single download to update with the stop
+ * reason. If omitted, all downloads will be updated.
+ * <li>{@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.
+ * <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.
+ * </ul>
+ */
+ 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:
+ *
+ * <ul>
+ * <li>{@link #KEY_REQUIREMENTS} - A {@link Requirements}.
+ * <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.
+ * </ul>
+ */
+ 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<Class<? extends DownloadService>, 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.
+ *
+ * <p>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.
+ *
+ * <p>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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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<? extends DownloadService> 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.
+ *
+ * <p>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.
+ *
+ * <p>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<Download> 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<Download> 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<? extends DownloadService> clazz, String action, boolean foreground) {
+ return getIntent(context, clazz, action).putExtra(KEY_FOREGROUND, foreground);
+ }
+
+ private static Intent getIntent(
+ Context context, Class<? extends DownloadService> 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<Download> 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<? extends DownloadService> serviceClass;
+ @Nullable private DownloadService downloadService;
+
+ private DownloadManagerHelper(
+ Context context,
+ DownloadManager downloadManager,
+ boolean foregroundAllowed,
+ @Nullable Scheduler scheduler,
+ Class<? extends DownloadService> 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<Download> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/Downloader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/Downloader.java
new file mode 100644
index 0000000000..894d908e72
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java
new file mode 100644
index 0000000000..5b2f579868
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloaderFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/DownloaderFactory.java
new file mode 100644
index 0000000000..944f55f161
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/FilterableManifest.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/FilterableManifest.java
new file mode 100644
index 0000000000..1bd32f7d45
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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 <T> The manifest type.
+ */
+public interface FilterableManifest<T> {
+
+ /**
+ * 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<StreamKey> streamKeys);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/FilteringManifestParser.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/FilteringManifestParser.java
new file mode 100644
index 0000000000..a34d749039
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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 <T> The {@link FilterableManifest} type.
+ */
+public final class FilteringManifestParser<T extends FilterableManifest<T>> implements Parser<T> {
+
+ private final Parser<? extends T> parser;
+ @Nullable private final List<StreamKey> 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<? extends T> parser, @Nullable List<StreamKey> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/ProgressiveDownloader.java
new file mode 100644
index 0000000000..7437dab5ca
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/SegmentDownloader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/SegmentDownloader.java
new file mode 100644
index 0000000000..92947b9bc9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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 <M> The type of the manifest object.
+ */
+public abstract class SegmentDownloader<M extends FilterableManifest<M>> implements Downloader {
+
+ /** Smallest unit of content to be downloaded. */
+ protected static class Segment implements Comparable<Segment> {
+
+ /** 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<StreamKey> 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<StreamKey> 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<Segment> 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<Long, Long> 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<Segment> 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<Segment> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/StreamKey.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/StreamKey.java
new file mode 100644
index 0000000000..acbcc9afa4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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").
+ *
+ * <p>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<StreamKey>, 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<StreamKey> CREATOR =
+ new Parcelable.Creator<StreamKey>() {
+
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/WritableDownloadIndex.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/WritableDownloadIndex.java
new file mode 100644
index 0000000000..f57619f0c4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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}.
+ *
+ * <p>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.
+ *
+ * <p>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}.
+ *
+ * <p>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}.
+ *
+ * <p>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}).
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/offline/package-info.java
new file mode 100644
index 0000000000..a353e22107
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/package-info.java
new file mode 100644
index 0000000000..d9cb1c1493
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/PlatformScheduler.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
new file mode 100644
index 0000000000..bb866944d4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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:
+ *
+ * <pre>{@literal
+ * <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+ * <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+ *
+ * <service android:name="com.google.android.exoplayer2.scheduler.PlatformScheduler$PlatformSchedulerService"
+ * android:permission="android.permission.BIND_JOB_SERVICE"
+ * android:exported="true"/>
+ * }</pre>
+ */
+@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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Requirements.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Requirements.java
new file mode 100644
index 0000000000..9ef8fdb3f6
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<Requirements> CREATOR =
+ new Creator<Requirements>() {
+
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java
new file mode 100644
index 0000000000..edb860ac05
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Scheduler.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/Scheduler.java
new file mode 100644
index 0000000000..c7a7afcd2d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/scheduler/package-info.java
new file mode 100644
index 0000000000..b4e68ebfff
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java
new file mode 100644
index 0000000000..1f67f7e645
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java
new file mode 100644
index 0000000000..dba911f622
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java
@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+/**
+ * Interface for callbacks to be notified of {@link MediaSource} events.
+ *
+ * @deprecated Use {@link MediaSourceEventListener}.
+ */
+@Deprecated
+public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener {}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BaseMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BaseMediaSource.java
new file mode 100644
index 0000000000..f9ca6ff311
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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<MediaSourceCaller> mediaSourceCallers;
+ private final HashSet<MediaSourceCaller> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BehindLiveWindowException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BehindLiveWindowException.java
new file mode 100644
index 0000000000..d5eeeb89a6
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/BehindLiveWindowException.java
@@ -0,0 +1,29 @@
+/*
+ * 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 java.io.IOException;
+
+/**
+ * Thrown when a live playback falls behind the available media window.
+ */
+public final class BehindLiveWindowException extends IOException {
+
+ public BehindLiveWindowException() {
+ super();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
new file mode 100644
index 0000000000..7467d946cc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
@@ -0,0 +1,345 @@
+/*
+ * 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 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
+ * samples.
+ */
+public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
+
+ /**
+ * The {@link MediaPeriod} wrapped by this clipping media period.
+ */
+ public final MediaPeriod mediaPeriod;
+
+ @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.
+ *
+ * <p>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, boolean enableInitialDiscontinuity, long startUs, long endUs) {
+ this.mediaPeriod = mediaPeriod;
+ sampleStreams = new ClippingSampleStream[0];
+ pendingInitialDiscontinuityPositionUs = enableInitialDiscontinuity ? startUs : C.TIME_UNSET;
+ this.startUs = startUs;
+ this.endUs = endUs;
+ }
+
+ /**
+ * 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 updateClipping(long startUs, long endUs) {
+ this.startUs = startUs;
+ this.endUs = endUs;
+ }
+
+ @Override
+ public void prepare(MediaPeriod.Callback callback, long positionUs) {
+ this.callback = callback;
+ mediaPeriod.prepare(this, positionUs);
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ mediaPeriod.maybeThrowPrepareError();
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return mediaPeriod.getTrackGroups();
+ }
+
+ @Override
+ public long selectTracks(
+ @NullableType TrackSelection[] selections,
+ boolean[] mayRetainStreamFlags,
+ @NullableType SampleStream[] streams,
+ boolean[] streamResetFlags,
+ long positionUs) {
+ sampleStreams = new ClippingSampleStream[streams.length];
+ @NullableType SampleStream[] childStreams = new SampleStream[streams.length];
+ for (int i = 0; i < streams.length; i++) {
+ sampleStreams[i] = (ClippingSampleStream) streams[i];
+ childStreams[i] = sampleStreams[i] != null ? sampleStreams[i].childStream : null;
+ }
+ 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 (childStreams[i] == null) {
+ sampleStreams[i] = null;
+ } else if (sampleStreams[i] == null || sampleStreams[i].childStream != childStreams[i]) {
+ sampleStreams[i] = new ClippingSampleStream(childStreams[i]);
+ }
+ streams[i] = sampleStreams[i];
+ }
+ return enablePositionUs;
+ }
+
+ @Override
+ 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 (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) {
+ return C.TIME_UNSET;
+ }
+ Assertions.checkState(discontinuityUs >= startUs);
+ Assertions.checkState(endUs == C.TIME_END_OF_SOURCE || discontinuityUs <= endUs);
+ return discontinuityUs;
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ long bufferedPositionUs = mediaPeriod.getBufferedPositionUs();
+ if (bufferedPositionUs == C.TIME_END_OF_SOURCE
+ || (endUs != C.TIME_END_OF_SOURCE && bufferedPositionUs >= endUs)) {
+ return C.TIME_END_OF_SOURCE;
+ }
+ 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);
+ 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
+ public long getNextLoadPositionUs() {
+ long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs();
+ if (nextLoadPositionUs == C.TIME_END_OF_SOURCE
+ || (endUs != C.TIME_END_OF_SOURCE && nextLoadPositionUs >= endUs)) {
+ return C.TIME_END_OF_SOURCE;
+ }
+ return nextLoadPositionUs;
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ return mediaPeriod.continueLoading(positionUs);
+ }
+
+ @Override
+ public boolean isLoading() {
+ return mediaPeriod.isLoading();
+ }
+
+ // MediaPeriod.Callback implementation.
+
+ @Override
+ public void onPrepared(MediaPeriod mediaPeriod) {
+ 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,
+ // which may be before the clipping start point. When the renderer reads a buffer with a
+ // 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.
+ // 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 final class ClippingSampleStream implements SampleStream {
+
+ public final SampleStream childStream;
+
+ private boolean sentEos;
+
+ public ClippingSampleStream(SampleStream childStream) {
+ this.childStream = childStream;
+ }
+
+ public void clearSentEos() {
+ sentEos = false;
+ }
+
+ @Override
+ public boolean isReady() {
+ return !isPendingInitialDiscontinuity() && childStream.isReady();
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ childStream.maybeThrowError();
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean requireFormat) {
+ if (isPendingInitialDiscontinuity()) {
+ return C.RESULT_NOTHING_READ;
+ }
+ if (sentEos) {
+ buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ return C.RESULT_BUFFER_READ;
+ }
+ 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;
+ }
+ return result;
+ }
+
+ @Override
+ public int skipData(long positionUs) {
+ if (isPendingInitialDiscontinuity()) {
+ return C.RESULT_NOTHING_READ;
+ }
+ return childStream.skipData(positionUs);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaSource.java
new file mode 100644
index 0000000000..373076957d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ClippingMediaSource.java
@@ -0,0 +1,375 @@
+/*
+ * 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 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.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 must consist of a single period.
+ */
+public final class ClippingMediaSource extends CompositeMediaSource<Void> {
+
+ /** 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<ClippingMediaPeriod> mediaPeriods;
+ private final Timeline.Window window;
+
+ @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.
+ *
+ * <p>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.
+ *
+ * <p>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,
+ 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
+ @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 {
+ if (clippingError != null) {
+ throw clippingError;
+ }
+ super.maybeThrowSourceInfoRefreshError();
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ ClippingMediaPeriod mediaPeriod =
+ new ClippingMediaPeriod(
+ mediaSource.createPeriod(id, allocator, startPositionUs),
+ enableInitialDiscontinuity,
+ periodStartUs,
+ periodEndUs);
+ mediaPeriods.add(mediaPeriod);
+ return mediaPeriod;
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ Assertions.checkState(mediaPeriods.remove(mediaPeriod));
+ mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
+ 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 ForwardingTimeline {
+
+ 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.
+ *
+ * @param timeline The timeline to clip.
+ * @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)
+ 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);
+ }
+ }
+ 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 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
+ : Math.min(window.defaultPositionUs, endUs);
+ window.defaultPositionUs -= startUs;
+ }
+ long startMs = C.usToMs(startUs);
+ if (window.presentationStartTimeMs != C.TIME_UNSET) {
+ window.presentationStartTimeMs += startMs;
+ }
+ if (window.windowStartTimeMs != C.TIME_UNSET) {
+ window.windowStartTimeMs += startMs;
+ }
+ return window;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ 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);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeMediaSource.java
new file mode 100644
index 0000000000..ed46b8ee94
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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 <T> The type of the id used to identify prepared child sources.
+ */
+public abstract class CompositeMediaSource<T> extends BaseMediaSource {
+
+ private final HashMap<T, MediaSourceAndListener> 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.
+ *
+ * <p>{@link #onChildSourceInfoRefreshed(Object, MediaSource, Timeline)} will be called when the
+ * child source updates its timeline with the same {@code id} passed to this method.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java
new file mode 100644
index 0000000000..9a72903528
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java
@@ -0,0 +1,95 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+
+/**
+ * A {@link SequenceableLoader} that encapsulates multiple other {@link SequenceableLoader}s.
+ */
+public class CompositeSequenceableLoader implements SequenceableLoader {
+
+ protected final SequenceableLoader[] loaders;
+
+ public CompositeSequenceableLoader(SequenceableLoader[] loaders) {
+ this.loaders = loaders;
+ }
+
+ @Override
+ 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();
+ if (loaderNextLoadPositionUs != C.TIME_END_OF_SOURCE) {
+ nextLoadPositionUs = Math.min(nextLoadPositionUs, loaderNextLoadPositionUs);
+ }
+ }
+ 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;
+ boolean madeProgressThisIteration;
+ do {
+ madeProgressThisIteration = false;
+ long nextLoadPositionUs = getNextLoadPositionUs();
+ if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
+ break;
+ }
+ for (SequenceableLoader loader : loaders) {
+ long loaderNextLoadPositionUs = loader.getNextLoadPositionUs();
+ boolean isLoaderBehind =
+ loaderNextLoadPositionUs != C.TIME_END_OF_SOURCE
+ && loaderNextLoadPositionUs <= positionUs;
+ if (loaderNextLoadPositionUs == nextLoadPositionUs || isLoaderBehind) {
+ madeProgressThisIteration |= loader.continueLoading(positionUs);
+ }
+ }
+ madeProgress |= madeProgressThisIteration;
+ } while (madeProgressThisIteration);
+ return madeProgress;
+ }
+
+ @Override
+ public boolean isLoading() {
+ for (SequenceableLoader loader : loaders) {
+ if (loader.isLoading()) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java
new file mode 100644
index 0000000000..1ac76d6167
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
new file mode 100644
index 0000000000..aa6f486473
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
@@ -0,0 +1,1017 @@
+/*
+ * 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.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.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. 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 extends CompositeMediaSource<MediaSourceHolder> {
+
+ 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;
+
+ // Accessed on any thread.
+ @GuardedBy("this")
+ private final List<MediaSourceHolder> mediaSourcesPublic;
+
+ @GuardedBy("this")
+ private final Set<HandlerAndRunnable> pendingOnCompletionActions;
+
+ @GuardedBy("this")
+ @Nullable
+ private Handler playbackThreadHandler;
+
+ // Accessed on the playback thread only.
+ private final List<MediaSourceHolder> mediaSourceHolders;
+ private final Map<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
+ private final Map<Object, MediaSourceHolder> mediaSourceByUid;
+ private final Set<MediaSourceHolder> enabledMediaSourceHolders;
+ private final boolean isAtomic;
+ private final boolean useLazyPreparation;
+
+ private boolean timelineUpdateScheduled;
+ private Set<HandlerAndRunnable> 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(/* 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 &lt;= index &lt;= {@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 &lt;= index &lt;= {@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<MediaSource> 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<MediaSource> 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 &lt;= index &lt;= {@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<MediaSource> 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 &lt;= index &lt;= {@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<MediaSource> mediaSources,
+ Handler handler,
+ Runnable onCompletionAction) {
+ addPublicMediaSources(index, mediaSources, handler, onCompletionAction);
+ }
+
+ /**
+ * Removes a {@link MediaSource} from the playlist.
+ *
+ * <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int,
+ * int)} instead.
+ *
+ * <p>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 &lt;= index &lt; {@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.
+ *
+ * <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int,
+ * int, Handler, Runnable)} instead.
+ *
+ * <p>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 &lt;= index &lt; {@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).
+ *
+ * <p>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 &lt;= index &lt;= {@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 &lt;= index &lt;= {@link #getSize()}.
+ * @throws IndexOutOfBoundsException When the range is malformed, i.e. {@code fromIndex} &lt; 0,
+ * {@code toIndex} &gt; {@link #getSize()}, {@code fromIndex} &gt; {@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.
+ *
+ * <p>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 &lt;= index &lt;= {@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 &lt;= index &lt;= {@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} &lt; 0,
+ * {@code toIndex} &gt; {@link #getSize()}, {@code fromIndex} &gt; {@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 &lt;= index &lt; {@link #getSize()}.
+ * @param newIndex The target index of the media source in the playlist. This index must be in the
+ * range of 0 &lt;= index &lt; {@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 &lt;= index &lt; {@link #getSize()}.
+ * @param newIndex The target index of the media source in the playlist. This index must be in the
+ * range of 0 &lt;= index &lt; {@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 &lt;= index &lt;= {@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
+ 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
+ protected void enableInternal() {
+ // Suppress enabling all child sources here as they can be lazily enabled when creating periods.
+ }
+
+ @Override
+ 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) {
+ MediaSourceHolder holder =
+ Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));
+ holder.mediaSource.releasePeriod(mediaPeriod);
+ holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id);
+ if (!mediaSourceByMediaPeriod.isEmpty()) {
+ disableUnusedMediaSources();
+ }
+ maybeReleaseChildSource(holder);
+ }
+
+ @Override
+ 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<MediaSource> 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<MediaSourceHolder> 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);
+ }
+ }
+ }
+
+ @GuardedBy("this")
+ @Nullable
+ private HandlerAndRunnable createOnCompletionAction(
+ @Nullable Handler handler, @Nullable Runnable runnable) {
+ if (handler == null || runnable == null) {
+ return null;
+ }
+ HandlerAndRunnable handlerAndRunnable = new HandlerAndRunnable(handler, runnable);
+ pendingOnCompletionActions.add(handlerAndRunnable);
+ return handlerAndRunnable;
+ }
+
+ // Internal methods. Called on the playback thread.
+
+ @SuppressWarnings("unchecked")
+ private boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_ADD:
+ MessageData<Collection<MediaSourceHolder>> addMessage =
+ (MessageData<Collection<MediaSourceHolder>>) 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<Integer> removeMessage = (MessageData<Integer>) 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<Integer> moveMessage = (MessageData<Integer>) 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<ShuffleOrder> shuffleOrderMessage =
+ (MessageData<ShuffleOrder>) Util.castNonNull(msg.obj);
+ shuffleOrder = shuffleOrderMessage.customData;
+ scheduleTimelineUpdate(shuffleOrderMessage.onCompletionAction);
+ break;
+ case MSG_UPDATE_TIMELINE:
+ updateTimelineAndScheduleOnCompletionActions();
+ break;
+ case MSG_ON_COMPLETION:
+ Set<HandlerAndRunnable> actions = (Set<HandlerAndRunnable>) Util.castNonNull(msg.obj);
+ dispatchOnCompletionActions(actions);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ return true;
+ }
+
+ 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<HandlerAndRunnable> 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<HandlerAndRunnable> onCompletionActions) {
+ for (HandlerAndRunnable pendingAction : onCompletionActions) {
+ pendingAction.dispatch();
+ }
+ pendingOnCompletionActions.removeAll(onCompletionActions);
+ }
+
+ private void addMediaSourcesInternal(
+ int index, Collection<MediaSourceHolder> 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<MediaSourceHolder> 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<MediaPeriodId> 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<T> {
+
+ 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 Object[] uids;
+ private final HashMap<Object, Integer> childIndexByUid;
+
+ public ConcatenatedTimeline(
+ Collection<MediaSourceHolder> 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;
+ 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.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 windowCount;
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return periodCount;
+ }
+ }
+
+ /** 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java
new file mode 100644
index 0000000000..237510bea3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/DefaultMediaSourceEventListener.java
new file mode 100644
index 0000000000..c25750247f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/EmptySampleStream.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/EmptySampleStream.java
new file mode 100644
index 0000000000..398c6b91fc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/EmptySampleStream.java
@@ -0,0 +1,50 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import java.io.IOException;
+
+/**
+ * An empty {@link SampleStream}.
+ */
+public final class EmptySampleStream implements SampleStream {
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired) {
+ buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ return C.RESULT_BUFFER_READ;
+ }
+
+ @Override
+ public int skipData(long positionUs) {
+ return 0;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaSource.java
new file mode 100644
index 0000000000..3b72f51c44
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaSource.java
@@ -0,0 +1,394 @@
+/*
+ * 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.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;
+
+/** @deprecated Use {@link ProgressiveMediaSource} instead. */
+@Deprecated
+@SuppressWarnings("deprecation")
+public final class ExtractorMediaSource extends CompositeMediaSource<Void> {
+
+ /** @deprecated Use {@link MediaSourceEventListener} instead. */
+ @Deprecated
+ public interface EventListener {
+
+ /**
+ * Called when an error occurs loading media data.
+ * <p>
+ * 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
+ * <em>not</em> 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.
+ */
+ void onLoadError(IOException error);
+
+ }
+
+ /** @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.
+ *
+ * <p>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()}.
+ *
+ * <p>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};
+ }
+ }
+
+ /**
+ * @deprecated Use {@link ProgressiveMediaSource#DEFAULT_LOADING_CHECK_INTERVAL_BYTES} instead.
+ */
+ @Deprecated
+ public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES =
+ ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
+
+ private final ProgressiveMediaSource progressiveMediaSource;
+
+ /**
+ * @param uri The {@link Uri} of the media stream.
+ * @param dataSourceFactory A factory for {@link DataSource}s to read the media.
+ * @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 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.
+ */
+ @Deprecated
+ public ExtractorMediaSource(
+ Uri uri,
+ DataSource.Factory dataSourceFactory,
+ ExtractorsFactory extractorsFactory,
+ @Nullable Handler eventHandler,
+ @Nullable EventListener eventListener) {
+ this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null);
+ }
+
+ /**
+ * @param uri The {@link Uri} of the media stream.
+ * @param dataSourceFactory A factory for {@link DataSource}s to read the media.
+ * @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 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.
+ * @deprecated Use {@link Factory} instead.
+ */
+ @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);
+ }
+
+ /**
+ * @param uri The {@link Uri} of the media stream.
+ * @param dataSourceFactory A factory for {@link DataSource}s to read the media.
+ * @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 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.
+ */
+ @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
+ @Nullable
+ public Object getTag() {
+ return progressiveMediaSource.getTag();
+ }
+
+ @Override
+ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ super.prepareSourceInternal(mediaTransferListener);
+ prepareChildSource(/* id= */ null, progressiveMediaSource);
+ }
+
+ @Override
+ 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) {
+ progressiveMediaSource.releasePeriod(mediaPeriod);
+ }
+
+ @Deprecated
+ private static final class EventListenerWrapper implements MediaSourceEventListener {
+
+ private final EventListener eventListener;
+
+ public EventListenerWrapper(EventListener eventListener) {
+ this.eventListener = Assertions.checkNotNull(eventListener);
+ }
+
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ForwardingTimeline.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ForwardingTimeline.java
new file mode 100644
index 0000000000..ce985708d0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/IcyDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/IcyDataSource.java
new file mode 100644
index 0000000000..b35525743a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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<String, List<String>> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/LoopingMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/LoopingMediaSource.java
new file mode 100644
index 0000000000..880bfd6a4f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/LoopingMediaSource.java
@@ -0,0 +1,214 @@
+/*
+ * 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 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.util.HashMap;
+import java.util.Map;
+
+/**
+ * Loops a {@link MediaSource} a specified number of times.
+ *
+ * <p>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 extends CompositeMediaSource<Void> {
+
+ private final MediaSource childSource;
+ private final int loopCount;
+ private final Map<MediaPeriodId, MediaPeriodId> childMediaPeriodIdToMediaPeriodId;
+ private final Map<MediaPeriod, MediaPeriodId> mediaPeriodToChildMediaPeriodId;
+
+ /**
+ * Loops the provided source indefinitely. Note that it is usually better to use
+ * {@link ExoPlayer#setRepeatMode(int)}.
+ *
+ * @param childSource The {@link MediaSource} to loop.
+ */
+ public LoopingMediaSource(MediaSource childSource) {
+ this(childSource, Integer.MAX_VALUE);
+ }
+
+ /**
+ * 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.
+ */
+ public LoopingMediaSource(MediaSource childSource, int loopCount) {
+ Assertions.checkArgument(loopCount > 0);
+ this.childSource = childSource;
+ this.loopCount = loopCount;
+ childMediaPeriodIdToMediaPeriodId = new HashMap<>();
+ mediaPeriodToChildMediaPeriodId = new HashMap<>();
+ }
+
+ @Override
+ @Nullable
+ public Object getTag() {
+ return childSource.getTag();
+ }
+
+ @Override
+ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ super.prepareSourceInternal(mediaTransferListener);
+ prepareChildSource(/* id= */ null, childSource);
+ }
+
+ @Override
+ 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
+ protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) {
+ Timeline loopingTimeline =
+ loopCount != Integer.MAX_VALUE
+ ? new LoopingTimeline(timeline, loopCount)
+ : new InfinitelyLoopingTimeline(timeline);
+ refreshSourceInfo(loopingTimeline);
+ }
+
+ @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;
+ private final int childWindowCount;
+ 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.loopCount = loopCount;
+ if (childPeriodCount > 0) {
+ Assertions.checkState(loopCount <= Integer.MAX_VALUE / childPeriodCount,
+ "LoopingMediaSource contains too many periods");
+ }
+ }
+
+ @Override
+ public int getWindowCount() {
+ return childWindowCount * loopCount;
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return childPeriodCount * loopCount;
+ }
+
+ @Override
+ protected int getChildIndexByPeriodIndex(int periodIndex) {
+ return periodIndex / childPeriodCount;
+ }
+
+ @Override
+ protected int getChildIndexByWindowIndex(int windowIndex) {
+ return windowIndex / childWindowCount;
+ }
+
+ @Override
+ protected int getChildIndexByChildUid(Object childUid) {
+ if (!(childUid instanceof Integer)) {
+ return C.INDEX_UNSET;
+ }
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaPeriod.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaPeriod.java
new file mode 100644
index 0000000000..4fe7b137b6
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MaskingMediaSource.java
new file mode 100644
index 0000000000..8c867a8c26
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<Void> {
+
+ 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<Object, Long> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaPeriod.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaPeriod.java
new file mode 100644
index 0000000000..3effcec904
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaPeriod.java
@@ -0,0 +1,251 @@
+/*
+ * 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 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;
+
+/**
+ * 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 {
+
+ /**
+ * A callback to be notified of {@link MediaPeriod} events.
+ */
+ interface Callback extends SequenceableLoader.Callback<MediaPeriod> {
+
+ /**
+ * Called when preparation completes.
+ *
+ * <p>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.
+ *
+ * <p>{@code callback.onPrepared} is called when preparation completes. If preparation fails,
+ * {@link #maybeThrowPrepareError()} will throw an {@link IOException}.
+ *
+ * <p>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, long positionUs);
+
+ /**
+ * Throws an error that's preventing the period from becoming prepared. Does nothing if no such
+ * error exists.
+ *
+ * <p>This method is only called before the period has completed preparation.
+ *
+ * @throws IOException The underlying error.
+ */
+ void maybeThrowPrepareError() throws IOException;
+
+ /**
+ * Returns the {@link TrackGroup}s exposed by the period.
+ *
+ * <p>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}.
+ *
+ * <p>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<StreamKey> getStreamKeys(List<TrackSelection> trackSelections) {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Performs a track selection.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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 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. 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(
+ @NullableType TrackSelection[] selections,
+ boolean[] mayRetainStreamFlags,
+ @NullableType SampleStream[] streams,
+ boolean[] streamResetFlags,
+ long positionUs);
+
+ /**
+ * Discards buffered media up to the specified position.
+ *
+ * <p>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, boolean toKeyframe);
+
+ /**
+ * Attempts to read a discontinuity.
+ *
+ * <p>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.
+ *
+ * <p>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();
+
+ /**
+ * Attempts to seek to the specified position in microseconds.
+ *
+ * <p>After this method has been called, all {@link SampleStream}s provided by the period are
+ * guaranteed to start from a key frame.
+ *
+ * <p>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}.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>This method is only called after the period has been prepared. It may be called when no
+ * tracks are selected.
+ */
+ @Override
+ long getNextLoadPositionUs();
+
+ /**
+ * Attempts to continue loading.
+ *
+ * <p>This method may be called both during and after the period has been prepared.
+ *
+ * <p>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.
+ *
+ * <p>This method is only called after the period has been prepared.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSource.java
new file mode 100644
index 0000000000..7e757d5ade
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSource.java
@@ -0,0 +1,325 @@
+/*
+ * 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.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;
+
+/**
+ * Defines and provides media to be played by an {@link org.mozilla.thirdparty.com.google.android.exoplayer2ExoPlayer}. A
+ * MediaSource has two main responsibilities:
+ *
+ * <ul>
+ * <li>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)}.
+ * <li>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.
+ * </ul>
+ *
+ * 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 {
+
+ /** A caller of media sources, which will be notified of source events. */
+ interface MediaSourceCaller {
+
+ /**
+ * Called when the {@link Timeline} has been refreshed.
+ *
+ * <p>Called on the playback thread.
+ *
+ * @param source The {@link MediaSource} whose info has been refreshed.
+ * @param timeline The source's timeline.
+ */
+ 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;
+ }
+ }
+
+ /**
+ * Adds a {@link MediaSourceEventListener} to the list of listeners which are notified of media
+ * source events.
+ *
+ * @param handler A handler on the which listener events will be posted.
+ * @param eventListener The listener to be added.
+ */
+ 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}.
+ *
+ * <p>Should not be called directly from application code.
+ *
+ * <p>{@link MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} will be called once
+ * the source has a {@link Timeline}.
+ *
+ * <p>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.
+ *
+ * <p>Should not be called directly from application code.
+ *
+ * <p>Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}.
+ */
+ void maybeThrowSourceInfoRefreshError() throws IOException;
+
+ /**
+ * Enables the source for the creation of {@link MediaPeriod MediaPeriods}.
+ *
+ * <p>Should not be called directly from application code.
+ *
+ * <p>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}.
+ *
+ * <p>Should not be called directly from application code.
+ *
+ * <p>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 startPositionUs The expected start position, in microseconds.
+ * @return A new {@link MediaPeriod}.
+ */
+ MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs);
+
+ /**
+ * Releases the period.
+ *
+ * <p>Should not be called directly from application code.
+ *
+ * @param mediaPeriod The period to release.
+ */
+ void releasePeriod(MediaPeriod mediaPeriod);
+
+ /**
+ * 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.
+ *
+ * <p>Should not be called directly from application code.
+ *
+ * <p>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 disable(MediaSourceCaller caller);
+
+ /**
+ * Unregisters a caller, and disables and releases the source if no longer required.
+ *
+ * <p>Should not be called directly from application code.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceEventListener.java
new file mode 100644
index 0000000000..53c50d8a26
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<String, List<String>> 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<String, List<String>> 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.
+ *
+ * <p>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
+ * <em>not</em> be called in addition to this method.
+ *
+ * <p>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
+ * <em>not</em> 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<ListenerAndHandler> listenerAndHandlers;
+ private final long mediaTimeOffsetMs;
+
+ /** Creates an event dispatcher. */
+ public EventDispatcher() {
+ this(
+ /* listenerAndHandlers= */ new CopyOnWriteArrayList<>(),
+ /* windowIndex= */ 0,
+ /* mediaPeriodId= */ null,
+ /* mediaTimeOffsetMs= */ 0);
+ }
+
+ private EventDispatcher(
+ CopyOnWriteArrayList<ListenerAndHandler> 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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MediaSourceFactory.java
new file mode 100644
index 0000000000..37c9dcee25
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<StreamKey> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaPeriod.java
new file mode 100644
index 0000000000..f3315ec5cd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaPeriod.java
@@ -0,0 +1,256 @@
+/*
+ * 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 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.
+ */
+/* package */ final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
+
+ public final MediaPeriod[] periods;
+
+ private final IdentityHashMap<SampleStream, Integer> streamPeriodIndices;
+ private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
+ private final ArrayList<MediaPeriod> childrenPendingPreparation;
+
+ @Nullable private Callback callback;
+ @Nullable private TrackGroupArray trackGroups;
+ private MediaPeriod[] enabledPeriods;
+ private SequenceableLoader compositeSequenceableLoader;
+
+ 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, long positionUs) {
+ this.callback = callback;
+ Collections.addAll(childrenPendingPreparation, periods);
+ for (MediaPeriod period : periods) {
+ period.prepare(this, positionUs);
+ }
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ for (MediaPeriod period : periods) {
+ period.maybeThrowPrepareError();
+ }
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return Assertions.checkNotNull(trackGroups);
+ }
+
+ @Override
+ 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];
+ for (int i = 0; i < selections.length; i++) {
+ streamChildIndices[i] = streams[i] == null ? C.INDEX_UNSET
+ : streamPeriodIndices.get(streams[i]);
+ selectionChildIndices[i] = C.INDEX_UNSET;
+ if (selections[i] != null) {
+ TrackGroup trackGroup = selections[i].getTrackGroup();
+ for (int j = 0; j < periods.length; j++) {
+ if (periods[j].getTrackGroups().indexOf(trackGroup) != C.INDEX_UNSET) {
+ selectionChildIndices[i] = j;
+ break;
+ }
+ }
+ }
+ }
+ streamPeriodIndices.clear();
+ // Select tracks for each child, copying the resulting streams back into a new streams array.
+ @NullableType SampleStream[] newStreams = new SampleStream[selections.length];
+ @NullableType SampleStream[] childStreams = new SampleStream[selections.length];
+ @NullableType TrackSelection[] childSelections = new TrackSelection[selections.length];
+ ArrayList<MediaPeriod> enabledPeriodsList = new ArrayList<>(periods.length);
+ for (int i = 0; i < periods.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;
+ }
+ long selectPositionUs = periods[i].selectTracks(childSelections, mayRetainStreamFlags,
+ childStreams, streamResetFlags, positionUs);
+ if (i == 0) {
+ positionUs = selectPositionUs;
+ } else if (selectPositionUs != positionUs) {
+ 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.
+ SampleStream childStream = Assertions.checkNotNull(childStreams[j]);
+ newStreams[j] = childStreams[j];
+ periodEnabled = true;
+ streamPeriodIndices.put(childStream, i);
+ } else if (streamChildIndices[j] == i) {
+ // Assert that the child cleared any previous stream.
+ Assertions.checkState(childStreams[j] == null);
+ }
+ }
+ if (periodEnabled) {
+ enabledPeriodsList.add(periods[i]);
+ }
+ }
+ // Copy the new streams back into the streams array.
+ System.arraycopy(newStreams, 0, streams, 0, newStreams.length);
+ // Update the local state.
+ enabledPeriods = new MediaPeriod[enabledPeriodsList.size()];
+ enabledPeriodsList.toArray(enabledPeriods);
+ compositeSequenceableLoader =
+ compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(enabledPeriods);
+ return positionUs;
+ }
+
+ @Override
+ public void discardBuffer(long positionUs, boolean toKeyframe) {
+ for (MediaPeriod period : enabledPeriods) {
+ period.discardBuffer(positionUs, toKeyframe);
+ }
+ }
+
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ compositeSequenceableLoader.reevaluateBuffer(positionUs);
+ }
+
+ @Override
+ public boolean continueLoading(long 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 compositeSequenceableLoader.getNextLoadPositionUs();
+ }
+
+ @Override
+ public long readDiscontinuity() {
+ long positionUs = periods[0].readDiscontinuity();
+ // 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.");
+ }
+ }
+ // It must be possible to seek enabled periods to the new position, if there is one.
+ if (positionUs != C.TIME_UNSET) {
+ for (MediaPeriod enabledPeriod : enabledPeriods) {
+ if (enabledPeriod != periods[0]
+ && enabledPeriod.seekToUs(positionUs) != positionUs) {
+ throw new IllegalStateException("Unexpected child seekToUs result.");
+ }
+ }
+ }
+ return positionUs;
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ return compositeSequenceableLoader.getBufferedPositionUs();
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ positionUs = enabledPeriods[0].seekToUs(positionUs);
+ // 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("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 preparedPeriod) {
+ childrenPendingPreparation.remove(preparedPeriod);
+ if (!childrenPendingPreparation.isEmpty()) {
+ return;
+ }
+ int totalTrackGroupCount = 0;
+ for (MediaPeriod period : periods) {
+ totalTrackGroupCount += period.getTrackGroups().length;
+ }
+ TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount];
+ int trackGroupIndex = 0;
+ for (MediaPeriod period : periods) {
+ TrackGroupArray periodTrackGroups = period.getTrackGroups();
+ int periodTrackGroupCount = periodTrackGroups.length;
+ for (int j = 0; j < periodTrackGroupCount; j++) {
+ trackGroupArray[trackGroupIndex++] = periodTrackGroups.get(j);
+ }
+ }
+ trackGroups = new TrackGroupArray(trackGroupArray);
+ Assertions.checkNotNull(callback).onPrepared(this);
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod ignored) {
+ Assertions.checkNotNull(callback).onContinueLoadingRequested(this);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaSource.java
new file mode 100644
index 0000000000..ac2ef3c7da
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/MergingMediaSource.java
@@ -0,0 +1,184 @@
+/*
+ * 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 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.
+ *
+ * <p>The {@link Timeline}s of the sources being merged must have the same number of periods.
+ */
+public final class MergingMediaSource extends CompositeMediaSource<Integer> {
+
+ /**
+ * Thrown when a {@link MergingMediaSource} cannot merge its sources.
+ */
+ public static final class IllegalMergeException extends IOException {
+
+ /** The reason the merge failed. One of {@link #REASON_PERIOD_COUNT_MISMATCH}. */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({REASON_PERIOD_COUNT_MISMATCH})
+ public @interface Reason {}
+ /**
+ * The sources have different period counts.
+ */
+ public static final int REASON_PERIOD_COUNT_MISMATCH = 0;
+
+ /**
+ * The reason the merge failed.
+ */
+ @Reason public final int reason;
+
+ /**
+ * @param reason The reason the merge failed.
+ */
+ public IllegalMergeException(@Reason int reason) {
+ this.reason = reason;
+ }
+
+ }
+
+ private static final int PERIOD_COUNT_UNSET = -1;
+
+ private final MediaSource[] mediaSources;
+ private final Timeline[] timelines;
+ private final ArrayList<MediaSource> pendingTimelineSources;
+ private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
+
+ private int periodCount;
+ @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));
+ periodCount = PERIOD_COUNT_UNSET;
+ timelines = new Timeline[mediaSources.length];
+ }
+
+ @Override
+ @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++) {
+ prepareChildSource(i, mediaSources[i]);
+ }
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ if (mergeError != null) {
+ throw mergeError;
+ }
+ super.maybeThrowSourceInfoRefreshError();
+ }
+
+ @Override
+ 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++) {
+ MediaPeriodId childMediaPeriodId =
+ id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex));
+ periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator, startPositionUs);
+ }
+ return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods);
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ MergingMediaPeriod mergingPeriod = (MergingMediaPeriod) mediaPeriod;
+ for (int i = 0; i < mediaSources.length; i++) {
+ mediaSources[i].releasePeriod(mergingPeriod.periods[i]);
+ }
+ }
+
+ @Override
+ protected void releaseSourceInternal() {
+ super.releaseSourceInternal();
+ Arrays.fill(timelines, null);
+ periodCount = PERIOD_COUNT_UNSET;
+ mergeError = null;
+ pendingTimelineSources.clear();
+ Collections.addAll(pendingTimelineSources, mediaSources);
+ }
+
+ @Override
+ protected void onChildSourceInfoRefreshed(
+ Integer id, MediaSource mediaSource, Timeline timeline) {
+ if (mergeError == null) {
+ mergeError = checkTimelineMerges(timeline);
+ }
+ if (mergeError != null) {
+ return;
+ }
+ pendingTimelineSources.remove(mediaSource);
+ timelines[id] = timeline;
+ if (pendingTimelineSources.isEmpty()) {
+ refreshSourceInfo(timelines[0]);
+ }
+ }
+
+ @Override
+ @Nullable
+ protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
+ Integer id, MediaPeriodId mediaPeriodId) {
+ return id == 0 ? mediaPeriodId : null;
+ }
+
+ @Nullable
+ private IllegalMergeException checkTimelineMerges(Timeline timeline) {
+ if (periodCount == PERIOD_COUNT_UNSET) {
+ periodCount = timeline.getPeriodCount();
+ } else if (timeline.getPeriodCount() != periodCount) {
+ return new IllegalMergeException(IllegalMergeException.REASON_PERIOD_COUNT_MISMATCH);
+ }
+ return null;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java
new file mode 100644
index 0000000000..4c62a73edb
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<ProgressiveMediaPeriod.ExtractingLoadable>,
+ 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<String, String> 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<String, String> createIcyMetadataHeaders() {
+ Map<String, String> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ProgressiveMediaSource.java
new file mode 100644
index 0000000000..bed34a354b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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}.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleDataQueue.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleDataQueue.java
new file mode 100644
index 0000000000..81933a468d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleQueue.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleQueue.java
new file mode 100644
index 0000000000..639cccee00
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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}.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>{@link Format Formats} read from this method may be associated to a {@link DrmSession}
+ * through {@link FormatHolder#drmSession}, which is populated in two scenarios:
+ *
+ * <ul>
+ * <li>The {@link Format} has a non-null {@link Format#drmInitData}.
+ * <li>The {@link DrmSessionManager} provides placeholder sessions for this queue's track type.
+ * See {@link DrmSessionManager#acquirePlaceholderSession(Looper, int)}.
+ * </ul>
+ *
+ * @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)}).
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleStream.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleStream.java
new file mode 100644
index 0000000000..54a7d0f895
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SampleStream.java
@@ -0,0 +1,79 @@
+/*
+ * 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 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 java.io.IOException;
+
+/**
+ * A stream of media samples (and associated format information).
+ */
+public interface SampleStream {
+
+ /**
+ * Returns whether data is available to be read.
+ * <p>
+ * Note: If the stream has ended then a buffer with the end of stream flag can always be read from
+ * {@link #readData(FormatHolder, DecoderInputBuffer, boolean)}. Hence an ended stream is always
+ * ready.
+ *
+ * @return Whether data is available to be read.
+ */
+ boolean isReady();
+
+ /**
+ * Throws an error that's preventing data from being read. Does nothing if no such error exists.
+ *
+ * @throws IOException The underlying error.
+ */
+ void maybeThrowError() throws IOException;
+
+ /**
+ * Attempts to read from the stream.
+ *
+ * <p>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. 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.
+ * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
+ * {@link C#RESULT_BUFFER_READ}.
+ */
+ int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired);
+
+ /**
+ * Attempts to skip to the keyframe before the specified position, or to the end of the stream if
+ * {@code positionUs} is beyond it.
+ *
+ * @param positionUs The specified time.
+ * @return The number of samples that were skipped.
+ */
+ int skipData(long positionUs);
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SequenceableLoader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SequenceableLoader.java
new file mode 100644
index 0000000000..09cb8b663b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SequenceableLoader.java
@@ -0,0 +1,77 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+
+// TODO: Clarify the requirements for implementing this interface [Internal ref: b/36250203].
+/**
+ * A loader that can proceed in approximate synchronization with other loaders.
+ */
+public interface SequenceableLoader {
+
+ /**
+ * A callback to be notified of {@link SequenceableLoader} events.
+ */
+ interface Callback<T extends SequenceableLoader> {
+
+ /**
+ * Called by the loader to indicate that it wishes for its {@link #continueLoading(long)} method
+ * to be called when it can continue to load data. Called on the playback thread.
+ */
+ void onContinueLoadingRequested(T source);
+
+ }
+
+ /**
+ * 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.
+ */
+ long getNextLoadPositionUs();
+
+ /**
+ * Attempts to continue loading.
+ *
+ * @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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ShuffleOrder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ShuffleOrder.java
new file mode 100644
index 0000000000..f137054145
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SilenceMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SilenceMediaSource.java
new file mode 100644
index 0000000000..096cc66622
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<SampleStream> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SinglePeriodTimeline.java
new file mode 100644
index 0000000000..72d805dfa3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SinglePeriodTimeline.java
@@ -0,0 +1,227 @@
+/*
+ * 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 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;
+
+/**
+ * A {@link Timeline} consisting of a single period and static window.
+ */
+public final class SinglePeriodTimeline extends Timeline {
+
+ 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 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, boolean isDynamic, boolean isLive) {
+ this(durationUs, isSeekable, isDynamic, isLive, /* manifest= */ null, /* tag= */ null);
+ }
+
+ /**
+ * 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.
+ * @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 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
+ public int getWindowCount() {
+ return 1;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
+ Assertions.checkIndex(windowIndex, 0, 1);
+ long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs;
+ 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(
+ Window.SINGLE_WINDOW_UID,
+ tag,
+ manifest,
+ presentationStartTimeMs,
+ windowStartTimeMs,
+ isSeekable,
+ isDynamic,
+ isLive,
+ windowDefaultStartPositionUs,
+ windowDurationUs,
+ 0,
+ 0,
+ windowPositionInPeriodUs);
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return 1;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ Assertions.checkIndex(periodIndex, 0, 1);
+ Object uid = setIds ? UID : null;
+ return period.set(/* id= */ null, uid, 0, periodDurationUs, -windowPositionInPeriodUs);
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
new file mode 100644
index 0000000000..6c7d92dac9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
@@ -0,0 +1,423 @@
+/*
+ * 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 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.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.
+ */
+/* package */ final class SingleSampleMediaPeriod implements MediaPeriod,
+ Loader.Callback<SingleSampleMediaPeriod.SourceLoadable> {
+
+ /**
+ * The initial size of the allocation used to hold the sample data.
+ */
+ private static final int INITIAL_SAMPLE_SIZE = 1024;
+
+ private final DataSpec dataSpec;
+ private final DataSource.Factory dataSourceFactory;
+ @Nullable private final TransferListener transferListener;
+ private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private final EventDispatcher eventDispatcher;
+ private final TrackGroupArray tracks;
+ private final ArrayList<SampleStreamImpl> 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 @MonotonicNonNull [] sampleData;
+ /* package */ int sampleSize;
+
+ 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.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, long positionUs) {
+ callback.onPrepared(this);
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return tracks;
+ }
+
+ @Override
+ 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]);
+ streams[i] = null;
+ }
+ if (streams[i] == null && selections[i] != null) {
+ SampleStreamImpl stream = new SampleStreamImpl();
+ sampleStreams.add(stream);
+ streams[i] = stream;
+ streamResetFlags[i] = true;
+ }
+ }
+ return positionUs;
+ }
+
+ @Override
+ 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() || loader.hasFatalError()) {
+ return false;
+ }
+ 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;
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ return loadingFinished || loader.isLoading() ? C.TIME_END_OF_SOURCE : 0;
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ return loadingFinished ? C.TIME_END_OF_SOURCE : 0;
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ for (int i = 0; i < sampleStreams.size(); i++) {
+ 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 = (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) {
+ 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 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);
+
+ 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 {
+
+ private static final int STREAM_STATE_SEND_FORMAT = 0;
+ private static final int STREAM_STATE_SEND_SAMPLE = 1;
+ private static final int STREAM_STATE_END_OF_STREAM = 2;
+
+ private int streamState;
+ private boolean notifiedDownstreamFormat;
+
+ public void reset() {
+ if (streamState == STREAM_STATE_END_OF_STREAM) {
+ streamState = STREAM_STATE_SEND_SAMPLE;
+ }
+ }
+
+ @Override
+ public boolean isReady() {
+ return loadingFinished;
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ 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;
+ } else if (requireFormat || streamState == STREAM_STATE_SEND_FORMAT) {
+ formatHolder.format = format;
+ streamState = STREAM_STATE_SEND_SAMPLE;
+ return C.RESULT_FORMAT_READ;
+ } 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 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 {
+
+ public final DataSpec dataSpec;
+
+ private final StatsDataSource 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
+ public void cancelLoad() {
+ // Never happens.
+ }
+
+ @Override
+ public void load() throws IOException, InterruptedException {
+ // We always load from the beginning, so reset bytesRead to 0.
+ dataSource.resetBytesRead();
+ try {
+ // Create and open the input.
+ dataSource.open(dataSpec);
+ // Load the sample data.
+ int result = 0;
+ while (result != C.RESULT_END_OF_INPUT) {
+ int sampleSize = (int) dataSource.getBytesRead();
+ if (sampleData == null) {
+ sampleData = new byte[INITIAL_SAMPLE_SIZE];
+ } else if (sampleSize == sampleData.length) {
+ sampleData = Arrays.copyOf(sampleData, sampleData.length * 2);
+ }
+ result = dataSource.read(sampleData, sampleSize, sampleData.length - sampleSize);
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
new file mode 100644
index 0000000000..01f35ef775
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
@@ -0,0 +1,371 @@
+/*
+ * 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.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 extends BaseMediaSource {
+
+ /**
+ * Listener of {@link SingleSampleMediaSource} events.
+ *
+ * @deprecated Use {@link MediaSourceEventListener}.
+ */
+ @Deprecated
+ public interface EventListener {
+
+ /**
+ * Called when an error occurs loading media data.
+ *
+ * @param sourceId The id of the reporting {@link SingleSampleMediaSource}.
+ * @param e The cause of the failure.
+ */
+ void onLoadError(int sourceId, IOException e);
+
+ }
+
+ /** Factory for {@link SingleSampleMediaSource}. */
+ public static final class Factory {
+
+ 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.
+ *
+ * <p>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()}.
+ *
+ * <p>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 long durationUs;
+ private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private final boolean treatLoadErrorsAsEndOfStream;
+ private final Timeline timeline;
+ @Nullable private final Object tag;
+
+ @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);
+ }
+
+ /**
+ * @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);
+ }
+
+ /**
+ * @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.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
+ @Nullable
+ public Object getTag() {
+ return tag;
+ }
+
+ @Override
+ protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ transferListener = mediaTransferListener;
+ refreshSourceInfo(timeline);
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ return new SingleSampleMediaPeriod(
+ dataSpec,
+ dataSourceFactory,
+ transferListener,
+ format,
+ durationUs,
+ loadErrorHandlingPolicy,
+ createEventDispatcher(id),
+ treatLoadErrorsAsEndOfStream);
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ ((SingleSampleMediaPeriod) mediaPeriod).release();
+ }
+
+ @Override
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroup.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroup.java
new file mode 100644
index 0000000000..566238dbdb
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroup.java
@@ -0,0 +1,142 @@
+/*
+ * 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.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;
+import java.util.Arrays;
+
+// TODO: Add an allowMultipleStreams boolean to indicate where the one stream per group restriction
+// does not apply.
+/**
+ * Defines a group of tracks exposed by a {@link MediaPeriod}.
+ *
+ * <p>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 implements Parcelable {
+
+ /**
+ * The number of tracks in the group.
+ */
+ public final int length;
+
+ private final Format[] formats;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * @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);
+ this.formats = formats;
+ 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.
+ *
+ * @param index The index of the track.
+ * @return The track's format.
+ */
+ public Format getFormat(int index) {
+ return formats[index];
+ }
+
+ /**
+ * 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]) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = 17;
+ result = 31 * result + Arrays.hashCode(formats);
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ TrackGroup other = (TrackGroup) obj;
+ 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<TrackGroup> CREATOR =
+ new Parcelable.Creator<TrackGroup>() {
+
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroupArray.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroupArray.java
new file mode 100644
index 0000000000..103a45080e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/TrackGroupArray.java
@@ -0,0 +1,141 @@
+/*
+ * 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.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 implements Parcelable {
+
+ /**
+ * The empty array.
+ */
+ public static final TrackGroupArray EMPTY = new TrackGroupArray();
+
+ /**
+ * The number of groups in the array. Greater than or equal to zero.
+ */
+ public final int length;
+
+ private final TrackGroup[] trackGroups;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * @param trackGroups The groups. Must not be null or contain null elements, but may be empty.
+ */
+ public TrackGroupArray(TrackGroup... trackGroups) {
+ this.trackGroups = trackGroups;
+ 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.
+ *
+ * @param index The index of the group.
+ * @return The group.
+ */
+ public TrackGroup get(int index) {
+ return trackGroups[index];
+ }
+
+ /**
+ * Returns the index of a group within the array.
+ *
+ * @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;
+ }
+ }
+ 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) {
+ hashCode = Arrays.hashCode(trackGroups);
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ TrackGroupArray other = (TrackGroupArray) obj;
+ 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<TrackGroupArray> CREATOR =
+ new Parcelable.Creator<TrackGroupArray>() {
+
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java
new file mode 100644
index 0000000000..ccb9d350fc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java
@@ -0,0 +1,40 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+
+/**
+ * Thrown if the input format was not recognized.
+ */
+public class UnrecognizedInputFormatException extends ParserException {
+
+ /**
+ * The {@link Uri} from which the unrecognized data was read.
+ */
+ public final Uri uri;
+
+ /**
+ * @param message The detail message for the exception.
+ * @param uri The {@link Uri} from which the unrecognized data was read.
+ */
+ public UnrecognizedInputFormatException(String message, Uri uri) {
+ super(message);
+ this.uri = uri;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
new file mode 100644
index 0000000000..83b5b1bc40
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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}.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsLoader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsLoader.java
new file mode 100644
index 0000000000..12ffb8ec0d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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}.
+ *
+ * <p>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).
+ *
+ * <p>{@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)}.
+ *
+ * <p>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).
+ *
+ * <p>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.
+ *
+ * <p>This method must be called before the player is prepared with media using this ads loader.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
new file mode 100644
index 0000000000..02c33a3d34
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<MediaPeriodId> {
+
+ /**
+ * 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<MediaSource, List<MaskingMediaPeriod>> 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<MaskingMediaPeriod> 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<MaskingMediaPeriod> 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<MaskingMediaPeriod> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java
new file mode 100644
index 0000000000..44f6d0bc66
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java
new file mode 100644
index 0000000000..406cd1617a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java
@@ -0,0 +1,100 @@
+/*
+ * 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 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;
+
+/**
+ * A base implementation of {@link MediaChunk} that outputs to a {@link BaseMediaChunkOutput}.
+ */
+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;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @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 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,
+ @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;
+ }
+
+ /**
+ * Initializes the chunk for loading, setting the {@link BaseMediaChunkOutput} that will receive
+ * samples as they are loaded.
+ *
+ * @param output The output that will receive the loaded media samples.
+ */
+ public void init(BaseMediaChunkOutput output) {
+ this.output = output;
+ firstSampleIndices = output.getWriteIndices();
+ }
+
+ /**
+ * Returns the index of the first sample in the specified track of the output that will originate
+ * from this chunk.
+ */
+ public final int getFirstSampleIndex(int trackIndex) {
+ return firstSampleIndices[trackIndex];
+ }
+
+ /**
+ * Returns the output most recently passed to {@link #init(BaseMediaChunkOutput)}.
+ */
+ protected final BaseMediaChunkOutput getOutput() {
+ return output;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkIterator.java
new file mode 100644
index 0000000000..3987260578
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java
new file mode 100644
index 0000000000..5d1f93bf01
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java
@@ -0,0 +1,80 @@
+/*
+ * 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.chunk;
+
+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;
+
+/**
+ * A {@link TrackOutputProvider} that provides {@link TrackOutput TrackOutputs} based on a
+ * predefined mapping from track type to output.
+ */
+public final class BaseMediaChunkOutput implements TrackOutputProvider {
+
+ private static final String TAG = "BaseMediaChunkOutput";
+
+ private final int[] trackTypes;
+ private final SampleQueue[] sampleQueues;
+
+ /**
+ * @param trackTypes The track types of the individual track outputs.
+ * @param sampleQueues The individual sample queues.
+ */
+ public BaseMediaChunkOutput(int[] trackTypes, SampleQueue[] sampleQueues) {
+ this.trackTypes = trackTypes;
+ this.sampleQueues = sampleQueues;
+ }
+
+ @Override
+ public TrackOutput track(int id, int type) {
+ for (int i = 0; i < trackTypes.length; i++) {
+ if (type == trackTypes[i]) {
+ return sampleQueues[i];
+ }
+ }
+ Log.e(TAG, "Unmatched track of type: " + type);
+ return new DummyTrackOutput();
+ }
+
+ /**
+ * Returns the current absolute write indices of the individual sample queues.
+ */
+ public int[] getWriteIndices() {
+ 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;
+ }
+
+ /**
+ * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples
+ * subsequently written to the sample queues.
+ */
+ public void setSampleOffsetUs(long sampleOffsetUs) {
+ for (SampleQueue sampleQueue : sampleQueues) {
+ if (sampleQueue != null) {
+ sampleQueue.setSampleOffsetUs(sampleOffsetUs);
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.java
new file mode 100644
index 0000000000..3f4450eddd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.java
@@ -0,0 +1,137 @@
+/*
+ * 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.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
+ * for the playback of streams.
+ */
+public abstract class Chunk implements Loadable {
+
+ /**
+ * The {@link DataSpec} that defines the data to be loaded.
+ */
+ public final DataSpec dataSpec;
+ /**
+ * The type of the chunk. One of the {@code DATA_TYPE_*} constants defined in {@link C}. For
+ * reporting only.
+ */
+ public final int type;
+ /**
+ * The format of the track to which this chunk belongs, or null if the chunk does not belong to
+ * a track.
+ */
+ public final Format trackFormat;
+ /**
+ * One of the {@link C} {@code SELECTION_REASON_*} constants if the chunk belongs to a track.
+ * {@link C#SELECTION_REASON_UNKNOWN} if the chunk does not belong to a track.
+ */
+ public final int trackSelectionReason;
+ /**
+ * Optional data associated with the selection of the track to which this chunk belongs. Null if
+ * the chunk does not belong to a track.
+ */
+ @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.
+ */
+ public final long startTimeUs;
+ /**
+ * The end time of the media contained by the chunk, or {@link C#TIME_UNSET} if the data being
+ * loaded does not contain media samples.
+ */
+ public final long endTimeUs;
+
+ protected final StatsDataSource dataSource;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param type See {@link #type}.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param startTimeUs See {@link #startTimeUs}.
+ * @param endTimeUs See {@link #endTimeUs}.
+ */
+ 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;
+ this.trackSelectionReason = trackSelectionReason;
+ this.trackSelectionData = trackSelectionData;
+ this.startTimeUs = startTimeUs;
+ this.endTimeUs = endTimeUs;
+ }
+
+ /**
+ * Returns the duration of the chunk in microseconds.
+ */
+ public final long getDurationUs() {
+ return endTimeUs - startTimeUs;
+ }
+
+ /**
+ * Returns the number of bytes that have been loaded. Must only be called after the load
+ * completed, failed, or was canceled.
+ */
+ 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<String, List<String>> getResponseHeaders() {
+ return dataSource.getLastResponseHeaders();
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
new file mode 100644
index 0000000000..04cef9198c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
@@ -0,0 +1,220 @@
+/*
+ * 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.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;
+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.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * An {@link Extractor} wrapper for loading chunks that contain a single primary track, and possibly
+ * additional embedded tracks.
+ * <p>
+ * The wrapper allows switching of the {@link TrackOutput}s that receive parsed data.
+ */
+public final class ChunkExtractorWrapper implements ExtractorOutput {
+
+ /**
+ * Provides {@link TrackOutput} instances to be written to by the wrapper.
+ */
+ public interface TrackOutputProvider {
+
+ /**
+ * Called to get the {@link TrackOutput} for a specific track.
+ * <p>
+ * 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.exoplayer2C} {@code TRACK_TYPE_*} constants.
+ * @return The {@link TrackOutput} for the given track identifier.
+ */
+ TrackOutput track(int id, int type);
+
+ }
+
+ public final Extractor extractor;
+
+ private final int primaryTrackType;
+ private final Format primaryTrackManifestFormat;
+ private final SparseArray<BindingTrackOutput> bindingTrackOutputs;
+
+ private boolean extractorInitialized;
+ private TrackOutputProvider trackOutputProvider;
+ private long endTimeUs;
+ private SeekMap seekMap;
+ private Format[] sampleFormats;
+
+ /**
+ * @param extractor The extractor to wrap.
+ * @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, int primaryTrackType,
+ Format primaryTrackManifestFormat) {
+ this.extractor = extractor;
+ this.primaryTrackType = primaryTrackType;
+ this.primaryTrackManifestFormat = primaryTrackManifestFormat;
+ bindingTrackOutputs = new SparseArray<>();
+ }
+
+ /**
+ * Returns the {@link SeekMap} most recently output by the extractor, or null.
+ */
+ public SeekMap getSeekMap() {
+ return seekMap;
+ }
+
+ /**
+ * Returns the sample {@link Format}s most recently output by the extractor, or null.
+ */
+ public Format[] getSampleFormats() {
+ return sampleFormats;
+ }
+
+ /**
+ * 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(
+ @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(/* position= */ 0, startTimeUs == C.TIME_UNSET ? 0 : startTimeUs);
+ for (int i = 0; i < bindingTrackOutputs.size(); i++) {
+ bindingTrackOutputs.valueAt(i).bind(trackOutputProvider, endTimeUs);
+ }
+ }
+ }
+
+ // ExtractorOutput implementation.
+
+ @Override
+ public TrackOutput track(int id, int type) {
+ BindingTrackOutput bindingTrackOutput = bindingTrackOutputs.get(id);
+ if (bindingTrackOutput == null) {
+ // Assert that if we're seeing a new track we have not seen endTracks.
+ Assertions.checkState(sampleFormats == null);
+ // 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;
+ }
+
+ @Override
+ public void endTracks() {
+ Format[] sampleFormats = new Format[bindingTrackOutputs.size()];
+ for (int i = 0; i < bindingTrackOutputs.size(); i++) {
+ sampleFormats[i] = bindingTrackOutputs.valueAt(i).sampleFormat;
+ }
+ this.sampleFormats = sampleFormats;
+ }
+
+ @Override
+ public void seekMap(SeekMap seekMap) {
+ this.seekMap = seekMap;
+ }
+
+ // Internal logic.
+
+ private static final class BindingTrackOutput implements TrackOutput {
+
+ 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, long endTimeUs) {
+ if (trackOutputProvider == null) {
+ trackOutput = dummyTrackOutput;
+ return;
+ }
+ this.endTimeUs = endTimeUs;
+ trackOutput = trackOutputProvider.track(id, type);
+ if (sampleFormat != null) {
+ trackOutput.format(sampleFormat);
+ }
+ }
+
+ @Override
+ public void format(Format format) {
+ sampleFormat = manifestFormat != null ? format.copyWithManifestFormatInfo(manifestFormat)
+ : format;
+ trackOutput.format(sampleFormat);
+ }
+
+ @Override
+ public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ return trackOutput.sampleData(input, length, allowEndOfInput);
+ }
+
+ @Override
+ public void sampleData(ParsableByteArray data, int length) {
+ trackOutput.sampleData(data, length);
+ }
+
+ @Override
+ public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
+ CryptoData cryptoData) {
+ if (endTimeUs != C.TIME_UNSET && timeUs >= endTimeUs) {
+ trackOutput = dummyTrackOutput;
+ }
+ trackOutput.sampleMetadata(timeUs, flags, size, offset, cryptoData);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkHolder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkHolder.java
new file mode 100644
index 0000000000..ef9daddd2c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkHolder.java
@@ -0,0 +1,41 @@
+/*
+ * 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 androidx.annotation.Nullable;
+
+/**
+ * Holds a chunk or an indication that the end of the stream has been reached.
+ */
+public final class ChunkHolder {
+
+ /** The chunk. */
+ @Nullable public Chunk chunk;
+
+ /**
+ * Indicates that the end of the stream has been reached.
+ */
+ public boolean endOfStream;
+
+ /**
+ * Clears the holder.
+ */
+ public void clear() {
+ chunk = null;
+ endOfStream = false;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
new file mode 100644
index 0000000000..a789805cd7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
@@ -0,0 +1,791 @@
+/*
+ * 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 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.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.List;
+
+/**
+ * A {@link SampleStream} that loads media in {@link Chunk}s, obtained from a {@link ChunkSource}.
+ * May also be configured to expose additional embedded {@link SampleStream}s.
+ */
+public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, SequenceableLoader,
+ Loader.Callback<Chunk>, Loader.ReleaseCallback {
+
+ /** A callback to be notified when a sample stream has finished being released. */
+ public interface ReleaseCallback<T extends ChunkSource> {
+
+ /**
+ * Called when the {@link ChunkSampleStream} has finished being released.
+ *
+ * @param chunkSampleStream The released sample stream.
+ */
+ void onSampleStreamReleased(ChunkSampleStream<T> 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<ChunkSampleStream<T>> callback;
+ private final EventDispatcher eventDispatcher;
+ private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private final Loader loader;
+ private final ChunkHolder nextChunkHolder;
+ private final ArrayList<BaseMediaChunk> mediaChunks;
+ private final List<BaseMediaChunk> readOnlyMediaChunks;
+ private final SampleQueue primarySampleQueue;
+ private final SampleQueue[] embeddedSampleQueues;
+ private final BaseMediaChunkOutput chunkOutput;
+
+ private Format primaryDownstreamTrackFormat;
+ @Nullable private ReleaseCallback<T> releaseCallback;
+ private long pendingResetPositionUs;
+ private long lastSeekPositionUs;
+ private int nextNotifyPrimaryFormatMediaChunkIndex;
+
+ /* package */ long decodeOnlyUntilPositionUs;
+ /* package */ boolean loadingFinished;
+
+ /**
+ * 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 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,
+ @Nullable int[] embeddedTrackTypes,
+ @Nullable Format[] embeddedTrackFormats,
+ T chunkSource,
+ Callback<ChunkSampleStream<T>> 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.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ loader = new Loader("Loader:ChunkSampleStream");
+ nextChunkHolder = new ChunkHolder();
+ mediaChunks = new ArrayList<>();
+ readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
+
+ int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length;
+ embeddedSampleQueues = new SampleQueue[embeddedTrackCount];
+ embeddedTracksSelected = new boolean[embeddedTrackCount];
+ int[] trackTypes = new int[1 + embeddedTrackCount];
+ SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount];
+
+ primarySampleQueue = new SampleQueue(allocator, drmSessionManager);
+ trackTypes[0] = primaryTrackType;
+ sampleQueues[0] = primarySampleQueue;
+
+ for (int i = 0; i < embeddedTrackCount; i++) {
+ SampleQueue sampleQueue =
+ new SampleQueue(allocator, DrmSessionManager.getDummyDrmSessionManager());
+ embeddedSampleQueues[i] = sampleQueue;
+ sampleQueues[i + 1] = sampleQueue;
+ trackTypes[i + 1] = embeddedTrackTypes[i];
+ }
+
+ chunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues);
+ pendingResetPositionUs = positionUs;
+ lastSeekPositionUs = positionUs;
+ }
+
+ /**
+ * 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 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);
+ }
+
+ /**
+ * Selects the embedded track, returning a new {@link EmbeddedSampleStream} from which the track's
+ * samples can be consumed. {@link EmbeddedSampleStream#release()} must be called on the returned
+ * stream when the track is no longer required, and before calling this method again to obtain
+ * another stream for the same track.
+ *
+ * @param positionUs The current playback position in microseconds.
+ * @param trackType The type of the embedded track to enable.
+ * @return The {@link EmbeddedSampleStream} for the embedded track.
+ */
+ public EmbeddedSampleStream selectEmbeddedTrack(long positionUs, int trackType) {
+ for (int i = 0; i < embeddedSampleQueues.length; i++) {
+ if (embeddedTrackTypes[i] == trackType) {
+ Assertions.checkState(!embeddedTracksSelected[i]);
+ embeddedTracksSelected[i] = true;
+ embeddedSampleQueues[i].seekTo(positionUs, /* allowTimeBeyondBuffer= */ true);
+ return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i);
+ }
+ }
+ // Should never happen.
+ throw new IllegalStateException();
+ }
+
+ /**
+ * Returns the {@link ChunkSource} used by this stream.
+ */
+ public T getChunkSource() {
+ return chunkSource;
+ }
+
+ /**
+ * 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 track is fully buffered.
+ */
+ @Override
+ public long getBufferedPositionUs() {
+ if (loadingFinished) {
+ return C.TIME_END_OF_SOURCE;
+ } else if (isPendingReset()) {
+ return pendingResetPositionUs;
+ } else {
+ long bufferedPositionUs = lastSeekPositionUs;
+ BaseMediaChunk lastMediaChunk = getLastMediaChunk();
+ BaseMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk
+ : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null;
+ if (lastCompletedMediaChunk != null) {
+ bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs);
+ }
+ return Math.max(bufferedPositionUs, primarySampleQueue.getLargestQueuedTimestampUs());
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @param positionUs The seek position in microseconds.
+ */
+ public void seekToUs(long positionUs) {
+ lastSeekPositionUs = positionUs;
+ 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;
+ }
+ }
+
+ // 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 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 {
+ loader.clearFatalError();
+ primarySampleQueue.reset();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.reset();
+ }
+ }
+ }
+ }
+
+ /**
+ * Releases the stream.
+ *
+ * <p>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() {
+ release(null);
+ }
+
+ /**
+ * Releases the stream.
+ *
+ * <p>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<T> 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);
+ }
+ }
+
+ // SampleStream implementation.
+
+ @Override
+ public boolean isReady() {
+ return !isPendingReset() && primarySampleQueue.isReady(loadingFinished);
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ loader.maybeThrowError();
+ primarySampleQueue.maybeThrowError();
+ if (!loader.isLoading()) {
+ chunkSource.maybeThrowError();
+ }
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired) {
+ if (isPendingReset()) {
+ return C.RESULT_NOTHING_READ;
+ }
+ maybeNotifyPrimaryTrackFormatChanged();
+
+ return primarySampleQueue.read(
+ formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs);
+ }
+
+ @Override
+ 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.
+
+ @Override
+ public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) {
+ chunkSource.onChunkLoadCompleted(loadable);
+ 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);
+ }
+
+ @Override
+ public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs,
+ boolean released) {
+ 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();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.reset();
+ }
+ callback.onContinueLoadingRequested(this);
+ }
+ }
+
+ @Override
+ public LoadErrorAction onLoadError(
+ Chunk loadable,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ IOException error,
+ int errorCount) {
+ long bytesLoaded = loadable.bytesLoaded();
+ boolean isMediaChunk = isMediaChunk(loadable);
+ 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.");
+ }
+ }
+
+ 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 loadErrorAction;
+ }
+
+ // SequenceableLoader implementation
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ if (loadingFinished || loader.isLoading() || loader.hasFatalError()) {
+ return false;
+ }
+
+ boolean pendingReset = isPendingReset();
+ List<BaseMediaChunk> 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;
+ }
+
+ if (loadable == null) {
+ return false;
+ }
+
+ if (isMediaChunk(loadable)) {
+ BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable;
+ 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, 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 : 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
+
+ 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 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;
+ }
+ }
+
+ 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,
+ currentChunk.trackSelectionReason, currentChunk.trackSelectionData,
+ currentChunk.startTimeUs);
+ }
+ primaryDownstreamTrackFormat = trackFormat;
+ }
+
+ /**
+ * Returns the media chunk index corresponding to a given primary sample index.
+ *
+ * @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 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;
+ }
+ }
+ 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(firstRemovedChunk.getFirstSampleIndex(i + 1));
+ }
+ return firstRemovedChunk;
+ }
+
+ /**
+ * A {@link SampleStream} embedded in a {@link ChunkSampleStream}.
+ */
+ public final class EmbeddedSampleStream implements SampleStream {
+
+ public final ChunkSampleStream<T> parent;
+
+ private final SampleQueue sampleQueue;
+ private final int index;
+
+ private boolean notifiedDownstreamFormat;
+
+ public EmbeddedSampleStream(ChunkSampleStream<T> parent, SampleQueue sampleQueue, int index) {
+ this.parent = parent;
+ this.sampleQueue = sampleQueue;
+ this.index = index;
+ }
+
+ @Override
+ public boolean isReady() {
+ return !isPendingReset() && sampleQueue.isReady(loadingFinished);
+ }
+
+ @Override
+ 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
+ public void maybeThrowError() throws IOException {
+ // Do nothing. Errors will be thrown from the primary stream.
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired) {
+ if (isPendingReset()) {
+ return C.RESULT_NOTHING_READ;
+ }
+ maybeNotifyDownstreamFormat();
+ return sampleQueue.read(
+ formatHolder,
+ buffer,
+ formatRequired,
+ loadingFinished,
+ decodeOnlyUntilPositionUs);
+ }
+
+ public void release() {
+ Assertions.checkState(embeddedTracksSelected[index]);
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.java
new file mode 100644
index 0000000000..33cee8e20e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.java
@@ -0,0 +1,111 @@
+/*
+ * 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 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;
+
+/**
+ * A provider of {@link Chunk}s for a {@link ChunkSampleStream} to load.
+ */
+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.
+ * <p>
+ * This method should only be called after the source has been prepared.
+ *
+ * @throws IOException The underlying error.
+ */
+ void maybeThrowError() throws IOException;
+
+ /**
+ * Evaluates whether {@link MediaChunk}s should be removed from the back of the queue.
+ * <p>
+ * Removing {@link MediaChunk}s from the back of the queue can be useful if they could be replaced
+ * with chunks of a significantly higher quality (e.g. because the available bandwidth has
+ * substantially increased).
+ *
+ * @param playbackPositionUs The current playback position.
+ * @param queue The queue of buffered {@link MediaChunk}s.
+ * @return The preferred queue size.
+ */
+ int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue);
+
+ /**
+ * Returns the next chunk to load.
+ *
+ * <p>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 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(
+ long playbackPositionUs,
+ long loadPositionUs,
+ List<? extends MediaChunk> queue,
+ ChunkHolder out);
+
+ /**
+ * Called when the {@link ChunkSampleStream} has finished loading a chunk obtained from this
+ * source.
+ *
+ * <p>This method should only be called when the source is enabled.
+ *
+ * @param chunk The chunk whose load has been completed.
+ */
+ void onChunkLoadCompleted(Chunk chunk);
+
+ /**
+ * Called when the {@link ChunkSampleStream} encounters an error loading a chunk obtained from
+ * this source.
+ *
+ * <p>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.
+ * @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, long blacklistDurationMs);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
new file mode 100644
index 0000000000..98865e8b0e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
@@ -0,0 +1,157 @@
+/*
+ * 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 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;
+
+/**
+ * A {@link BaseMediaChunk} that uses an {@link Extractor} to decode sample data.
+ */
+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 long nextLoadPosition;
+ private volatile boolean loadCanceled;
+ private boolean loadCompleted;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @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 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,
+ 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 long getNextChunkIndex() {
+ return chunkIndex + chunkCount;
+ }
+
+ @Override
+ public boolean isLoadCompleted() {
+ return loadCompleted;
+ }
+
+ // Loadable implementation.
+
+ @Override
+ public final void cancelLoad() {
+ loadCanceled = true;
+ }
+
+ @SuppressWarnings("NonAtomicVolatileUpdate")
+ @Override
+ public final void load() throws IOException, InterruptedException {
+ 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.
+ 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, DUMMY_POSITION_HOLDER);
+ }
+ Assertions.checkState(result != Extractor.RESULT_SEEK);
+ } finally {
+ nextLoadPosition = input.getPosition() - dataSpec.absoluteStreamPosition;
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java
new file mode 100644
index 0000000000..583f8ceeee
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java
@@ -0,0 +1,119 @@
+/*
+ * 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 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.util.Util;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * A base class for {@link Chunk} implementations where the data should be loaded into a
+ * {@code byte[]} before being consumed.
+ */
+public abstract class DataChunk extends Chunk {
+
+ private static final int READ_GRANULARITY = 16 * 1024;
+
+ private byte[] data;
+
+ private volatile boolean loadCanceled;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param type See {@link #type}.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @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,
+ @Nullable Object trackSelectionData,
+ byte[] data) {
+ super(dataSource, dataSpec, type, trackFormat, trackSelectionReason, trackSelectionData,
+ C.TIME_UNSET, C.TIME_UNSET);
+ this.data = data;
+ }
+
+ /**
+ * Returns the array in which the data is held.
+ * <p>
+ * This method should be used for recycling the holder only, and not for reading the data.
+ *
+ * @return The array in which the data is held.
+ */
+ public byte[] getDataHolder() {
+ return data;
+ }
+
+ // Loadable implementation
+
+ @Override
+ public final void cancelLoad() {
+ loadCanceled = true;
+ }
+
+ @Override
+ public final void load() throws IOException, InterruptedException {
+ try {
+ dataSource.open(dataSpec);
+ int limit = 0;
+ int bytesRead = 0;
+ while (bytesRead != C.RESULT_END_OF_INPUT && !loadCanceled) {
+ maybeExpandData(limit);
+ bytesRead = dataSource.read(data, limit, READ_GRANULARITY);
+ if (bytesRead != -1) {
+ limit += bytesRead;
+ }
+ }
+ if (!loadCanceled) {
+ consume(data, limit);
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ }
+
+ /**
+ * Called by {@link #load()}. Implementations should override this method to consume the loaded
+ * data.
+ *
+ * @param data An array containing the data.
+ * @param limit The limit of the data.
+ * @throws IOException If an error occurs consuming the loaded data.
+ */
+ protected abstract void consume(byte[] data, int limit) throws IOException;
+
+ private void maybeExpandData(int limit) {
+ if (data == null) {
+ data = new byte[READ_GRANULARITY];
+ } else if (data.length < limit + READ_GRANULARITY) {
+ // The new length is calculated as (data.length + READ_GRANULARITY) rather than
+ // (limit + READ_GRANULARITY) in order to avoid small increments in the length.
+ data = Arrays.copyOf(data, data.length + READ_GRANULARITY);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
new file mode 100644
index 0000000000..db6e82c2c7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
@@ -0,0 +1,112 @@
+/*
+ * 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 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;
+
+ @MonotonicNonNull private TrackOutputProvider trackOutputProvider;
+ private long nextLoadPosition;
+ private volatile boolean loadCanceled;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @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,
+ @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;
+ }
+
+ /**
+ * 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.
+
+ @Override
+ public void cancelLoad() {
+ loadCanceled = true;
+ }
+
+ @SuppressWarnings("NonAtomicVolatileUpdate")
+ @Override
+ public void load() throws IOException, InterruptedException {
+ if (nextLoadPosition == 0) {
+ extractorWrapper.init(
+ trackOutputProvider, /* startTimeUs= */ C.TIME_UNSET, /* endTimeUs= */ C.TIME_UNSET);
+ }
+ try {
+ // Create and open the input.
+ 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, DUMMY_POSITION_HOLDER);
+ }
+ Assertions.checkState(result != Extractor.RESULT_SEEK);
+ } finally {
+ nextLoadPosition = input.getPosition() - dataSpec.absoluteStreamPosition;
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.java
new file mode 100644
index 0000000000..81c9d216b9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.java
@@ -0,0 +1,68 @@
+/*
+ * 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 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.util.Assertions;
+
+/**
+ * An abstract base class for {@link Chunk}s that contain media samples.
+ */
+public abstract class MediaChunk extends Chunk {
+
+ /** 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.
+ * @param dataSpec Defines the data to be loaded.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @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, or {@link C#INDEX_UNSET} if it is not known.
+ */
+ 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 or {@link C#INDEX_UNSET} if it is not known. */
+ public long getNextChunkIndex() {
+ return chunkIndex != C.INDEX_UNSET ? chunkIndex + 1 : C.INDEX_UNSET;
+ }
+
+ /**
+ * Returns whether the chunk has been fully loaded.
+ */
+ public abstract boolean isLoadCompleted();
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkIterator.java
new file mode 100644
index 0000000000..c6f5b1d41e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunkListIterator.java
new file mode 100644
index 0000000000..1b3004418e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<? extends MediaChunk> 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<? extends MediaChunk> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
new file mode 100644
index 0000000000..b3d30408ee
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
@@ -0,0 +1,120 @@
+/*
+ * 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 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.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+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.Util;
+import java.io.IOException;
+
+/**
+ * A {@link BaseMediaChunk} for chunks consisting of a single raw sample.
+ */
+public final class SingleSampleMediaChunk extends BaseMediaChunk {
+
+ private final int trackType;
+ private final Format sampleFormat;
+
+ private long nextLoadPosition;
+ private boolean loadCompleted;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @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, 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,
+ 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;
+ }
+
+
+ @Override
+ public boolean isLoadCompleted() {
+ return loadCompleted;
+ }
+
+ // Loadable implementation.
+
+ @Override
+ public void cancelLoad() {
+ // Do nothing.
+ }
+
+ @SuppressWarnings("NonAtomicVolatileUpdate")
+ @Override
+ public void load() throws IOException, InterruptedException {
+ 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 += nextLoadPosition;
+ }
+ ExtractorInput extractorInput =
+ new DefaultExtractorInput(dataSource, nextLoadPosition, length);
+ // Load the sample data.
+ int result = 0;
+ while (result != C.RESULT_END_OF_INPUT) {
+ nextLoadPosition += result;
+ result = trackOutput.sampleData(extractorInput, Integer.MAX_VALUE, true);
+ }
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/Aes128DataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/Aes128DataSource.java
new file mode 100644
index 0000000000..4643c0402c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/Aes128DataSource.java
@@ -0,0 +1,129 @@
+/*
+ * 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;
+
+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;
+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;
+import javax.crypto.spec.IvParameterSpec;
+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.
+ *
+ * <p>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 */ class Aes128DataSource implements DataSource {
+
+ private final DataSource upstream;
+ private final byte[] encryptionKey;
+ private final byte[] encryptionIv;
+
+ @Nullable private CipherInputStream cipherInputStream;
+
+ /**
+ * @param upstream The upstream {@link DataSource}.
+ * @param encryptionKey The encryption key.
+ * @param encryptionIv The encryption initialization vector.
+ */
+ public Aes128DataSource(DataSource upstream, byte[] encryptionKey, byte[] encryptionIv) {
+ this.upstream = upstream;
+ this.encryptionKey = encryptionKey;
+ this.encryptionIv = encryptionIv;
+ }
+
+ @Override
+ public final void addTransferListener(TransferListener transferListener) {
+ upstream.addTransferListener(transferListener);
+ }
+
+ @Override
+ public final long open(DataSpec dataSpec) throws IOException {
+ Cipher cipher;
+ try {
+ cipher = getCipherInstance();
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ throw new RuntimeException(e);
+ }
+
+ Key cipherKey = new SecretKeySpec(encryptionKey, "AES");
+ AlgorithmParameterSpec cipherIV = new IvParameterSpec(encryptionIv);
+
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIV);
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ throw new RuntimeException(e);
+ }
+
+ DataSourceInputStream inputStream = new DataSourceInputStream(upstream, dataSpec);
+ cipherInputStream = new CipherInputStream(inputStream, cipher);
+ inputStream.open();
+
+ return C.LENGTH_UNSET;
+ }
+
+ @Override
+ 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;
+ }
+ return bytesRead;
+ }
+
+ @Override
+ @Nullable
+ public final Uri getUri() {
+ return upstream.getUri();
+ }
+
+ @Override
+ public final Map<String, List<String>> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java
new file mode 100644
index 0000000000..cbe2f797b7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+
+/**
+ * Default implementation of {@link HlsDataSourceFactory}.
+ */
+public final class DefaultHlsDataSourceFactory implements HlsDataSourceFactory {
+
+ private final DataSource.Factory dataSourceFactory;
+
+ /**
+ * @param dataSourceFactory The {@link DataSource.Factory} to use for all data types.
+ */
+ public DefaultHlsDataSourceFactory(DataSource.Factory dataSourceFactory) {
+ this.dataSourceFactory = dataSourceFactory;
+ }
+
+ @Override
+ public DataSource createDataSource(int dataType) {
+ return dataSourceFactory.createDataSource();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java
new file mode 100644
index 0000000000..6f39e1bff8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<Format> muxedCaptionFormats,
+ TimestampAdjuster timestampAdjuster,
+ Map<String, List<String>> 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<Format> 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<Format> 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<Format> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/FullSegmentEncryptionKeyCache.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/FullSegmentEncryptionKeyCache.java
new file mode 100644
index 0000000000..eab538582d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<Uri, byte[]> backingMap;
+
+ public FullSegmentEncryptionKeyCache(int maxSize) {
+ backingMap =
+ new LinkedHashMap<Uri, byte[]>(
+ /* initialCapacity= */ maxSize + 1, /* loadFactor= */ 1, /* accessOrder= */ false) {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<Uri, byte[]> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
new file mode 100644
index 0000000000..da935389d8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
@@ -0,0 +1,668 @@
+/*
+ * 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;
+
+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.DataChunk;
+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;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.BaseTrackSelection;
+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.util.Arrays;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/** Source of Hls (possibly adaptive) chunks. */
+/* package */ class HlsChunkSource {
+
+ /**
+ * Chunk holder that allows the scheduling of retries.
+ */
+ public static final class HlsChunkHolder {
+
+ public HlsChunkHolder() {
+ clear();
+ }
+
+ /** 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. */
+ @Nullable public Uri playlistUrl;
+
+ /**
+ * Clears the holder.
+ */
+ public void clear() {
+ chunk = null;
+ endOfStream = false;
+ 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 Uri[] playlistUrls;
+ private final Format[] playlistFormats;
+ private final HlsPlaylistTracker playlistTracker;
+ private final TrackGroup trackGroup;
+ @Nullable private final List<Format> muxedCaptionFormats;
+ private final FullSegmentEncryptionKeyCache keyCache;
+
+ private boolean isTimestampMaster;
+ private byte[] scratchSpace;
+ @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 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 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(
+ HlsExtractorFactory extractorFactory,
+ HlsPlaylistTracker playlistTracker,
+ Uri[] playlistUrls,
+ Format[] playlistFormats,
+ HlsDataSourceFactory dataSourceFactory,
+ @Nullable TransferListener mediaTransferListener,
+ TimestampAdjusterProvider timestampAdjusterProvider,
+ @Nullable List<Format> muxedCaptionFormats) {
+ this.extractorFactory = extractorFactory;
+ this.playlistTracker = playlistTracker;
+ this.playlistUrls = playlistUrls;
+ this.playlistFormats = playlistFormats;
+ this.timestampAdjusterProvider = timestampAdjusterProvider;
+ this.muxedCaptionFormats = muxedCaptionFormats;
+ 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;
+ }
+ trackSelection = new InitializationTrackSelection(trackGroup, initialTrackSelection);
+ }
+
+ /**
+ * If the source is currently having difficulty providing chunks, then this method throws the
+ * underlying error. Otherwise does nothing.
+ *
+ * @throws IOException The underlying error.
+ */
+ public void maybeThrowError() throws IOException {
+ if (fatalError != null) {
+ throw fatalError;
+ }
+ if (expectedPlaylistUrl != null && seenExpectedPlaylistError) {
+ playlistTracker.maybeThrowPlaylistRefreshError(expectedPlaylistUrl);
+ }
+ }
+
+ /**
+ * Returns the track group exposed by the source.
+ */
+ public TrackGroup getTrackGroup() {
+ return trackGroup;
+ }
+
+ /**
+ * Sets the current track selection.
+ *
+ * @param trackSelection The {@link TrackSelection}.
+ */
+ public void setTrackSelection(TrackSelection trackSelection) {
+ this.trackSelection = trackSelection;
+ }
+
+ /** Returns the current {@link TrackSelection}. */
+ public TrackSelection getTrackSelection() {
+ return trackSelection;
+ }
+
+ /**
+ * Resets the source.
+ */
+ public void reset() {
+ fatalError = null;
+ }
+
+ /**
+ * Sets whether this chunk source is responsible for initializing timestamp adjusters.
+ *
+ * @param isTimestampMaster True if this chunk source is responsible for initializing timestamp
+ * adjusters.
+ */
+ public void setIsTimestampMaster(boolean isTimestampMaster) {
+ this.isTimestampMaster = isTimestampMaster;
+ }
+
+ /**
+ * Returns the next chunk to load.
+ *
+ * <p>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(
+ long playbackPositionUs,
+ long loadPositionUs,
+ List<HlsMediaChunk> 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 track.
+ MediaChunkIterator[] mediaChunkIterators = createMediaChunkIterators(previous, loadPositionUs);
+ trackSelection.updateSelectedTrack(
+ playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, mediaChunkIterators);
+ int selectedTrackIndex = trackSelection.getSelectedIndexInTrackGroup();
+
+ 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(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.
+ 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 segmentIndexInPlaylist = (int) (chunkMediaSequence - mediaPlaylist.mediaSequence);
+ int availableSegmentCount = mediaPlaylist.segments.size();
+ if (segmentIndexInPlaylist >= availableSegmentCount) {
+ if (mediaPlaylist.hasEndTag) {
+ if (allowEndOfStream || availableSegmentCount == 0) {
+ out.endOfStream = true;
+ return;
+ }
+ segmentIndexInPlaylist = availableSegmentCount - 1;
+ } else /* Live */ {
+ 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;
+ }
+
+ 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));
+ }
+
+ /**
+ * Called when the {@link HlsSampleStreamWrapper} has finished loading a chunk obtained from this
+ * source.
+ *
+ * @param chunk The chunk whose load has been completed.
+ */
+ public void onChunkLoadCompleted(Chunk chunk) {
+ if (chunk instanceof EncryptionKeyChunk) {
+ EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk;
+ scratchSpace = encryptionKeyChunk.getDataHolder();
+ keyCache.put(
+ encryptionKeyChunk.dataSpec.uri, Assertions.checkNotNull(encryptionKeyChunk.getResult()));
+ }
+ }
+
+ /**
+ * 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 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 maybeBlacklistTrack(Chunk chunk, long blacklistDurationMs) {
+ return trackSelection.blacklist(
+ trackSelection.indexOf(trackGroup.indexOf(chunk.trackFormat)), blacklistDurationMs);
+ }
+
+ /**
+ * Called when a playlist load encounters an error.
+ *
+ * @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 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.
+
+ /**
+ * 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 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[] 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);
+ }
+
+ @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.
+
+ /**
+ * A {@link TrackSelection} to use for initialization.
+ */
+ private static final class InitializationTrackSelection extends BaseTrackSelection {
+
+ private int selectedIndex;
+
+ public InitializationTrackSelection(TrackGroup group, int[] tracks) {
+ super(group, tracks);
+ selectedIndex = indexOf(group.getFormat(0));
+ }
+
+ @Override
+ public void updateSelectedTrack(
+ long playbackPositionUs,
+ long bufferedDurationUs,
+ long availableDurationUs,
+ List<? extends MediaChunk> queue,
+ MediaChunkIterator[] mediaChunkIterators) {
+ long nowMs = SystemClock.elapsedRealtime();
+ if (!isBlacklisted(selectedIndex, nowMs)) {
+ return;
+ }
+ // Try from lowest bitrate to highest.
+ for (int i = length - 1; i >= 0; i--) {
+ if (!isBlacklisted(i, nowMs)) {
+ selectedIndex = i;
+ return;
+ }
+ }
+ // Should never happen.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public int getSelectedIndex() {
+ return selectedIndex;
+ }
+
+ @Override
+ public int getSelectionReason() {
+ return C.SELECTION_REASON_UNKNOWN;
+ }
+
+ @Override
+ @Nullable
+ public Object getSelectionData() {
+ return null;
+ }
+
+ }
+
+ private static final class EncryptionKeyChunk extends DataChunk {
+
+ private byte @MonotonicNonNull [] result;
+
+ 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);
+ }
+
+ @Override
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java
new file mode 100644
index 0000000000..66fac54b8d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java
@@ -0,0 +1,35 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+
+/**
+ * Creates {@link DataSource}s for HLS playlists, encryption and media chunks.
+ */
+public interface HlsDataSourceFactory {
+
+ /**
+ * Creates a {@link DataSource} for the given data type.
+ *
+ * @param dataType The data type for which the {@link DataSource} will be used. One of {@link C}
+ * {@code .DATA_TYPE_*} constants.
+ * @return A {@link DataSource} for the given data type.
+ */
+ DataSource createDataSource(int dataType);
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java
new file mode 100644
index 0000000000..8f445f97ed
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<Format> muxedCaptionFormats,
+ TimestampAdjuster timestampAdjuster,
+ Map<String, List<String>> responseHeaders,
+ ExtractorInput sniffingExtractorInput)
+ throws InterruptedException, IOException;
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsManifest.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsManifest.java
new file mode 100644
index 0000000000..52a5632134
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsManifest.java
@@ -0,0 +1,44 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
+
+/**
+ * Holds a master playlist along with a snapshot of one of its media playlists.
+ */
+public final class HlsManifest {
+
+ /**
+ * The master playlist of an HLS stream.
+ */
+ public final HlsMasterPlaylist masterPlaylist;
+ /**
+ * A snapshot of a media playlist referred to by {@link #masterPlaylist}.
+ */
+ public final HlsMediaPlaylist mediaPlaylist;
+
+ /**
+ * @param masterPlaylist The master playlist.
+ * @param mediaPlaylist The media playlist.
+ */
+ HlsManifest(HlsMasterPlaylist masterPlaylist, HlsMediaPlaylist mediaPlaylist) {
+ this.masterPlaylist = masterPlaylist;
+ this.mediaPlaylist = mediaPlaylist;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
new file mode 100644
index 0000000000..173e53faad
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
@@ -0,0 +1,519 @@
+/*
+ * 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;
+
+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.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.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.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 {
+
+ /**
+ * 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<Format> 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);
+
+ // 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 AtomicInteger uidSource = new AtomicInteger();
+
+ /**
+ * A unique identifier for the chunk.
+ */
+ public final int uid;
+
+ /**
+ * The discontinuity sequence number of the chunk.
+ */
+ public final int discontinuitySequenceNumber;
+
+ /** 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 boolean isMasterTimestampSource;
+ private final boolean hasGapTag;
+ private final TimestampAdjuster timestampAdjuster;
+ private final boolean shouldSpliceIn;
+ private final HlsExtractorFactory extractorFactory;
+ @Nullable private final List<Format> muxedCaptionFormats;
+ @Nullable private final DrmInitData drmInitData;
+ private final Id3Decoder id3Decoder;
+ private final ParsableByteArray scratchId3Data;
+ private final boolean mediaSegmentEncrypted;
+ private final boolean initSegmentEncrypted;
+
+ @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 boolean loadCompleted;
+
+ private HlsMediaChunk(
+ HlsExtractorFactory extractorFactory,
+ DataSource mediaDataSource,
+ DataSpec dataSpec,
+ Format format,
+ boolean mediaSegmentEncrypted,
+ @Nullable DataSource initDataSource,
+ @Nullable DataSpec initDataSpec,
+ boolean initSegmentEncrypted,
+ Uri playlistUrl,
+ @Nullable List<Format> 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.initDataSource = initDataSource;
+ this.initDataLoadRequired = initDataSpec != null;
+ this.initSegmentEncrypted = initSegmentEncrypted;
+ this.playlistUrl = playlistUrl;
+ this.isMasterTimestampSource = isMasterTimestampSource;
+ this.timestampAdjuster = timestampAdjuster;
+ 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();
+ }
+
+ /**
+ * Initializes the chunk for loading, setting the {@link HlsSampleStreamWrapper} that will receive
+ * samples as they are loaded.
+ *
+ * @param output The output that will receive the loaded samples.
+ */
+ public void init(HlsSampleStreamWrapper output) {
+ this.output = output;
+ output.init(uid, shouldSpliceIn);
+ }
+
+ @Override
+ public boolean isLoadCompleted() {
+ return loadCompleted;
+ }
+
+ // Loadable implementation
+
+ @Override
+ public void cancelLoad() {
+ loadCanceled = true;
+ }
+
+ @Override
+ public void load() throws IOException, InterruptedException {
+ // 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) {
+ if (!hasGapTag) {
+ loadMedia();
+ }
+ loadCompleted = true;
+ }
+ }
+
+ // Internal methods.
+
+ @RequiresNonNull("output")
+ private void maybeLoadInitData() throws IOException, InterruptedException {
+ if (!initDataLoadRequired) {
+ return;
+ }
+ // 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 (!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 = prepareExtraction(dataSource, loadDataSpec);
+ if (skipLoadedBytes) {
+ input.skipFully(nextLoadPosition);
+ }
+ try {
+ int result = Extractor.RESULT_CONTINUE;
+ while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+ result = extractor.read(input, DUMMY_POSITION_HOLDER);
+ }
+ } finally {
+ nextLoadPosition = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ }
+
+ @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;
+ }
+
+ /**
+ * Peek the presentation timestamp of the first sample in the chunk from an ID3 PRIV as defined
+ * in the HLS spec, version 20, Section 3.4. Returns {@link C#TIME_UNSET} if the frame is not
+ * found. This method only modifies the peek position.
+ *
+ * @param input The {@link ExtractorInput} to obtain the PRIV frame from.
+ * @return The parsed, adjusted timestamp in microseconds
+ * @throws IOException If an error occurred peeking from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, InterruptedException {
+ input.resetPeekPosition();
+ 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;
+ }
+ scratchId3Data.reset(Id3Decoder.ID3_HEADER_LENGTH);
+ int id = scratchId3Data.readUnsignedInt24();
+ if (id != Id3Decoder.ID3_TAG) {
+ return C.TIME_UNSET;
+ }
+ scratchId3Data.skipBytes(3); // version(2), flags(1).
+ int id3Size = scratchId3Data.readSynchSafeInt();
+ int requiredCapacity = id3Size + 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);
+ }
+ input.peekFully(scratchId3Data.data, Id3Decoder.ID3_HEADER_LENGTH, id3Size);
+ Metadata metadata = id3Decoder.decode(scratchId3Data.data, id3Size);
+ if (metadata == null) {
+ return C.TIME_UNSET;
+ }
+ int metadataLength = metadata.length();
+ for (int i = 0; i < metadataLength; i++) {
+ Metadata.Entry frame = metadata.get(i);
+ if (frame instanceof PrivFrame) {
+ PrivFrame privFrame = (PrivFrame) frame;
+ if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) {
+ 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 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 segment is fully encrypted, returns an {@link Aes128DataSource} that wraps the original
+ * in order to decrypt the loaded data. Else returns the original.
+ *
+ * <p>{@code fullSegmentEncryptionKey} & {@code encryptionIv} can either both be null, or neither.
+ */
+ 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 dataSource;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
new file mode 100644
index 0000000000..60aa5298c3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
@@ -0,0 +1,858 @@
+/*
+ * 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;
+
+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.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.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.
+ */
+public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper.Callback,
+ HlsPlaylistTracker.PlaylistEventListener {
+
+ private final HlsExtractorFactory extractorFactory;
+ private final HlsPlaylistTracker playlistTracker;
+ private final HlsDataSourceFactory dataSourceFactory;
+ @Nullable private final TransferListener mediaTransferListener;
+ private final DrmSessionManager<?> drmSessionManager;
+ private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private final EventDispatcher eventDispatcher;
+ private final Allocator allocator;
+ private final IdentityHashMap<SampleStream, Integer> streamWrapperIndices;
+ private final TimestampAdjusterProvider timestampAdjusterProvider;
+ private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
+ private final boolean allowChunklessPreparation;
+ private final @HlsMediaSource.MetadataType int metadataType;
+ private final boolean useSessionKeys;
+
+ @Nullable private Callback callback;
+ private int pendingPrepareCount;
+ private @MonotonicNonNull TrackGroupArray trackGroups;
+ private HlsSampleStreamWrapper[] sampleStreamWrappers;
+ private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
+ // Maps sample stream wrappers to variant/rendition index by matching array positions.
+ private int[][] manifestUrlIndicesPerWrapper;
+ private SequenceableLoader compositeSequenceableLoader;
+ private boolean notifiedReadingStarted;
+
+ /**
+ * 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.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();
+ sampleStreamWrappers = new HlsSampleStreamWrapper[0];
+ enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0];
+ manifestUrlIndicesPerWrapper = new int[0][];
+ eventDispatcher.mediaPeriodCreated();
+ }
+
+ public void release() {
+ playlistTracker.removeListener(this);
+ for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
+ sampleStreamWrapper.release();
+ }
+ callback = null;
+ eventDispatcher.mediaPeriodReleased();
+ }
+
+ @Override
+ public void prepare(Callback callback, long positionUs) {
+ this.callback = callback;
+ playlistTracker.addListener(this);
+ buildAndPrepareSampleStreamWrappers(positionUs);
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
+ sampleStreamWrapper.maybeThrowPrepareError();
+ }
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ // 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<StreamKey> getStreamKeys(List<TrackSelection> 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<StreamKey> 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(
+ @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];
+ for (int i = 0; i < selections.length; i++) {
+ streamChildIndices[i] = streams[i] == null ? C.INDEX_UNSET
+ : streamWrapperIndices.get(streams[i]);
+ selectionChildIndices[i] = C.INDEX_UNSET;
+ if (selections[i] != null) {
+ TrackGroup trackGroup = selections[i].getTrackGroup();
+ for (int j = 0; j < sampleStreamWrappers.length; j++) {
+ if (sampleStreamWrappers[j].getTrackGroups().indexOf(trackGroup) != C.INDEX_UNSET) {
+ selectionChildIndices[i] = j;
+ break;
+ }
+ }
+ }
+ }
+
+ 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];
+ @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;
+ }
+ 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.checkNotNull(childStream);
+ newStreams[j] = childStream;
+ wrapperEnabled = true;
+ streamWrapperIndices.put(childStream, i);
+ } else if (streamChildIndices[j] == i) {
+ // Assert that the child cleared any previous stream.
+ Assertions.checkState(childStream == null);
+ }
+ }
+ if (wrapperEnabled) {
+ 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 =
+ Util.nullSafeArrayCopy(newEnabledSampleStreamWrappers, newEnabledSampleStreamWrapperCount);
+ compositeSequenceableLoader =
+ compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(
+ enabledSampleStreamWrappers);
+ return positionUs;
+ }
+
+ @Override
+ 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) {
+ 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 compositeSequenceableLoader.getNextLoadPositionUs();
+ }
+
+ @Override
+ public long readDiscontinuity() {
+ if (!notifiedReadingStarted) {
+ eventDispatcher.readingStarted();
+ notifiedReadingStarted = true;
+ }
+ return C.TIME_UNSET;
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ return compositeSequenceableLoader.getBufferedPositionUs();
+ }
+
+ @Override
+ public long seekToUs(long 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
+ public void onPrepared() {
+ if (--pendingPrepareCount > 0) {
+ return;
+ }
+
+ int totalTrackGroupCount = 0;
+ for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
+ totalTrackGroupCount += sampleStreamWrapper.getTrackGroups().length;
+ }
+ TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount];
+ int trackGroupIndex = 0;
+ for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
+ int wrapperTrackGroupCount = sampleStreamWrapper.getTrackGroups().length;
+ for (int j = 0; j < wrapperTrackGroupCount; j++) {
+ trackGroupArray[trackGroupIndex++] = sampleStreamWrapper.getTrackGroups().get(j);
+ }
+ }
+ trackGroups = new TrackGroupArray(trackGroupArray);
+ callback.onPrepared(this);
+ }
+
+ @Override
+ public void onPlaylistRefreshRequired(Uri url) {
+ playlistTracker.refreshPlaylist(url);
+ }
+
+ @Override
+ public void onContinueLoadingRequested(HlsSampleStreamWrapper sampleStreamWrapper) {
+ callback.onContinueLoadingRequested(this);
+ }
+
+ // PlaylistListener implementation.
+
+ @Override
+ public void onPlaylistChanged() {
+ callback.onContinueLoadingRequested(this);
+ }
+
+ @Override
+ public boolean onPlaylistError(Uri url, long blacklistDurationMs) {
+ boolean noBlacklistingFailure = true;
+ for (HlsSampleStreamWrapper streamWrapper : sampleStreamWrappers) {
+ noBlacklistingFailure &= streamWrapper.onPlaylistError(url, blacklistDurationMs);
+ }
+ callback.onContinueLoadingRequested(this);
+ return noBlacklistingFailure;
+ }
+
+ // Internal methods.
+
+ private void buildAndPrepareSampleStreamWrappers(long positionUs) {
+ HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist());
+ Map<String, DrmInitData> overridingDrmInitData =
+ useSessionKeys
+ ? deriveOverridingDrmInitData(masterPlaylist.sessionKeyDrmInitData)
+ : Collections.emptyMap();
+
+ boolean hasVariants = !masterPlaylist.variants.isEmpty();
+ List<Rendition> audioRenditions = masterPlaylist.audios;
+ List<Rendition> subtitleRenditions = masterPlaylist.subtitles;
+
+ pendingPrepareCount = 0;
+ ArrayList<HlsSampleStreamWrapper> sampleStreamWrappers = new ArrayList<>();
+ ArrayList<int[]> manifestUrlIndicesPerWrapper = new ArrayList<>();
+
+ if (hasVariants) {
+ buildAndPrepareMainSampleStreamWrapper(
+ masterPlaylist,
+ positionUs,
+ sampleStreamWrappers,
+ manifestUrlIndicesPerWrapper,
+ overridingDrmInitData);
+ }
+
+ // TODO: Build video stream wrappers here.
+
+ 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;
+ }
+
+ /**
+ * This method creates and starts preparation of the main {@link HlsSampleStreamWrapper}.
+ *
+ * <p>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.
+ *
+ * <p>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:
+ *
+ * <ul>
+ * <li>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.
+ * <li>Closed captions will only be exposed if they are declared by the master playlist.
+ * <li>An ID3 track is exposed preemptively, in case the segments contain an ID3 track.
+ * </ul>
+ *
+ * @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<HlsSampleStreamWrapper> sampleStreamWrappers,
+ List<int[]> manifestUrlIndicesPerWrapper,
+ Map<String, DrmInitData> 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<TrackGroup> 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<Format> 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 void buildAndPrepareAudioSampleStreamWrappers(
+ long positionUs,
+ List<Rendition> audioRenditions,
+ List<HlsSampleStreamWrapper> sampleStreamWrappers,
+ List<int[]> manifestUrlsIndicesPerWrapper,
+ Map<String, DrmInitData> overridingDrmInitData) {
+ ArrayList<Uri> scratchPlaylistUrls =
+ new ArrayList<>(/* initialCapacity= */ audioRenditions.size());
+ ArrayList<Format> scratchPlaylistFormats =
+ new ArrayList<>(/* initialCapacity= */ audioRenditions.size());
+ ArrayList<Integer> scratchIndicesList =
+ new ArrayList<>(/* initialCapacity= */ audioRenditions.size());
+ HashSet<String> 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 HlsSampleStreamWrapper buildSampleStreamWrapper(
+ int trackType,
+ Uri[] playlistUrls,
+ Format[] playlistFormats,
+ @Nullable Format muxedAudioFormat,
+ @Nullable List<Format> muxedCaptionFormats,
+ Map<String, DrmInitData> 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<String, DrmInitData> deriveOverridingDrmInitData(
+ List<DrmInitData> sessionKeyDrmInitData) {
+ ArrayList<DrmInitData> mutableSessionKeyDrmInitData = new ArrayList<>(sessionKeyDrmInitData);
+ HashMap<String, DrmInitData> 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 {
+ 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;
+ }
+ }
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
new file mode 100644
index 0000000000..2fa49e13f0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
@@ -0,0 +1,528 @@
+/*
+ * 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;
+
+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.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 extends BaseMediaSource
+ implements HlsPlaylistTracker.PrimaryPlaylistListener {
+
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.hls");
+ }
+
+ /**
+ * The types of metadata that can be extracted from HLS streams.
+ *
+ * <p>Allowed values:
+ *
+ * <ul>
+ * <li>{@link #METADATA_TYPE_ID3}
+ * <li>{@link #METADATA_TYPE_EMSG}
+ * </ul>
+ *
+ * <p>See {@link Factory#setMetadataType(int)}.
+ */
+ @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<StreamKey> 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()}.
+ *
+ * <p>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}.
+ *
+ * <p>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}).
+ *
+ * <p>HLS supports in-band ID3 in both TS and fMP4 streams, but in the fMP4 case the data is
+ * wrapped in an EMSG box [<a href="https://aomediacodec.github.io/av1-id3/">spec</a>].
+ *
+ * <p>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.
+ *
+ * <p>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<StreamKey> 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 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;
+
+ @Nullable private TransferListener mediaTransferListener;
+
+ 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.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
+ @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.maybeThrowPrimaryPlaylistRefreshError();
+ }
+
+ @Override
+ 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
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ ((HlsMediaPeriod) mediaPeriod).release();
+ }
+
+ @Override
+ 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 offsetFromInitialStartTimeUs =
+ playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
+ long periodDurationUs =
+ playlist.hasEndTag ? offsetFromInitialStartTimeUs + playlist.durationUs : C.TIME_UNSET;
+ List<HlsMediaPlaylist.Segment> segments = playlist.segments;
+ if (windowDefaultStartPositionUs == C.TIME_UNSET) {
+ 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(
+ 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(
+ presentationStartTimeMs,
+ windowStartTimeMs,
+ /* periodDurationUs= */ playlist.durationUs,
+ /* windowDurationUs= */ playlist.durationUs,
+ /* windowPositionInPeriodUs= */ 0,
+ windowDefaultStartPositionUs,
+ /* isSeekable= */ true,
+ /* isDynamic= */ false,
+ /* isLive= */ false,
+ manifest,
+ tag);
+ }
+ refreshSourceInfo(timeline);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStream.java
new file mode 100644
index 0000000000..5f44810af5
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStream.java
@@ -0,0 +1,97 @@
+/*
+ * 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;
+
+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 sample queue in HLS.
+ */
+/* package */ final class HlsSampleStream implements SampleStream {
+
+ private final int trackGroupIndex;
+ private final HlsSampleStreamWrapper sampleStreamWrapper;
+ private int sampleQueueIndex;
+
+ public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int trackGroupIndex) {
+ this.sampleStreamWrapper = sampleStreamWrapper;
+ 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 sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL
+ || (hasValidSampleQueueIndex() && sampleStreamWrapper.isReady(sampleQueueIndex));
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ 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) {
+ 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 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
new file mode 100644
index 0000000000..833abbc29f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
@@ -0,0 +1,1535 @@
+/*
+ * 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;
+
+import android.net.Uri;
+import android.os.Handler;
+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.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.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.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.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<Chunk>,
+ Loader.ReleaseCallback, SequenceableLoader, ExtractorOutput, UpstreamFormatChangedListener {
+
+ /**
+ * A callback to be notified of events.
+ */
+ public interface Callback extends SequenceableLoader.Callback<HlsSampleStreamWrapper> {
+
+ /**
+ * Called when the wrapper has been prepared.
+ *
+ * <p>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();
+
+ /**
+ * Called to schedule a {@link #continueLoading(long)} call when the playlist referred by the
+ * given url changes.
+ */
+ void onPlaylistRefreshRequired(Uri playlistUrl);
+ }
+
+ 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<Integer> 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;
+ @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 ArrayList<HlsMediaChunk> mediaChunks;
+ private final List<HlsMediaChunk> 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<HlsSampleStream> hlsSampleStreams;
+ private final Map<String, DrmInitData> overridingDrmInitData;
+
+ private FormatAdjustingSampleQueue[] sampleQueues;
+ private int[] sampleQueueTrackIds;
+ private Set<Integer> sampleQueueMappingDoneByType;
+ private SparseIntArray sampleQueueIndicesByType;
+ @MonotonicNonNull private TrackOutput emsgUnwrappingTrackOutput;
+ private int primarySampleQueueType;
+ private int primarySampleQueueIndex;
+ private boolean sampleQueuesBuilt;
+ private boolean prepared;
+ private int enabledTrackGroupCount;
+ @MonotonicNonNull private Format upstreamTrackFormat;
+ @Nullable private Format downstreamTrackFormat;
+ private boolean released;
+
+ // Tracks are complicated in HLS. See documentation of buildTracksFromSampleStreams for details.
+ // Indexed by track (as exposed by this source).
+ @MonotonicNonNull private TrackGroupArray trackGroups;
+ @MonotonicNonNull private Set<TrackGroup> optionalTrackGroups;
+ // Indexed by track group.
+ private int @MonotonicNonNull [] trackGroupToSampleQueueIndex;
+ private int primaryTrackGroupIndex;
+ 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 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,
+ Map<String, DrmInitData> 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.drmSessionManager = drmSessionManager;
+ this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ this.eventDispatcher = eventDispatcher;
+ this.metadataType = metadataType;
+ loader = new Loader("Loader:HlsSampleStreamWrapper");
+ nextChunkHolder = new HlsChunkSource.HlsChunkHolder();
+ 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;
+ }
+
+ public void continuePreparing() {
+ if (!prepared) {
+ continueLoading(lastSeekPositionUs);
+ }
+ }
+
+ /**
+ * 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 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 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++) {
+ HlsSampleStream stream = (HlsSampleStream) streams[i];
+ if (stream != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
+ enabledTrackGroupCount--;
+ stream.unbindSampleQueue();
+ streams[i] = null;
+ }
+ }
+ // 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++) {
+ 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;
+ 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;
+ }
+ }
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+ }
+
+ updateSampleStreams(streams);
+ seenFirstTrackSelection = true;
+ return seekRequired;
+ }
+
+ 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 {
+ 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;
+ } else if (isPendingReset()) {
+ return pendingResetPositionUs;
+ } else {
+ long bufferedPositionUs = lastSeekPositionUs;
+ 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);
+ }
+ if (sampleQueuesBuilt) {
+ for (SampleQueue sampleQueue : sampleQueues) {
+ bufferedPositionUs =
+ Math.max(bufferedPositionUs, sampleQueue.getLargestQueuedTimestampUs());
+ }
+ }
+ return bufferedPositionUs;
+ }
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ if (isPendingReset()) {
+ return pendingResetPositionUs;
+ } else {
+ return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs;
+ }
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ if (loadingFinished || loader.isLoading() || loader.hasFatalError()) {
+ return false;
+ }
+
+ List<HlsMediaChunk> 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;
+ Uri playlistUrlToLoad = nextChunkHolder.playlistUrl;
+ nextChunkHolder.clear();
+
+ if (endOfStream) {
+ pendingResetPositionUs = C.TIME_UNSET;
+ loadingFinished = true;
+ return true;
+ }
+
+ if (loadable == null) {
+ if (playlistUrlToLoad != null) {
+ callback.onPlaylistRefreshRequired(playlistUrlToLoad);
+ }
+ return false;
+ }
+
+ if (isMediaChunk(loadable)) {
+ pendingResetPositionUs = C.TIME_UNSET;
+ HlsMediaChunk mediaChunk = (HlsMediaChunk) loadable;
+ mediaChunk.init(this);
+ mediaChunks.add(mediaChunk);
+ upstreamTrackFormat = mediaChunk.trackFormat;
+ }
+ 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 boolean isLoading() {
+ return loader.isLoading();
+ }
+
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ // Do nothing.
+ }
+
+ // Loader.Callback implementation.
+
+ @Override
+ public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) {
+ chunkSource.onChunkLoadCompleted(loadable);
+ 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 {
+ callback.onContinueLoadingRequested(this);
+ }
+ }
+
+ @Override
+ public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs,
+ boolean released) {
+ 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) {
+ resetSampleQueues();
+ if (enabledTrackGroupCount > 0) {
+ callback.onContinueLoadingRequested(this);
+ }
+ }
+ }
+
+ @Override
+ public LoadErrorAction onLoadError(
+ Chunk loadable,
+ long elapsedRealtimeMs,
+ long loadDurationMs,
+ IOException error,
+ int errorCount) {
+ long bytesLoaded = loadable.bytesLoaded();
+ boolean isMediaChunk = isMediaChunk(loadable);
+ 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;
+ }
+ }
+ 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.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 loadErrorAction;
+ }
+
+ // Called by the consuming thread, but only when there is no loading thread.
+
+ /**
+ * Initializes the wrapper for loading a chunk.
+ *
+ * @param chunkUid The chunk's uid.
+ * @param shouldSpliceIn Whether the samples parsed from the chunk should be spliced into any
+ * samples already queued to the wrapper.
+ */
+ public void init(int chunkUid, boolean shouldSpliceIn) {
+ this.chunkUid = chunkUid;
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.sourceId(chunkUid);
+ }
+ if (shouldSpliceIn) {
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.splice();
+ }
+ }
+ }
+
+ // ExtractorOutput implementation. Called by the loading thread.
+
+ @Override
+ 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;
+ }
+ }
+ }
+
+ 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.
+ *
+ * <p>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.
+ *
+ * <p>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);
+ 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() {
+ tracksEnded = true;
+ handler.post(onTracksEndedRunnable);
+ }
+
+ @Override
+ public void seekMap(SeekMap seekMap) {
+ // Do nothing.
+ }
+
+ // UpstreamFormatChangedListener implementation. Called by the loading thread.
+
+ @Override
+ public void onUpstreamFormatChanged(Format format) {
+ 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.
+ *
+ * <p>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.
+ *
+ * <p>The final {@link DrmInitData} for subsequently queued samples is determined as followed:
+ *
+ * <ol>
+ * <li>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}.
+ * <li>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.
+ * </ol>
+ *
+ * <p>
+ *
+ * @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 || trackGroupToSampleQueueIndex != null || !sampleQueuesBuilt) {
+ return;
+ }
+ for (SampleQueue sampleQueue : sampleQueues) {
+ if (sampleQueue.getUpstreamFormat() == null) {
+ return;
+ }
+ }
+ 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.
+ *
+ * <p>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.
+ *
+ * <p>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}:
+ *
+ * <ul>
+ * <li>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.
+ * <li>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.
+ * <li>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.
+ * </ul>
+ */
+ @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 = C.TRACK_TYPE_NONE;
+ int primaryExtractorTrackIndex = C.INDEX_UNSET;
+ int extractorTrackCount = sampleQueues.length;
+ for (int i = 0; i < extractorTrackCount; i++) {
+ String sampleMimeType = sampleQueues[i].getUpstreamFormat().sampleMimeType;
+ int trackType;
+ if (MimeTypes.isVideo(sampleMimeType)) {
+ trackType = C.TRACK_TYPE_VIDEO;
+ } else if (MimeTypes.isAudio(sampleMimeType)) {
+ trackType = C.TRACK_TYPE_AUDIO;
+ } else if (MimeTypes.isText(sampleMimeType)) {
+ trackType = C.TRACK_TYPE_TEXT;
+ } else {
+ trackType = C.TRACK_TYPE_NONE;
+ }
+ if (getTrackTypeScore(trackType) > getTrackTypeScore(primaryExtractorTrackType)) {
+ primaryExtractorTrackType = trackType;
+ primaryExtractorTrackIndex = i;
+ } else if (trackType == primaryExtractorTrackType
+ && primaryExtractorTrackIndex != C.INDEX_UNSET) {
+ // We have multiple tracks of the primary type. We only want an index if there only exists a
+ // single track of the primary type, so unset the index again.
+ primaryExtractorTrackIndex = C.INDEX_UNSET;
+ }
+ }
+
+ TrackGroup chunkSourceTrackGroup = chunkSource.getTrackGroup();
+ int chunkSourceTrackCount = chunkSourceTrackGroup.length;
+
+ // Instantiate the necessary internal data-structures.
+ primaryTrackGroupIndex = C.INDEX_UNSET;
+ 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[i].getUpstreamFormat();
+ if (i == primaryExtractorTrackIndex) {
+ Format[] formats = new Format[chunkSourceTrackCount];
+ 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 == C.TRACK_TYPE_VIDEO
+ && MimeTypes.isAudio(sampleFormat.sampleMimeType)
+ ? muxedAudioFormat
+ : null;
+ trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat, false));
+ }
+ }
+ this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups);
+ Assertions.checkState(optionalTrackGroups == null);
+ optionalTrackGroups = Collections.emptySet();
+ }
+
+ 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);
+ }
+ return new TrackGroupArray(trackGroups);
+ }
+
+ private HlsMediaChunk getLastMediaChunk() {
+ return mediaChunks.get(mediaChunks.size() - 1);
+ }
+
+ private boolean isPendingReset() {
+ return pendingResetPositionUs != C.TIME_UNSET;
+ }
+
+ /**
+ * 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 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<String, DrmInitData> overridingDrmInitData;
+ @Nullable private DrmInitData drmInitData;
+
+ public FormatAdjustingSampleQueue(
+ Allocator allocator,
+ DrmSessionManager<?> drmSessionManager,
+ Map<String, DrmInitData> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java
new file mode 100644
index 0000000000..681fe57240
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<VariantInfo> CREATOR =
+ new Parcelable.Creator<VariantInfo>() {
+ @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<VariantInfo> 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<VariantInfo> 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<VariantInfo> 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<HlsTrackMetadataEntry> CREATOR =
+ new Parcelable.Creator<HlsTrackMetadataEntry>() {
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java
new file mode 100644
index 0000000000..a67a92b4b7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java
new file mode 100644
index 0000000000..e2a652d05c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.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.source.hls;
+
+import android.util.SparseArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+
+/**
+ * Provides {@link TimestampAdjuster} instances for use during HLS playbacks.
+ */
+public final class TimestampAdjusterProvider {
+
+ // TODO: Prevent this array from growing indefinitely large by removing adjusters that are no
+ // longer required.
+ private final SparseArray<TimestampAdjuster> timestampAdjusters;
+
+ public TimestampAdjusterProvider() {
+ timestampAdjusters = new SparseArray<>();
+ }
+
+ /**
+ * Returns a {@link TimestampAdjuster} suitable for adjusting the pts timestamps contained in
+ * a chunk with a given discontinuity sequence.
+ *
+ * @param discontinuitySequence The chunk's discontinuity sequence.
+ * @return A {@link TimestampAdjuster}.
+ */
+ public TimestampAdjuster getAdjuster(int discontinuitySequence) {
+ TimestampAdjuster adjuster = timestampAdjusters.get(discontinuitySequence);
+ if (adjuster == null) {
+ adjuster = new TimestampAdjuster(TimestampAdjuster.DO_NOT_OFFSET);
+ timestampAdjusters.put(discontinuitySequence, adjuster);
+ }
+ return adjuster;
+ }
+
+ /**
+ * Resets the provider.
+ */
+ public void reset() {
+ timestampAdjusters.clear();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/WebvttExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/WebvttExtractor.java
new file mode 100644
index 0000000000..1d5e669a03
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/WebvttExtractor.java
@@ -0,0 +1,195 @@
+/*
+ * 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;
+
+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;
+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.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;
+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.
+ *
+ * <p>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.
+ */
+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 int HEADER_MIN_LENGTH = 6 /* "WEBVTT" */;
+ private static final int HEADER_MAX_LENGTH = 3 /* optional Byte Order Mark */ + HEADER_MIN_LENGTH;
+
+ @Nullable private final String language;
+ private final TimestampAdjuster timestampAdjuster;
+ private final ParsableByteArray sampleDataWrapper;
+
+ private @MonotonicNonNull ExtractorOutput output;
+
+ private byte[] sampleData;
+ private int sampleSize;
+
+ public WebvttExtractor(@Nullable String language, TimestampAdjuster timestampAdjuster) {
+ this.language = language;
+ this.timestampAdjuster = timestampAdjuster;
+ this.sampleDataWrapper = new ParsableByteArray();
+ sampleData = new byte[1024];
+ }
+
+ // Extractor implementation.
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ // 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
+ public void init(ExtractorOutput output) {
+ this.output = output;
+ output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ // This extractor is only used for the HLS use case, which should not call this method.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @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.
+ if (sampleSize == sampleData.length) {
+ sampleData = Arrays.copyOf(sampleData,
+ (currentFileSize != C.LENGTH_UNSET ? currentFileSize : sampleData.length) * 3 / 2);
+ }
+
+ // Consume to the input.
+ int bytesRead = input.read(sampleData, sampleSize, sampleData.length - sampleSize);
+ if (bytesRead != C.RESULT_END_OF_INPUT) {
+ sampleSize += bytesRead;
+ if (currentFileSize == C.LENGTH_UNSET || sampleSize != currentFileSize) {
+ return Extractor.RESULT_CONTINUE;
+ }
+ }
+
+ // We've reached the end of the input, which corresponds to the end of the current file.
+ processSample();
+ 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.
+ 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.
+ 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()) {
+ throw new ParserException("X-TIMESTAMP-MAP doesn't contain local timestamp: " + line);
+ }
+ Matcher mediaTimestampMatcher = MEDIA_TIMESTAMP.matcher(line);
+ if (!mediaTimestampMatcher.find()) {
+ 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)));
+ }
+ }
+
+ // Find the first cue header and parse the start time.
+ Matcher cueHeaderMatcher = WebvttParserUtil.findNextCueHeader(webvttData);
+ if (cueHeaderMatcher == null) {
+ // No cues found. Don't output a sample, but still output a corresponding track.
+ buildTrackOutput(0);
+ return;
+ }
+
+ long firstCueTimeUs = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1));
+ long sampleTimeUs = timestampAdjuster.adjustTsTimestamp(
+ TimestampAdjuster.usToPts(firstCueTimeUs + tsTimestampUs - vttTimestampUs));
+ long subsampleOffsetUs = sampleTimeUs - firstCueTimeUs;
+ // Output the track.
+ TrackOutput trackOutput = buildTrackOutput(subsampleOffsetUs);
+ // Output the sample.
+ sampleDataWrapper.reset(sampleData, sampleSize);
+ trackOutput.sampleData(sampleDataWrapper, sampleSize);
+ 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,
+ Format.NO_VALUE, 0, language, null, subsampleOffsetUs));
+ output.endTracks();
+ return trackOutput;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java
new file mode 100644
index 0000000000..636100a8a9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>Example usage:
+ *
+ * <pre>{@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);
+ * }</pre>
+ */
+public final class HlsDownloader extends SegmentDownloader<HlsPlaylist> {
+
+ /**
+ * @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<StreamKey> streamKeys, DownloaderConstructorHelper constructorHelper) {
+ super(playlistUri, streamKeys, constructorHelper);
+ }
+
+ @Override
+ protected HlsPlaylist getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException {
+ return loadManifest(dataSource, dataSpec);
+ }
+
+ @Override
+ protected List<Segment> getSegments(
+ DataSource dataSource, HlsPlaylist playlist, boolean allowIncompleteList) throws IOException {
+ ArrayList<DataSpec> 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<Segment> segments = new ArrayList<>();
+ HashSet<Uri> 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<HlsMediaPlaylist.Segment> 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<Uri> mediaPlaylistUrls, List<DataSpec> 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<Uri> seenEncryptionKeyUris,
+ ArrayList<Segment> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/offline/package-info.java
new file mode 100644
index 0000000000..669bd44c89
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/package-info.java
new file mode 100644
index 0000000000..89882bb596
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistParserFactory.java
new file mode 100644
index 0000000000..394a97a56a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<HlsPlaylist> createPlaylistParser() {
+ return new HlsPlaylistParser();
+ }
+
+ @Override
+ public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
+ HlsMasterPlaylist masterPlaylist) {
+ return new HlsPlaylistParser(masterPlaylist);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java
new file mode 100644
index 0000000000..b7f6a06975
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<ParsingLoadable<HlsPlaylist>> {
+
+ /** 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<Uri, MediaPlaylistBundle> playlistBundles;
+ private final List<PlaylistEventListener> listeners;
+ private final double playlistStuckTargetDurationCoefficient;
+
+ @Nullable private ParsingLoadable.Parser<HlsPlaylist> 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<HlsPlaylist> 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<HlsPlaylist> 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<HlsPlaylist> 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<HlsPlaylist> 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<Variant> 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<Variant> 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<Uri> 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<Segment> 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<ParsingLoadable<HlsPlaylist>>, Runnable {
+
+ private final Uri playlistUrl;
+ private final Loader mediaPlaylistLoader;
+ private final ParsingLoadable<HlsPlaylist> 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<HlsPlaylist> 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<HlsPlaylist> 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<HlsPlaylist> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/FilteringHlsPlaylistParserFactory.java
new file mode 100644
index 0000000000..a8c9ea1756
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<StreamKey> 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<StreamKey> streamKeys) {
+ this.hlsPlaylistParserFactory = hlsPlaylistParserFactory;
+ this.streamKeys = streamKeys;
+ }
+
+ @Override
+ public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() {
+ return new FilteringManifestParser<>(
+ hlsPlaylistParserFactory.createPlaylistParser(), streamKeys);
+ }
+
+ @Override
+ public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
+ HlsMasterPlaylist masterPlaylist) {
+ return new FilteringManifestParser<>(
+ hlsPlaylistParserFactory.createPlaylistParser(masterPlaylist), streamKeys);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java
new file mode 100644
index 0000000000..376f2b4301
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java
@@ -0,0 +1,330 @@
+/*
+ * 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 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. */
+public final class HlsMasterPlaylist extends HlsPlaylist {
+
+ /** 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());
+
+ // 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;
+
+ /** The video rendition group referenced by this variant, or {@code null}. */
+ @Nullable public final String videoGroupId;
+
+ /** 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;
+ }
+
+ }
+
+ /** All of the media playlist URLs referenced by the playlist. */
+ public final List<Uri> mediaPlaylistUrls;
+ /** The variants declared by the playlist. */
+ public final List<Variant> variants;
+ /** The video renditions declared by the playlist. */
+ public final List<Rendition> videos;
+ /** The audio renditions declared by the playlist. */
+ public final List<Rendition> audios;
+ /** The subtitle renditions declared by the playlist. */
+ public final List<Rendition> subtitles;
+ /** The closed caption renditions declared by the playlist. */
+ public final List<Rendition> closedCaptions;
+
+ /**
+ * 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<Format> muxedCaptionFormats;
+ /** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */
+ public final Map<String, String> variableDefinitions;
+ /** DRM initialization data derived from #EXT-X-SESSION-KEY tags. */
+ public final List<DrmInitData> sessionKeyDrmInitData;
+
+ /**
+ * @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<String> tags,
+ List<Variant> variants,
+ List<Rendition> videos,
+ List<Rendition> audios,
+ List<Rendition> subtitles,
+ List<Rendition> closedCaptions,
+ @Nullable Format muxedAudioFormat,
+ @Nullable List<Format> muxedCaptionFormats,
+ boolean hasIndependentSegments,
+ Map<String, String> variableDefinitions,
+ List<DrmInitData> 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 = muxedCaptionFormats != null
+ ? Collections.unmodifiableList(muxedCaptionFormats) : null;
+ this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions);
+ this.sessionKeyDrmInitData = Collections.unmodifiableList(sessionKeyDrmInitData);
+ }
+
+ @Override
+ public HlsMasterPlaylist copy(List<StreamKey> 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> 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<Uri> getMediaPlaylistUrls(
+ List<Variant> variants,
+ List<Rendition> videos,
+ List<Rendition> audios,
+ List<Rendition> subtitles,
+ List<Rendition> closedCaptions) {
+ ArrayList<Uri> 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<Rendition> renditions, List<Uri> 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 <T> List<T> copyStreams(
+ List<T> streams, int groupIndex, List<StreamKey> streamKeys) {
+ List<T> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
new file mode 100644
index 0000000000..c3250a5cc0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
@@ -0,0 +1,375 @@
+/*
+ * 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 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. */
+public final class HlsMediaPlaylist extends HlsPlaylist {
+
+ /** Media segment reference. */
+ @SuppressWarnings("ComparableType")
+ public static final class Segment implements Comparable<Long> {
+
+ /**
+ * 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;
+ /**
+ * 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;
+
+ /** 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);
+ }
+
+ /**
+ * @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.drmInitData = drmInitData;
+ this.fullSegmentEncryptionKeyUri = fullSegmentEncryptionKeyUri;
+ this.encryptionIV = encryptionIV;
+ this.byterangeOffset = byterangeOffset;
+ this.byterangeLength = byterangeLength;
+ this.hasGapTag = hasGapTag;
+ }
+
+ @Override
+ public int compareTo(Long relativeStartTimeUs) {
+ return this.relativeStartTimeUs > relativeStartTimeUs
+ ? 1 : (this.relativeStartTimeUs < relativeStartTimeUs ? -1 : 0);
+ }
+
+ }
+
+ /**
+ * 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;
+ /**
+ * 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;
+ /**
+ * 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<Segment> segments;
+ /**
+ * The total duration of the playlist in microseconds.
+ */
+ public final long durationUs;
+
+ /**
+ * @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<String> 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<Segment> segments) {
+ super(baseUri, tags, hasIndependentSegments);
+ this.playlistType = playlistType;
+ this.startTimeUs = startTimeUs;
+ this.hasDiscontinuitySequence = hasDiscontinuitySequence;
+ this.discontinuitySequence = discontinuitySequence;
+ this.mediaSequence = mediaSequence;
+ this.version = version;
+ this.targetDurationUs = targetDurationUs;
+ this.hasEndTag = hasEndTag;
+ this.hasProgramDateTime = hasProgramDateTime;
+ this.protectionSchemes = protectionSchemes;
+ this.segments = Collections.unmodifiableList(segments);
+ if (!segments.isEmpty()) {
+ Segment last = segments.get(segments.size() - 1);
+ durationUs = last.relativeStartTimeUs + last.durationUs;
+ } else {
+ durationUs = 0;
+ }
+ this.startOffsetUs = startOffsetUs == C.TIME_UNSET ? C.TIME_UNSET
+ : startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs;
+ }
+
+ @Override
+ public HlsMediaPlaylist copy(List<StreamKey> streamKeys) {
+ return this;
+ }
+
+ /**
+ * Returns whether this playlist is newer than {@code other}.
+ *
+ * @param other The playlist to compare.
+ * @return Whether this playlist is newer than {@code other}.
+ */
+ public boolean isNewerThan(HlsMediaPlaylist other) {
+ if (other == null || mediaSequence > other.mediaSequence) {
+ return true;
+ }
+ if (mediaSequence < other.mediaSequence) {
+ return false;
+ }
+ // The media sequences are equal.
+ int segmentCount = segments.size();
+ int otherSegmentCount = other.segments.size();
+ return segmentCount > otherSegmentCount
+ || (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;
+ }
+
+ /**
+ * Returns a playlist identical to this one except for the start time, the discontinuity sequence
+ * and {@code hasDiscontinuitySequence} values. The first two are set to the specified values,
+ * {@code hasDiscontinuitySequence} is set to true.
+ *
+ * @param startTimeUs The start time for the returned playlist.
+ * @param discontinuitySequence The discontinuity sequence for the returned playlist.
+ * @return An identical playlist including the provided discontinuity and timing information.
+ */
+ public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) {
+ 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.
+ */
+ public HlsMediaPlaylist copyWithEndTag() {
+ if (this.hasEndTag) {
+ return this;
+ }
+ return new HlsMediaPlaylist(
+ playlistType,
+ baseUri,
+ tags,
+ startOffsetUs,
+ startTimeUs,
+ hasDiscontinuitySequence,
+ discontinuitySequence,
+ mediaSequence,
+ version,
+ targetDurationUs,
+ hasIndependentSegments,
+ /* hasEndTag= */ true,
+ hasProgramDateTime,
+ protectionSchemes,
+ segments);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java
new file mode 100644
index 0000000000..28f9b0eeb0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java
@@ -0,0 +1,50 @@
+/*
+ * 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 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<HlsPlaylist> {
+
+ /**
+ * The base uri. Used to resolve relative paths.
+ */
+ public final String baseUri;
+ /**
+ * The list of tags in the playlist.
+ */
+ public final List<String> tags;
+ /**
+ * Whether the media is formed of independent segments, as defined by the
+ * #EXT-X-INDEPENDENT-SEGMENTS tag.
+ */
+ public final boolean hasIndependentSegments;
+
+ /**
+ * @param baseUri See {@link #baseUri}.
+ * @param tags See {@link #tags}.
+ * @param hasIndependentSegments See {@link #hasIndependentSegments}.
+ */
+ protected HlsPlaylist(String baseUri, List<String> tags, boolean hasIndependentSegments) {
+ this.baseUri = baseUri;
+ this.tags = Collections.unmodifiableList(tags);
+ this.hasIndependentSegments = hasIndependentSegments;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
new file mode 100644
index 0000000000..5495d28520
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
@@ -0,0 +1,1007 @@
+/*
+ * 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.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.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.
+ */
+public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlaylist> {
+
+ private static final String PLAYLIST_HEADER = "#EXTM3U";
+
+ private static final String TAG_PREFIX = "#EXT";
+
+ private static final String TAG_VERSION = "#EXT-X-VERSION";
+ private static final String TAG_PLAYLIST_TYPE = "#EXT-X-PLAYLIST-TYPE";
+ private static final String TAG_DEFINE = "#EXT-X-DEFINE";
+ private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF";
+ private static final String TAG_MEDIA = "#EXT-X-MEDIA";
+ private static final String TAG_TARGET_DURATION = "#EXT-X-TARGETDURATION";
+ private static final String TAG_DISCONTINUITY = "#EXT-X-DISCONTINUITY";
+ private static final String TAG_DISCONTINUITY_SEQUENCE = "#EXT-X-DISCONTINUITY-SEQUENCE";
+ private static final String TAG_PROGRAM_DATE_TIME = "#EXT-X-PROGRAM-DATE-TIME";
+ private static final String TAG_INIT_SEGMENT = "#EXT-X-MAP";
+ private static final String TAG_INDEPENDENT_SEGMENTS = "#EXT-X-INDEPENDENT-SEGMENTS";
+ private static final String TAG_MEDIA_DURATION = "#EXTINF";
+ private static final String TAG_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE";
+ private static final String TAG_START = "#EXT-X-START";
+ private static final String TAG_ENDLIST = "#EXT-X-ENDLIST";
+ private static final String TAG_KEY = "#EXT-X-KEY";
+ private static final String TAG_SESSION_KEY = "#EXT-X-SESSION-KEY";
+ private static final String TAG_BYTERANGE = "#EXT-X-BYTERANGE";
+ private static final String TAG_GAP = "#EXT-X-GAP";
+
+ private static final String TYPE_AUDIO = "AUDIO";
+ private static final String TYPE_VIDEO = "VIDEO";
+ private static final String TYPE_SUBTITLES = "SUBTITLES";
+ private static final String TYPE_CLOSED_CAPTIONS = "CLOSED-CAPTIONS";
+
+ private static final String METHOD_NONE = "NONE";
+ private static final String METHOD_AES_128 = "AES-128";
+ private static final String METHOD_SAMPLE_AES = "SAMPLE-AES";
+ // Replaced by METHOD_SAMPLE_AES_CTR. Keep for backward compatibility.
+ private static final String METHOD_SAMPLE_AES_CENC = "SAMPLE-AES-CENC";
+ private static final String METHOD_SAMPLE_AES_CTR = "SAMPLE-AES-CTR";
+ private static final String KEYFORMAT_PLAYREADY = "com.microsoft.playready";
+ private static final String KEYFORMAT_IDENTITY = "identity";
+ private static final String KEYFORMAT_WIDEVINE_PSSH_BINARY =
+ "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";
+ private static final String KEYFORMAT_WIDEVINE_PSSH_JSON = "com.widevine";
+
+ private static final String BOOLEAN_TRUE = "YES";
+ private static final String BOOLEAN_FALSE = "NO";
+
+ private static final String ATTR_CLOSED_CAPTIONS_NONE = "CLOSED-CAPTIONS=NONE";
+
+ private static final Pattern REGEX_AVERAGE_BANDWIDTH =
+ Pattern.compile("AVERAGE-BANDWIDTH=(\\d+)\\b");
+ private static final Pattern REGEX_VIDEO = Pattern.compile("VIDEO=\"(.+?)\"");
+ private static final Pattern REGEX_AUDIO = Pattern.compile("AUDIO=\"(.+?)\"");
+ private static final Pattern REGEX_SUBTITLES = Pattern.compile("SUBTITLES=\"(.+?)\"");
+ private static final Pattern REGEX_CLOSED_CAPTIONS = Pattern.compile("CLOSED-CAPTIONS=\"(.+?)\"");
+ private static final Pattern REGEX_BANDWIDTH = Pattern.compile("[^-]BANDWIDTH=(\\d+)\\b");
+ private static final Pattern REGEX_CHANNELS = Pattern.compile("CHANNELS=\"(.+?)\"");
+ private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\"");
+ private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)");
+ private static final Pattern REGEX_FRAME_RATE = Pattern.compile("FRAME-RATE=([\\d\\.]+)\\b");
+ private static final Pattern REGEX_TARGET_DURATION = Pattern.compile(TAG_TARGET_DURATION
+ + ":(\\d+)\\b");
+ private static final Pattern REGEX_VERSION = Pattern.compile(TAG_VERSION + ":(\\d+)\\b");
+ private static final Pattern REGEX_PLAYLIST_TYPE = Pattern.compile(TAG_PLAYLIST_TYPE
+ + ":(.+)\\b");
+ private static final Pattern REGEX_MEDIA_SEQUENCE = Pattern.compile(TAG_MEDIA_SEQUENCE
+ + ":(\\d+)\\b");
+ private static final Pattern REGEX_MEDIA_DURATION = Pattern.compile(TAG_MEDIA_DURATION
+ + ":([\\d\\.]+)\\b");
+ private static final Pattern REGEX_MEDIA_TITLE =
+ Pattern.compile(TAG_MEDIA_DURATION + ":[\\d\\.]+\\b,(.+)");
+ private static final Pattern REGEX_TIME_OFFSET = Pattern.compile("TIME-OFFSET=(-?[\\d\\.]+)\\b");
+ private static final Pattern REGEX_BYTERANGE = Pattern.compile(TAG_BYTERANGE
+ + ":(\\d+(?:@\\d+)?)\\b");
+ private static final Pattern REGEX_ATTR_BYTERANGE =
+ Pattern.compile("BYTERANGE=\"(\\d+(?:@\\d+)?)\\b\"");
+ private static final Pattern REGEX_METHOD =
+ Pattern.compile(
+ "METHOD=("
+ + METHOD_NONE
+ + "|"
+ + METHOD_AES_128
+ + "|"
+ + METHOD_SAMPLE_AES
+ + "|"
+ + METHOD_SAMPLE_AES_CENC
+ + "|"
+ + METHOD_SAMPLE_AES_CTR
+ + ")"
+ + "\\s*(?:,|$)");
+ private static final Pattern REGEX_KEYFORMAT = Pattern.compile("KEYFORMAT=\"(.+?)\"");
+ private static final Pattern REGEX_KEYFORMATVERSIONS =
+ Pattern.compile("KEYFORMATVERSIONS=\"(.+?)\"");
+ private static final Pattern REGEX_URI = Pattern.compile("URI=\"(.+?)\"");
+ private static final Pattern REGEX_IV = Pattern.compile("IV=([^,.*]+)");
+ private static final Pattern REGEX_TYPE = Pattern.compile("TYPE=(" + TYPE_AUDIO + "|" + TYPE_VIDEO
+ + "|" + TYPE_SUBTITLES + "|" + TYPE_CLOSED_CAPTIONS + ")");
+ private static final Pattern REGEX_LANGUAGE = Pattern.compile("LANGUAGE=\"(.+?)\"");
+ private static final Pattern REGEX_NAME = Pattern.compile("NAME=\"(.+?)\"");
+ private static final Pattern REGEX_GROUP_ID = Pattern.compile("GROUP-ID=\"(.+?)\"");
+ private static final Pattern REGEX_CHARACTERISTICS = Pattern.compile("CHARACTERISTICS=\"(.+?)\"");
+ private static final Pattern REGEX_INSTREAM_ID =
+ Pattern.compile("INSTREAM-ID=\"((?:CC|SERVICE)\\d+)\"");
+ private static final Pattern REGEX_AUTOSELECT = compileBooleanAttrPattern("AUTOSELECT");
+ private static final Pattern REGEX_DEFAULT = compileBooleanAttrPattern("DEFAULT");
+ private static final Pattern REGEX_FORCED = compileBooleanAttrPattern("FORCED");
+ private static final Pattern REGEX_VALUE = Pattern.compile("VALUE=\"(.+?)\"");
+ private static final Pattern REGEX_IMPORT = Pattern.compile("IMPORT=\"(.+?)\"");
+ private static final Pattern REGEX_VARIABLE_REFERENCE =
+ Pattern.compile("\\{\\$([a-zA-Z0-9\\-_]+)\\}");
+
+ private final HlsMasterPlaylist masterPlaylist;
+
+ /**
+ * Creates an instance where media playlists are parsed without inheriting attributes from a
+ * master playlist.
+ */
+ public HlsPlaylistParser() {
+ this(HlsMasterPlaylist.EMPTY);
+ }
+
+ /**
+ * Creates an instance where parsed media playlists inherit attributes from the given master
+ * playlist.
+ *
+ * @param masterPlaylist The master playlist from which media playlists will inherit attributes.
+ */
+ public HlsPlaylistParser(HlsMasterPlaylist masterPlaylist) {
+ this.masterPlaylist = masterPlaylist;
+ }
+
+ @Override
+ public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
+ Queue<String> extraLines = new ArrayDeque<>();
+ String line;
+ try {
+ if (!checkPlaylistHeader(reader)) {
+ throw new UnrecognizedInputFormatException("Input does not start with the #EXTM3U header.",
+ uri);
+ }
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (line.isEmpty()) {
+ // Do nothing.
+ } else if (line.startsWith(TAG_STREAM_INF)) {
+ extraLines.add(line);
+ return parseMasterPlaylist(new LineIterator(extraLines, reader), uri.toString());
+ } else if (line.startsWith(TAG_TARGET_DURATION)
+ || line.startsWith(TAG_MEDIA_SEQUENCE)
+ || line.startsWith(TAG_MEDIA_DURATION)
+ || line.startsWith(TAG_KEY)
+ || line.startsWith(TAG_BYTERANGE)
+ || line.equals(TAG_DISCONTINUITY)
+ || line.equals(TAG_DISCONTINUITY_SEQUENCE)
+ || line.equals(TAG_ENDLIST)) {
+ extraLines.add(line);
+ return parseMediaPlaylist(
+ masterPlaylist, new LineIterator(extraLines, reader), uri.toString());
+ } else {
+ extraLines.add(line);
+ }
+ }
+ } finally {
+ Util.closeQuietly(reader);
+ }
+ throw new ParserException("Failed to parse the playlist, could not identify any tags.");
+ }
+
+ private static boolean checkPlaylistHeader(BufferedReader reader) throws IOException {
+ int last = reader.read();
+ if (last == 0xEF) {
+ if (reader.read() != 0xBB || reader.read() != 0xBF) {
+ return false;
+ }
+ // The playlist contains a Byte Order Mark, which gets discarded.
+ last = reader.read();
+ }
+ last = skipIgnorableWhitespace(reader, true, last);
+ int playlistHeaderLength = PLAYLIST_HEADER.length();
+ for (int i = 0; i < playlistHeaderLength; i++) {
+ if (last != PLAYLIST_HEADER.charAt(i)) {
+ return false;
+ }
+ last = reader.read();
+ }
+ last = skipIgnorableWhitespace(reader, false, last);
+ return Util.isLinebreak(last);
+ }
+
+ private static int skipIgnorableWhitespace(BufferedReader reader, boolean skipLinebreaks, int c)
+ throws IOException {
+ while (c != -1 && Character.isWhitespace(c) && (skipLinebreaks || !Util.isLinebreak(c))) {
+ c = reader.read();
+ }
+ return c;
+ }
+
+ private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri)
+ throws IOException {
+ HashMap<Uri, ArrayList<VariantInfo>> urlToVariantInfos = new HashMap<>();
+ HashMap<String, String> variableDefinitions = new HashMap<>();
+ ArrayList<Variant> variants = new ArrayList<>();
+ ArrayList<Rendition> videos = new ArrayList<>();
+ ArrayList<Rendition> audios = new ArrayList<>();
+ ArrayList<Rendition> subtitles = new ArrayList<>();
+ ArrayList<Rendition> closedCaptions = new ArrayList<>();
+ ArrayList<String> mediaTags = new ArrayList<>();
+ ArrayList<DrmInitData> sessionKeyDrmInitData = new ArrayList<>();
+ ArrayList<String> tags = new ArrayList<>();
+ Format muxedAudioFormat = null;
+ List<Format> muxedCaptionFormats = null;
+ boolean noClosedCaptions = false;
+ boolean hasIndependentSegmentsTag = false;
+
+ 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_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);
+ // 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) {
+ String[] widthAndHeight = resolutionString.split("x");
+ width = Integer.parseInt(widthAndHeight[0]);
+ height = Integer.parseInt(widthAndHeight[1]);
+ if (width <= 0 || height <= 0) {
+ // Resolution string is invalid.
+ width = Format.NO_VALUE;
+ height = Format.NO_VALUE;
+ }
+ } else {
+ width = Format.NO_VALUE;
+ height = Format.NO_VALUE;
+ }
+ float frameRate = Format.NO_VALUE;
+ String frameRateString =
+ parseOptionalStringAttr(line, REGEX_FRAME_RATE, variableDefinitions);
+ if (frameRateString != null) {
+ frameRate = Float.parseFloat(frameRateString);
+ }
+ String videoGroupId = parseOptionalStringAttr(line, REGEX_VIDEO, variableDefinitions);
+ String audioGroupId = parseOptionalStringAttr(line, REGEX_AUDIO, variableDefinitions);
+ String subtitlesGroupId =
+ parseOptionalStringAttr(line, REGEX_SUBTITLES, variableDefinitions);
+ String closedCaptionsGroupId =
+ parseOptionalStringAttr(line, REGEX_CLOSED_CAPTIONS, variableDefinitions);
+ if (!iterator.hasNext()) {
+ throw new ParserException("#EXT-X-STREAM-INF tag must be followed by another line");
+ }
+ line =
+ replaceVariableReferences(
+ iterator.next(), variableDefinitions); // #EXT-X-STREAM-INF's URI.
+ Uri uri = UriUtil.resolveToUri(baseUri, line);
+ Format format =
+ Format.createVideoContainerFormat(
+ /* id= */ Integer.toString(variants.size()),
+ /* label= */ null,
+ /* containerMimeType= */ MimeTypes.APPLICATION_M3U8,
+ /* sampleMimeType= */ null,
+ codecs,
+ /* metadata= */ null,
+ bitrate,
+ width,
+ height,
+ frameRate,
+ /* initializationData= */ null,
+ /* selectionFlags= */ 0,
+ /* roleFlags= */ 0);
+ Variant variant =
+ new Variant(
+ uri, format, videoGroupId, audioGroupId, subtitlesGroupId, closedCaptionsGroupId);
+ variants.add(variant);
+ ArrayList<VariantInfo> variantInfosForUrl = urlToVariantInfos.get(uri);
+ if (variantInfosForUrl == null) {
+ variantInfosForUrl = new ArrayList<>();
+ urlToVariantInfos.put(uri, variantInfosForUrl);
+ }
+ variantInfosForUrl.add(
+ new VariantInfo(
+ bitrate, videoGroupId, audioGroupId, subtitlesGroupId, closedCaptionsGroupId));
+ }
+ }
+
+ // TODO: Don't deduplicate variants by URL.
+ ArrayList<Variant> deduplicatedVariants = new ArrayList<>();
+ HashSet<Uri> 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);
+ }
+
+ @Nullable
+ private static Variant getVariantWithAudioGroup(ArrayList<Variant> 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;
+ }
+
+ @Nullable
+ private static Variant getVariantWithVideoGroup(ArrayList<Variant> 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<Variant> 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;
+ 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<String, String> variableDefinitions = new HashMap<>();
+ List<Segment> segments = new ArrayList<>();
+ List<String> tags = new ArrayList<>();
+
+ long segmentDurationUs = 0;
+ String segmentTitle = "";
+ boolean hasDiscontinuitySequence = false;
+ int playlistDiscontinuitySequence = 0;
+ int relativeDiscontinuitySequence = 0;
+ long playlistStartTimeUs = 0;
+ long segmentStartTimeUs = 0;
+ long segmentByteRangeOffset = 0;
+ long segmentByteRangeLength = C.LENGTH_UNSET;
+ long segmentMediaSequence = 0;
+ boolean hasGapTag = false;
+
+ DrmInitData playlistProtectionSchemes = null;
+ String fullSegmentEncryptionKeyUri = null;
+ String fullSegmentEncryptionIV = null;
+ TreeMap<String, SchemeData> 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, variableDefinitions);
+ if ("VOD".equals(playlistTypeString)) {
+ playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_VOD;
+ } else if ("EVENT".equals(playlistTypeString)) {
+ playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_EVENT;
+ }
+ } 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, variableDefinitions);
+ String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE, variableDefinitions);
+ if (byteRange != null) {
+ String[] splitByteRange = byteRange.split("@");
+ segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
+ if (splitByteRange.length > 1) {
+ segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
+ }
+ }
+ if (fullSegmentEncryptionKeyUri != null && fullSegmentEncryptionIV == null) {
+ // See RFC 8216, Section 4.3.2.5.
+ throw new ParserException(
+ "The encryption IV attribute must be present when an initialization segment is "
+ + "encrypted with METHOD=AES-128.");
+ }
+ initializationSegment =
+ new Segment(
+ uri,
+ segmentByteRangeOffset,
+ segmentByteRangeLength,
+ fullSegmentEncryptionKeyUri,
+ fullSegmentEncryptionIV);
+ segmentByteRangeOffset = 0;
+ segmentByteRangeLength = C.LENGTH_UNSET;
+ } else if (line.startsWith(TAG_TARGET_DURATION)) {
+ targetDurationUs = parseIntAttr(line, REGEX_TARGET_DURATION) * C.MICROS_PER_SECOND;
+ } else if (line.startsWith(TAG_MEDIA_SEQUENCE)) {
+ mediaSequence = parseLongAttr(line, REGEX_MEDIA_SEQUENCE);
+ segmentMediaSequence = mediaSequence;
+ } else if (line.startsWith(TAG_VERSION)) {
+ version = parseIntAttr(line, REGEX_VERSION);
+ } else if (line.startsWith(TAG_DEFINE)) {
+ String importName = parseOptionalStringAttr(line, REGEX_IMPORT, variableDefinitions);
+ if (importName != null) {
+ String value = masterPlaylist.variableDefinitions.get(importName);
+ if (value != null) {
+ variableDefinitions.put(importName, value);
+ } else {
+ // The master playlist does not declare the imported variable. Ignore.
+ }
+ } else {
+ variableDefinitions.put(
+ parseStringAttr(line, REGEX_NAME, variableDefinitions),
+ parseStringAttr(line, REGEX_VALUE, variableDefinitions));
+ }
+ } else if (line.startsWith(TAG_MEDIA_DURATION)) {
+ segmentDurationUs =
+ (long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND);
+ segmentTitle = parseOptionalStringAttr(line, REGEX_MEDIA_TITLE, "", variableDefinitions);
+ } else if (line.startsWith(TAG_KEY)) {
+ String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);
+ String keyFormat =
+ parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions);
+ fullSegmentEncryptionKeyUri = null;
+ fullSegmentEncryptionIV = null;
+ if (METHOD_NONE.equals(method)) {
+ currentSchemeDatas.clear();
+ cachedDrmInitData = null;
+ } else /* !METHOD_NONE.equals(method) */ {
+ fullSegmentEncryptionIV = parseOptionalStringAttr(line, REGEX_IV, variableDefinitions);
+ if (KEYFORMAT_IDENTITY.equals(keyFormat)) {
+ if (METHOD_AES_128.equals(method)) {
+ // The segment is fully encrypted using an identity key.
+ fullSegmentEncryptionKeyUri = parseStringAttr(line, REGEX_URI, variableDefinitions);
+ } else {
+ // Do nothing. Samples are encrypted using an identity key, but this is not supported.
+ // Hopefully, a traditional DRM alternative is also provided.
+ }
+ } else {
+ if (encryptionScheme == null) {
+ encryptionScheme = parseEncryptionScheme(method);
+ }
+ SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions);
+ if (schemeData != null) {
+ cachedDrmInitData = null;
+ currentSchemeDatas.put(keyFormat, schemeData);
+ }
+ }
+ }
+ } else if (line.startsWith(TAG_BYTERANGE)) {
+ String byteRange = parseStringAttr(line, REGEX_BYTERANGE, variableDefinitions);
+ String[] splitByteRange = byteRange.split("@");
+ segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
+ if (splitByteRange.length > 1) {
+ segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
+ }
+ } else if (line.startsWith(TAG_DISCONTINUITY_SEQUENCE)) {
+ hasDiscontinuitySequence = true;
+ playlistDiscontinuitySequence = Integer.parseInt(line.substring(line.indexOf(':') + 1));
+ } else if (line.equals(TAG_DISCONTINUITY)) {
+ relativeDiscontinuitySequence++;
+ } else if (line.startsWith(TAG_PROGRAM_DATE_TIME)) {
+ if (playlistStartTimeUs == 0) {
+ long programDatetimeUs =
+ C.msToUs(Util.parseXsDateTime(line.substring(line.indexOf(':') + 1)));
+ playlistStartTimeUs = programDatetimeUs - segmentStartTimeUs;
+ }
+ } else if (line.equals(TAG_GAP)) {
+ hasGapTag = true;
+ } else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) {
+ hasIndependentSegmentsTag = true;
+ } else if (line.equals(TAG_ENDLIST)) {
+ hasEndTag = true;
+ } else if (!line.startsWith("#")) {
+ String segmentEncryptionIV;
+ if (fullSegmentEncryptionKeyUri == null) {
+ segmentEncryptionIV = null;
+ } else if (fullSegmentEncryptionIV != null) {
+ segmentEncryptionIV = fullSegmentEncryptionIV;
+ } else {
+ segmentEncryptionIV = Long.toHexString(segmentMediaSequence);
+ }
+
+ segmentMediaSequence++;
+ if (segmentByteRangeLength == C.LENGTH_UNSET) {
+ segmentByteRangeOffset = 0;
+ }
+
+ if (cachedDrmInitData == null && !currentSchemeDatas.isEmpty()) {
+ SchemeData[] schemeDatas = currentSchemeDatas.values().toArray(new SchemeData[0]);
+ cachedDrmInitData = new DrmInitData(encryptionScheme, schemeDatas);
+ if (playlistProtectionSchemes == null) {
+ SchemeData[] playlistSchemeDatas = new SchemeData[schemeDatas.length];
+ for (int i = 0; i < schemeDatas.length; i++) {
+ playlistSchemeDatas[i] = schemeDatas[i].copyWithData(null);
+ }
+ playlistProtectionSchemes = new DrmInitData(encryptionScheme, playlistSchemeDatas);
+ }
+ }
+
+ segments.add(
+ new Segment(
+ replaceVariableReferences(line, variableDefinitions),
+ initializationSegment,
+ segmentTitle,
+ segmentDurationUs,
+ relativeDiscontinuitySequence,
+ segmentStartTimeUs,
+ cachedDrmInitData,
+ fullSegmentEncryptionKeyUri,
+ segmentEncryptionIV,
+ segmentByteRangeOffset,
+ segmentByteRangeLength,
+ hasGapTag));
+ segmentStartTimeUs += segmentDurationUs;
+ segmentDurationUs = 0;
+ segmentTitle = "";
+ if (segmentByteRangeLength != C.LENGTH_UNSET) {
+ segmentByteRangeOffset += segmentByteRangeLength;
+ }
+ segmentByteRangeLength = C.LENGTH_UNSET;
+ hasGapTag = false;
+ }
+ }
+ return new HlsMediaPlaylist(
+ playlistType,
+ baseUri,
+ tags,
+ startOffsetUs,
+ playlistStartTimeUs,
+ hasDiscontinuitySequence,
+ playlistDiscontinuitySequence,
+ mediaSequence,
+ version,
+ targetDurationUs,
+ hasIndependentSegmentsTag,
+ hasEndTag,
+ /* hasProgramDateTime= */ playlistStartTimeUs != 0,
+ playlistProtectionSchemes,
+ segments);
+ }
+
+ @C.SelectionFlags
+ private static int parseSelectionFlags(String line) {
+ int flags = 0;
+ if (parseOptionalBooleanAttribute(line, REGEX_DEFAULT, false)) {
+ flags |= C.SELECTION_FLAG_DEFAULT;
+ }
+ if (parseOptionalBooleanAttribute(line, REGEX_FORCED, false)) {
+ flags |= C.SELECTION_FLAG_FORCED;
+ }
+ if (parseOptionalBooleanAttribute(line, REGEX_AUTOSELECT, false)) {
+ flags |= C.SELECTION_FLAG_AUTOSELECT;
+ }
+ return flags;
+ }
+
+ @C.RoleFlags
+ private static int parseRoleFlags(String line, Map<String, String> 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;
+ }
+
+ @Nullable
+ private static SchemeData parseDrmSchemeData(
+ String line, String keyFormat, Map<String, String> 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 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<String, String> 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<String, String> variableDefinitions) {
+ return parseOptionalStringAttr(line, pattern, null, variableDefinitions);
+ }
+
+ private static @PolyNull String parseOptionalStringAttr(
+ String line,
+ Pattern pattern,
+ @PolyNull String defaultValue,
+ Map<String, String> 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<String, String> 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);
+ }
+ return defaultValue;
+ }
+
+ private static Pattern compileBooleanAttrPattern(String attribute) {
+ return Pattern.compile(attribute + "=(" + BOOLEAN_FALSE + "|" + BOOLEAN_TRUE + ")");
+ }
+
+ private static class LineIterator {
+
+ private final BufferedReader reader;
+ private final Queue<String> extraLines;
+
+ @Nullable private String next;
+
+ public LineIterator(Queue<String> 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 = Assertions.checkNotNull(extraLines.poll());
+ return true;
+ }
+ while ((next = reader.readLine()) != null) {
+ next = next.trim();
+ if (!next.isEmpty()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Return the next line, or throw {@link NoSuchElementException} if none. */
+ public String next() throws IOException {
+ if (hasNext()) {
+ String result = next;
+ next = null;
+ return result;
+ } else {
+ throw new NoSuchElementException();
+ }
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParserFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParserFactory.java
new file mode 100644
index 0000000000..deb1daf8a7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParserFactory.java
@@ -0,0 +1,38 @@
+/*
+ * 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;
+
+/** Factory for {@link HlsPlaylist} parsers. */
+public interface HlsPlaylistParserFactory {
+
+ /**
+ * Returns a stand-alone playlist parser. Playlists parsed by the returned parser do not inherit
+ * any attributes from other playlists.
+ */
+ ParsingLoadable.Parser<HlsPlaylist> 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<HlsPlaylist> createPlaylistParser(HlsMasterPlaylist masterPlaylist);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java
new file mode 100644
index 0000000000..69f8cb02c9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java
@@ -0,0 +1,226 @@
+/*
+ * 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 android.net.Uri;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+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.upstream.LoadErrorHandlingPolicy;
+import java.io.IOException;
+
+/**
+ * Tracks playlists associated to an HLS stream and provides snapshots.
+ *
+ * <p>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).
+ *
+ * <p>Playlist loads might encounter errors. The tracker may choose to blacklist them to ensure a
+ * primary playlist is always available.
+ */
+public interface HlsPlaylistTracker {
+
+ /** 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.
+ *
+ * @param mediaPlaylist The primary playlist new snapshot.
+ */
+ void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist);
+ }
+
+ /** Called on playlist loading events. */
+ interface PlaylistEventListener {
+
+ /**
+ * Called a playlist changes.
+ */
+ void onPlaylistChanged();
+
+ /**
+ * Called if an error is encountered while loading a playlist.
+ *
+ * @param url The loaded url that caused the 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.
+ */
+ boolean onPlaylistError(Uri url, long blacklistDurationMs);
+ }
+
+ /** Thrown when a playlist is considered to be stuck due to a server side error. */
+ final class PlaylistStuckException extends IOException {
+
+ /** The url of the stuck playlist. */
+ public final Uri url;
+
+ /**
+ * Creates an instance.
+ *
+ * @param url See {@link #url}.
+ */
+ public PlaylistStuckException(Uri url) {
+ this.url = url;
+ }
+ }
+
+ /** Thrown when the media sequence of a new snapshot indicates the server has reset. */
+ final class PlaylistResetException extends IOException {
+
+ /** The url of the reset playlist. */
+ public final Uri url;
+
+ /**
+ * Creates an instance.
+ *
+ * @param url See {@link #url}.
+ */
+ public PlaylistResetException(Uri url) {
+ this.url = url;
+ }
+ }
+
+ /**
+ * Starts the playlist tracker.
+ *
+ * <p>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 eventDispatcher A dispatcher to notify of events.
+ * @param listener A callback for the primary playlist change events.
+ */
+ void start(
+ Uri initialPlaylistUri, EventDispatcher eventDispatcher, PrimaryPlaylistListener listener);
+
+ /**
+ * Stops the playlist tracker and releases any acquired resources.
+ *
+ * <p>Must be called once per {@link #start} call.
+ */
+ void stop();
+
+ /**
+ * Registers a listener to receive events from the playlist tracker.
+ *
+ * @param listener The listener.
+ */
+ void addListener(PlaylistEventListener listener);
+
+ /**
+ * Unregisters a listener.
+ *
+ * @param listener The listener to unregister.
+ */
+ void removeListener(PlaylistEventListener listener);
+
+ /**
+ * Returns the master playlist.
+ *
+ * <p>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.
+ */
+ @Nullable
+ HlsMasterPlaylist getMasterPlaylist();
+
+ /**
+ * Returns the most recent snapshot available of the playlist referenced by the provided {@link
+ * Uri}.
+ *
+ * @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.
+ */
+ @Nullable
+ HlsMediaPlaylist getPlaylistSnapshot(Uri url, boolean isForPlayback);
+
+ /**
+ * 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 Uri}.
+ * @return Whether the snapshot of the playlist referenced by the provided {@link Uri} is valid.
+ */
+ boolean isSnapshotValid(Uri url);
+
+ /**
+ * 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.
+ */
+ void maybeThrowPrimaryPlaylistRefreshError() throws IOException;
+
+ /**
+ * 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 Uri}.
+ * @throws IOException The underyling error.
+ */
+ void maybeThrowPlaylistRefreshError(Uri url) throws IOException;
+
+ /**
+ * Requests a playlist refresh and whitelists it.
+ *
+ * <p>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.
+ */
+ boolean isLive();
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/source/hls/playlist/package-info.java
new file mode 100644
index 0000000000..be9f862644
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/CaptionStyleCompat.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/CaptionStyleCompat.java
new file mode 100644
index 0000000000..c9acc1c8f5
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/CaptionStyleCompat.java
@@ -0,0 +1,184 @@
+/*
+ * 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.text;
+
+import android.annotation.TargetApi;
+import android.graphics.Color;
+import android.graphics.Typeface;
+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;
+
+/**
+ * A compatibility wrapper for {@link CaptionStyle}.
+ */
+public final class CaptionStyleCompat {
+
+ /**
+ * 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
+ })
+ public @interface EdgeType {}
+ /**
+ * Edge type value specifying no character edges.
+ */
+ public static final int EDGE_TYPE_NONE = 0;
+ /**
+ * Edge type value specifying uniformly outlined character edges.
+ */
+ public static final int EDGE_TYPE_OUTLINE = 1;
+ /**
+ * Edge type value specifying drop-shadowed character edges.
+ */
+ public static final int EDGE_TYPE_DROP_SHADOW = 2;
+ /**
+ * Edge type value specifying raised bevel character edges.
+ */
+ public static final int EDGE_TYPE_RAISED = 3;
+ /**
+ * Edge type value specifying depressed bevel character edges.
+ */
+ public static final int EDGE_TYPE_DEPRESSED = 4;
+
+ /**
+ * Use color setting specified by the track and fallback to default caption style.
+ */
+ 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,
+ /* typeface= */ null);
+
+ /**
+ * The preferred foreground color.
+ */
+ public final int foregroundColor;
+
+ /**
+ * The preferred background color.
+ */
+ public final int backgroundColor;
+
+ /**
+ * The preferred window color.
+ */
+ public final int windowColor;
+
+ /**
+ * The preferred edge type. One of:
+ * <ul>
+ * <li>{@link #EDGE_TYPE_NONE}
+ * <li>{@link #EDGE_TYPE_OUTLINE}
+ * <li>{@link #EDGE_TYPE_DROP_SHADOW}
+ * <li>{@link #EDGE_TYPE_RAISED}
+ * <li>{@link #EDGE_TYPE_DEPRESSED}
+ * </ul>
+ */
+ @EdgeType public final int edgeType;
+
+ /**
+ * The preferred edge color, if using an edge type other than {@link #EDGE_TYPE_NONE}.
+ */
+ public final int edgeColor;
+
+ /** The preferred typeface, or {@code null} if unspecified. */
+ @Nullable public final Typeface typeface;
+
+ /**
+ * Creates a {@link CaptionStyleCompat} equivalent to a provided {@link CaptionStyle}.
+ *
+ * @param captionStyle A {@link CaptionStyle}.
+ * @return The equivalent {@link CaptionStyleCompat}.
+ */
+ @TargetApi(19)
+ public static CaptionStyleCompat createFromCaptionStyle(
+ CaptioningManager.CaptionStyle captionStyle) {
+ if (Util.SDK_INT >= 21) {
+ return createFromCaptionStyleV21(captionStyle);
+ } else {
+ // Note - Any caller must be on at least API level 19 or greater (because CaptionStyle did
+ // not exist in earlier API levels).
+ return createFromCaptionStyleV19(captionStyle);
+ }
+ }
+
+ /**
+ * @param foregroundColor See {@link #foregroundColor}.
+ * @param backgroundColor See {@link #backgroundColor}.
+ * @param windowColor See {@link #windowColor}.
+ * @param edgeType See {@link #edgeType}.
+ * @param edgeColor See {@link #edgeColor}.
+ * @param typeface See {@link #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;
+ this.edgeType = edgeType;
+ this.edgeColor = edgeColor;
+ this.typeface = typeface;
+ }
+
+ @TargetApi(19)
+ @SuppressWarnings("ResourceType")
+ private static CaptionStyleCompat createFromCaptionStyleV19(
+ CaptioningManager.CaptionStyle captionStyle) {
+ return new CaptionStyleCompat(
+ captionStyle.foregroundColor, captionStyle.backgroundColor, Color.TRANSPARENT,
+ captionStyle.edgeType, captionStyle.edgeColor, captionStyle.getTypeface());
+ }
+
+ @TargetApi(21)
+ @SuppressWarnings("ResourceType")
+ private static CaptionStyleCompat createFromCaptionStyleV21(
+ CaptioningManager.CaptionStyle captionStyle) {
+ return new CaptionStyleCompat(
+ captionStyle.hasForegroundColor() ? captionStyle.foregroundColor : DEFAULT.foregroundColor,
+ captionStyle.hasBackgroundColor() ? captionStyle.backgroundColor : DEFAULT.backgroundColor,
+ captionStyle.hasWindowColor() ? captionStyle.windowColor : DEFAULT.windowColor,
+ captionStyle.hasEdgeType() ? captionStyle.edgeType : DEFAULT.edgeType,
+ captionStyle.hasEdgeColor() ? captionStyle.edgeColor : DEFAULT.edgeColor,
+ captionStyle.getTypeface());
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/Cue.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/Cue.java
new file mode 100644
index 0000000000..71627781c1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/Cue.java
@@ -0,0 +1,435 @@
+/*
+ * 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.text;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+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;
+
+/**
+ * Contains information about a specific cue, including textual content and formatting data.
+ */
+public class Cue {
+
+ /** 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. 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 {}
+
+ /**
+ * An unset anchor or line type value.
+ */
+ public static final int TYPE_UNSET = Integer.MIN_VALUE;
+
+ /**
+ * Anchors the left (for horizontal positions) or top (for vertical positions) edge of the cue
+ * box.
+ */
+ public static final int ANCHOR_TYPE_START = 0;
+
+ /**
+ * Anchors the middle of the cue box.
+ */
+ public static final int ANCHOR_TYPE_MIDDLE = 1;
+
+ /**
+ * Anchors the right (for horizontal positions) or bottom (for vertical positions) edge of the cue
+ * box.
+ */
+ public static final int ANCHOR_TYPE_END = 2;
+
+ /**
+ * 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 {}
+
+ /**
+ * Value for {@link #lineType} when {@link #line} is a fractional position.
+ */
+ public static final int LINE_TYPE_FRACTION = 0;
+
+ /**
+ * Value for {@link #lineType} when {@link #line} is a line number.
+ */
+ 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.
+ */
+ @Nullable public final CharSequence text;
+
+ /** 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. */
+ @Nullable public final Bitmap bitmap;
+
+ /**
+ * The position of the {@link #lineAnchor} of the cue box within the viewport in the direction
+ * orthogonal to the writing direction, or {@link #DIMEN_UNSET}. When set, the interpretation of
+ * the value depends on the value of {@link #lineType}.
+ * <p>
+ * For horizontal text and {@link #lineType} equal to {@link #LINE_TYPE_FRACTION}, this is the
+ * fractional vertical position relative to the top of the viewport.
+ */
+ public final float line;
+
+ /**
+ * The type of the {@link #line} value.
+ *
+ * <p>{@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the
+ * viewport.
+ *
+ * <p>{@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.
+ *
+ * <p>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.
+ */
+ 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}.
+ *
+ * <p>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.
+ */
+ public final @AnchorType int lineAnchor;
+
+ /**
+ * The fractional position of the {@link #positionAnchor} of the cue box within the viewport in
+ * the direction orthogonal to {@link #line}, or {@link #DIMEN_UNSET}.
+ * <p>
+ * For horizontal text, this is the horizontal position relative to the left of the viewport. Note
+ * that positioning is relative to the left of the viewport even in the case of right-to-left
+ * text.
+ */
+ 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}.
+ *
+ * <p>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.
+ */
+ public final @AnchorType int positionAnchor;
+
+ /**
+ * The size of the cue box in the writing direction specified as a fraction of the viewport size
+ * in that direction, or {@link #DIMEN_UNSET}.
+ */
+ public final float size;
+
+ /**
+ * The bitmap height as a fraction of the of the viewport size, or {@link #DIMEN_UNSET} if the
+ * bitmap should be displayed at its natural height given the bitmap dimensions and the specified
+ * {@link #size}.
+ */
+ public final float bitmapHeight;
+
+ /**
+ * Specifies whether or not the {@link #windowColor} property is set.
+ */
+ public final boolean windowColorSet;
+
+ /**
+ * The fill color of the window.
+ */
+ 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.
+ *
+ * @param bitmap See {@link #bitmap}.
+ * @param horizontalPosition The position of the horizontal anchor within the viewport, expressed
+ * as a fraction of the viewport width.
+ * @param horizontalPositionAnchor The horizontal anchor. One of {@link #ANCHOR_TYPE_START},
+ * {@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 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}.
+ */
+ 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);
+ }
+
+ /**
+ * Creates a text cue whose {@link #textAlignment} is null, whose type parameters are set to
+ * {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}.
+ *
+ * @param text See {@link #text}.
+ */
+ public Cue(CharSequence text) {
+ this(
+ text,
+ /* textAlignment= */ null,
+ /* line= */ DIMEN_UNSET,
+ /* lineType= */ TYPE_UNSET,
+ /* lineAnchor= */ TYPE_UNSET,
+ /* position= */ DIMEN_UNSET,
+ /* positionAnchor= */ TYPE_UNSET,
+ /* size= */ DIMEN_UNSET);
+ }
+
+ /**
+ * 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}.
+ */
+ 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);
+ }
+
+ /**
+ * 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 windowColorSet See {@link #windowColorSet}.
+ * @param windowColor See {@link #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(
+ @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;
+ this.bitmap = bitmap;
+ this.line = line;
+ this.lineType = lineType;
+ this.lineAnchor = lineAnchor;
+ this.position = position;
+ this.positionAnchor = positionAnchor;
+ this.size = size;
+ this.bitmapHeight = bitmapHeight;
+ this.windowColorSet = windowColorSet;
+ this.windowColor = windowColor;
+ this.textSizeType = textSizeType;
+ this.textSize = textSize;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java
new file mode 100644
index 0000000000..b58bb1daea
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java
@@ -0,0 +1,100 @@
+/*
+ * 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.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;
+
+/**
+ * Base class for subtitle parsers that use their own decode thread.
+ */
+public abstract class SimpleSubtitleDecoder extends
+ SimpleDecoder<SubtitleInputBuffer, SubtitleOutputBuffer, SubtitleDecoderException> implements
+ SubtitleDecoder {
+
+ private final String name;
+
+ /** @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;
+ setInitialInputBufferSize(1024);
+ }
+
+ @Override
+ public final String getName() {
+ return name;
+ }
+
+ @Override
+ public void setPositionUs(long timeUs) {
+ // Do nothing
+ }
+
+ @Override
+ protected final SubtitleInputBuffer createInputBuffer() {
+ return new SubtitleInputBuffer();
+ }
+
+ @Override
+ protected final SubtitleOutputBuffer createOutputBuffer() {
+ 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
+ @Nullable
+ protected final SubtitleDecoderException decode(
+ SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) {
+ try {
+ 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]).
+ outputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY);
+ return null;
+ } catch (SubtitleDecoderException e) {
+ return e;
+ }
+ }
+
+ /**
+ * Decodes data into a {@link Subtitle}.
+ *
+ * @param data An array holding the data to be decoded, starting at position 0.
+ * @param size The size of the data to be decoded.
+ * @param reset Whether the decoder must be reset before decoding.
+ * @return The decoded {@link Subtitle}.
+ * @throws SubtitleDecoderException If a decoding error occurs.
+ */
+ protected abstract Subtitle decode(byte[] data, int size, boolean reset)
+ throws SubtitleDecoderException;
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java
new file mode 100644
index 0000000000..794b6c72f4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java
@@ -0,0 +1,38 @@
+/*
+ * 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.text;
+
+/**
+ * A {@link SubtitleOutputBuffer} for decoders that extend {@link SimpleSubtitleDecoder}.
+ */
+/* package */ final class SimpleSubtitleOutputBuffer extends SubtitleOutputBuffer {
+
+ private final SimpleSubtitleDecoder owner;
+
+ /**
+ * @param owner The decoder that owns this buffer.
+ */
+ public SimpleSubtitleOutputBuffer(SimpleSubtitleDecoder owner) {
+ super();
+ this.owner = owner;
+ }
+
+ @Override
+ public final void release() {
+ owner.releaseOutputBuffer(this);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/Subtitle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/Subtitle.java
new file mode 100644
index 0000000000..0c2a259f37
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/Subtitle.java
@@ -0,0 +1,59 @@
+/*
+ * 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.text;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.util.List;
+
+/**
+ * A subtitle consisting of timed {@link Cue}s.
+ */
+public interface Subtitle {
+
+ /**
+ * Returns the index of the first event that occurs after a given time (exclusive).
+ *
+ * @param timeUs The time in microseconds.
+ * @return The index of the next event, or {@link C#INDEX_UNSET} if there are no events after the
+ * specified time.
+ */
+ int getNextEventTimeIndex(long timeUs);
+
+ /**
+ * Returns the number of event times, where events are defined as points in time at which the cues
+ * returned by {@link #getCues(long)} changes.
+ *
+ * @return The number of event times.
+ */
+ int getEventTimeCount();
+
+ /**
+ * Returns the event time at a specified index.
+ *
+ * @param index The index of the event time to obtain.
+ * @return The event time in microseconds.
+ */
+ long getEventTime(int index);
+
+ /**
+ * Retrieve the cues that should be displayed at a given time.
+ *
+ * @param timeUs The time in microseconds.
+ * @return A list of cues that should be displayed, possibly empty.
+ */
+ List<Cue> getCues(long timeUs);
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoder.java
new file mode 100644
index 0000000000..dcf1a0c254
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoder.java
@@ -0,0 +1,35 @@
+/*
+ * 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.text;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.Decoder;
+
+/**
+ * Decodes {@link Subtitle}s from {@link SubtitleInputBuffer}s.
+ */
+public interface SubtitleDecoder extends
+ Decoder<SubtitleInputBuffer, SubtitleOutputBuffer, SubtitleDecoderException> {
+
+ /**
+ * Informs the decoder of the current playback position.
+ * <p>
+ * Must be called prior to each attempt to dequeue output buffers from the decoder.
+ *
+ * @param positionUs The current playback position in microseconds.
+ */
+ void setPositionUs(long positionUs);
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderException.java
new file mode 100644
index 0000000000..9ee15188b0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderException.java
@@ -0,0 +1,43 @@
+/*
+ * 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.text;
+
+/**
+ * Thrown when an error occurs decoding subtitle data.
+ */
+public class SubtitleDecoderException extends Exception {
+
+ /**
+ * @param message The detail message for this exception.
+ */
+ public SubtitleDecoderException(String message) {
+ 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.
+ */
+ public SubtitleDecoderException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
new file mode 100644
index 0000000000..2fb0200f0d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
@@ -0,0 +1,126 @@
+/*
+ * 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.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;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt.WebvttDecoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+
+/**
+ * A factory for {@link SubtitleDecoder} instances.
+ */
+public interface SubtitleDecoderFactory {
+
+ /**
+ * Returns whether the factory is able to instantiate a {@link SubtitleDecoder} for the given
+ * {@link Format}.
+ *
+ * @param format The {@link Format}.
+ * @return Whether the factory can instantiate a suitable {@link SubtitleDecoder}.
+ */
+ boolean supportsFormat(Format format);
+
+ /**
+ * Creates a {@link SubtitleDecoder} for the given {@link Format}.
+ *
+ * @param format The {@link Format}.
+ * @return A new {@link SubtitleDecoder}.
+ * @throws IllegalArgumentException If the {@link Format} is not supported.
+ */
+ SubtitleDecoder createDecoder(Format format);
+
+ /**
+ * Default {@link SubtitleDecoderFactory} implementation.
+ *
+ * <p>The formats supported by this factory are:
+ *
+ * <ul>
+ * <li>WebVTT ({@link WebvttDecoder})
+ * <li>WebVTT (MP4) ({@link Mp4WebvttDecoder})
+ * <li>TTML ({@link TtmlDecoder})
+ * <li>SubRip ({@link SubripDecoder})
+ * <li>SSA/ASS ({@link SsaDecoder})
+ * <li>TX3G ({@link Tx3gDecoder})
+ * <li>Cea608 ({@link Cea608Decoder})
+ * <li>Cea708 ({@link Cea708Decoder})
+ * <li>DVB ({@link DvbDecoder})
+ * <li>PGS ({@link PgsDecoder})
+ * </ul>
+ */
+ SubtitleDecoderFactory DEFAULT =
+ new SubtitleDecoderFactory() {
+
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleInputBuffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleInputBuffer.java
new file mode 100644
index 0000000000..dbcfe649b8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleInputBuffer.java
@@ -0,0 +1,34 @@
+/*
+ * 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.text;
+
+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 class SubtitleInputBuffer extends DecoderInputBuffer {
+
+ /**
+ * An offset that must be added to the subtitle's event times after it's been decoded, or
+ * {@link Format#OFFSET_SAMPLE_RELATIVE} if {@link #timeUs} should be added.
+ */
+ public long subsampleOffsetUs;
+
+ public SubtitleInputBuffer() {
+ super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java
new file mode 100644
index 0000000000..9cc7671b24
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java
@@ -0,0 +1,77 @@
+/*
+ * 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.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;
+
+/**
+ * Base class for {@link SubtitleDecoder} output buffers.
+ */
+public abstract class SubtitleOutputBuffer extends OutputBuffer implements Subtitle {
+
+ @Nullable private Subtitle subtitle;
+ private long subsampleOffsetUs;
+
+ /**
+ * Sets the content of the output buffer, consisting of a {@link Subtitle} and associated
+ * metadata.
+ *
+ * @param timeUs The time of the start of the subtitle in microseconds.
+ * @param subtitle The subtitle.
+ * @param subsampleOffsetUs An offset that must be added to the subtitle's event times, or
+ * {@link Format#OFFSET_SAMPLE_RELATIVE} if {@code timeUs} should be added.
+ */
+ public void setContent(long timeUs, Subtitle subtitle, long subsampleOffsetUs) {
+ this.timeUs = timeUs;
+ this.subtitle = subtitle;
+ this.subsampleOffsetUs = subsampleOffsetUs == Format.OFFSET_SAMPLE_RELATIVE ? this.timeUs
+ : subsampleOffsetUs;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return Assertions.checkNotNull(subtitle).getEventTimeCount();
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ return Assertions.checkNotNull(subtitle).getEventTime(index) + subsampleOffsetUs;
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ return Assertions.checkNotNull(subtitle).getNextEventTimeIndex(timeUs - subsampleOffsetUs);
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ return Assertions.checkNotNull(subtitle).getCues(timeUs - subsampleOffsetUs);
+ }
+
+ @Override
+ public abstract void release();
+
+ @Override
+ public void clear() {
+ super.clear();
+ subtitle = null;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/TextOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/TextOutput.java
new file mode 100644
index 0000000000..b15a2f1b35
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/TextOutput.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.text;
+
+import java.util.List;
+
+/**
+ * Receives text output.
+ */
+public interface TextOutput {
+
+ /**
+ * Called when there is a change in the {@link Cue}s.
+ *
+ * @param cues The {@link Cue}s. May be empty.
+ */
+ void onCues(List<Cue> cues);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/TextRenderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/TextRenderer.java
new file mode 100644
index 0000000000..428b106fcd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/TextRenderer.java
@@ -0,0 +1,350 @@
+/*
+ * 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.text;
+
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.Looper;
+import android.os.Message;
+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.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;
+import java.util.List;
+
+/**
+ * A renderer for text.
+ * <p>
+ * {@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 a {@link TextOutput}.
+ */
+public final class TextRenderer extends BaseRenderer implements Callback {
+
+ 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
+ })
+ private @interface ReplacementState {}
+ /**
+ * The decoder does not need to be replaced.
+ */
+ private static final int REPLACEMENT_STATE_NONE = 0;
+ /**
+ * The decoder needs to be replaced, 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 REPLACEMENT_STATE_SIGNAL_END_OF_STREAM = 1;
+ /**
+ * The decoder needs to be replaced, 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 REPLACEMENT_STATE_WAIT_END_OF_STREAM = 2;
+
+ private static final int MSG_UPDATE_OUTPUT = 0;
+
+ @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;
+ @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.
+ */
+ 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 decoderFactory A factory from which to obtain {@link SubtitleDecoder} instances.
+ */
+ public TextRenderer(
+ TextOutput output, @Nullable Looper outputLooper, SubtitleDecoderFactory decoderFactory) {
+ super(C.TRACK_TYPE_TEXT);
+ this.output = Assertions.checkNotNull(output);
+ this.outputHandler =
+ outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this);
+ this.decoderFactory = decoderFactory;
+ formatHolder = new FormatHolder();
+ }
+
+ @Override
+ @Capabilities
+ public int supportsFormat(Format format) {
+ 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, long offsetUs) {
+ streamFormat = formats[0];
+ if (decoder != null) {
+ decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM;
+ } else {
+ decoder = decoderFactory.createDecoder(streamFormat);
+ }
+ }
+
+ @Override
+ protected void onPositionReset(long positionUs, boolean joining) {
+ inputStreamEnded = false;
+ outputStreamEnded = false;
+ resetOutputAndDecoder();
+ }
+
+ @Override
+ public void render(long positionUs, long elapsedRealtimeUs) {
+ if (outputStreamEnded) {
+ return;
+ }
+
+ if (nextSubtitle == null) {
+ decoder.setPositionUs(positionUs);
+ try {
+ nextSubtitle = decoder.dequeueOutputBuffer();
+ } catch (SubtitleDecoderException e) {
+ handleDecoderError(e);
+ return;
+ }
+ }
+
+ if (getState() != STATE_STARTED) {
+ return;
+ }
+
+ boolean textRendererNeedsUpdate = false;
+ if (subtitle != null) {
+ // We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we
+ // advance to the next event.
+ long subtitleNextEventTimeUs = getNextEventTime();
+ while (subtitleNextEventTimeUs <= positionUs) {
+ nextSubtitleEventIndex++;
+ subtitleNextEventTimeUs = getNextEventTime();
+ textRendererNeedsUpdate = true;
+ }
+ }
+
+ if (nextSubtitle != null) {
+ if (nextSubtitle.isEndOfStream()) {
+ if (!textRendererNeedsUpdate && getNextEventTime() == Long.MAX_VALUE) {
+ if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
+ replaceDecoder();
+ } else {
+ releaseBuffers();
+ outputStreamEnded = true;
+ }
+ }
+ } else if (nextSubtitle.timeUs <= positionUs) {
+ // Advance to the next subtitle. Sync the next event index and trigger an update.
+ if (subtitle != null) {
+ subtitle.release();
+ }
+ subtitle = nextSubtitle;
+ nextSubtitle = null;
+ nextSubtitleEventIndex = subtitle.getNextEventTimeIndex(positionUs);
+ textRendererNeedsUpdate = true;
+ }
+ }
+
+ if (textRendererNeedsUpdate) {
+ // textRendererNeedsUpdate is set and we're playing. Update the renderer.
+ updateOutput(subtitle.getCues(positionUs));
+ }
+
+ if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
+ return;
+ }
+
+ try {
+ while (!inputStreamEnded) {
+ if (nextInputBuffer == null) {
+ nextInputBuffer = decoder.dequeueInputBuffer();
+ if (nextInputBuffer == null) {
+ return;
+ }
+ }
+ if (decoderReplacementState == REPLACEMENT_STATE_SIGNAL_END_OF_STREAM) {
+ nextInputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ decoder.queueInputBuffer(nextInputBuffer);
+ nextInputBuffer = null;
+ decoderReplacementState = REPLACEMENT_STATE_WAIT_END_OF_STREAM;
+ return;
+ }
+ // Try and read the next subtitle from the source.
+ int result = readSource(formatHolder, nextInputBuffer, false);
+ if (result == C.RESULT_BUFFER_READ) {
+ if (nextInputBuffer.isEndOfStream()) {
+ inputStreamEnded = true;
+ } else {
+ nextInputBuffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs;
+ nextInputBuffer.flip();
+ }
+ decoder.queueInputBuffer(nextInputBuffer);
+ nextInputBuffer = null;
+ } else if (result == C.RESULT_NOTHING_READ) {
+ return;
+ }
+ }
+ } catch (SubtitleDecoderException e) {
+ handleDecoderError(e);
+ return;
+ }
+ }
+
+ @Override
+ protected void onDisabled() {
+ streamFormat = null;
+ clearOutput();
+ releaseDecoder();
+ }
+
+ @Override
+ public boolean isEnded() {
+ return outputStreamEnded;
+ }
+
+ @Override
+ public boolean isReady() {
+ // Don't block playback whilst subtitles are loading.
+ // Note: To change this behavior, it will be necessary to consider [Internal: b/12949941].
+ return true;
+ }
+
+ private void releaseBuffers() {
+ nextInputBuffer = null;
+ nextSubtitleEventIndex = C.INDEX_UNSET;
+ if (subtitle != null) {
+ subtitle.release();
+ subtitle = null;
+ }
+ if (nextSubtitle != null) {
+ nextSubtitle.release();
+ nextSubtitle = null;
+ }
+ }
+
+ private void releaseDecoder() {
+ releaseBuffers();
+ decoder.release();
+ decoder = null;
+ decoderReplacementState = REPLACEMENT_STATE_NONE;
+ }
+
+ private void replaceDecoder() {
+ releaseDecoder();
+ decoder = decoderFactory.createDecoder(streamFormat);
+ }
+
+ private long getNextEventTime() {
+ return nextSubtitleEventIndex == C.INDEX_UNSET
+ || nextSubtitleEventIndex >= subtitle.getEventTimeCount()
+ ? Long.MAX_VALUE : subtitle.getEventTime(nextSubtitleEventIndex);
+ }
+
+ private void updateOutput(List<Cue> cues) {
+ if (outputHandler != null) {
+ outputHandler.obtainMessage(MSG_UPDATE_OUTPUT, cues).sendToTarget();
+ } else {
+ invokeUpdateOutputInternal(cues);
+ }
+ }
+
+ private void clearOutput() {
+ updateOutput(Collections.emptyList());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_UPDATE_OUTPUT:
+ invokeUpdateOutputInternal((List<Cue>) msg.obj);
+ return true;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ private void invokeUpdateOutputInternal(List<Cue> cues) {
+ output.onCues(cues);
+ }
+
+ /**
+ * Called when {@link #decoder} throws an exception, so it can be logged and playback can
+ * continue.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
new file mode 100644
index 0000000000..320b4f3f07
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
@@ -0,0 +1,1014 @@
+/*
+ * 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.text.cea;
+
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.text.Layout.Alignment;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+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.Collections;
+import java.util.List;
+
+/**
+ * A {@link SubtitleDecoder} for CEA-608 (also known as "line 21 captions" and "EIA-608").
+ */
+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 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;
+ private static final int CC_MODE_POP_ON = 2;
+ private static final int CC_MODE_PAINT_ON = 3;
+
+ 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[] 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;
+
+ // An implied first byte for packets that are only 2 bytes long, consisting of marker bits
+ // (0b11111) + valid bit (0b1) + NTSC field 1 type bits (0b00).
+ private static final byte CC_IMPLICIT_DATA_HEADER = (byte) 0xFC;
+
+ /**
+ * Command initiating pop-on style captioning. Subsequent data should be loaded into a
+ * non-displayed memory and held there until the {@link #CTRL_END_OF_CAPTION} command is received,
+ * 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.
+ */
+ private static final byte CTRL_ROLL_UP_CAPTIONS_2_ROWS = 0x25;
+ /**
+ * Command initiating roll-up style captioning, with the maximum of 3 rows displayed
+ * simultaneously.
+ */
+ private static final byte CTRL_ROLL_UP_CAPTIONS_3_ROWS = 0x26;
+ /**
+ * Command initiating roll-up style captioning, with the maximum of 4 rows displayed
+ * 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;
+ /**
+ * 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_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;
+
+ /**
+ * 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[] {
+ 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, // ! " # $ % & '
+ 0x28, 0x29, // ( )
+ 0xE1, // 2A: 225 'á' "Latin small letter A with acute"
+ 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, // + , - . /
+ 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 0 1 2 3 4 5 6 7
+ 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, // 8 9 : ; < = > ?
+ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, // @ A B C D E F G
+ 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, // H I J K L M N O
+ 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, // P Q R S T U V W
+ 0x58, 0x59, 0x5A, 0x5B, // X Y Z [
+ 0xE9, // 5C: 233 'é' "Latin small letter E with acute"
+ 0x5D, // ]
+ 0xED, // 5E: 237 'í' "Latin small letter I with acute"
+ 0xF3, // 5F: 243 'ó' "Latin small letter O with acute"
+ 0xFA, // 60: 250 'ú' "Latin small letter U with acute"
+ 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, // a b c d e f g
+ 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, // h i j k l m n o
+ 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, // p q r s t u v w
+ 0x78, 0x79, 0x7A, // x y z
+ 0xE7, // 7B: 231 'ç' "Latin small letter C with cedilla"
+ 0xF7, // 7C: 247 '÷' "Division sign"
+ 0xD1, // 7D: 209 'Ñ' "Latin capital letter N with tilde"
+ 0xF1, // 7E: 241 'ñ' "Latin small letter N with tilde"
+ 0x25A0 // 7F: "Black Square" (NB: 2588 = Full Block)
+ };
+
+ // Special North American 608 CC char set.
+ private static final int[] SPECIAL_CHARACTER_SET = new int[] {
+ 0xAE, // 30: 174 '®' "Registered Sign" - registered trademark symbol
+ 0xB0, // 31: 176 '°' "Degree Sign"
+ 0xBD, // 32: 189 '½' "Vulgar Fraction One Half" (1/2 symbol)
+ 0xBF, // 33: 191 '¿' "Inverted Question Mark"
+ 0x2122, // 34: "Trade Mark Sign" (tm superscript)
+ 0xA2, // 35: 162 '¢' "Cent Sign"
+ 0xA3, // 36: 163 '£' "Pound Sign" - pounds sterling
+ 0x266A, // 37: "Eighth Note" - music note
+ 0xE0, // 38: 224 'à' "Latin small letter A with grave"
+ 0x20, // 39: TRANSPARENT SPACE - for now use ordinary space
+ 0xE8, // 3A: 232 'è' "Latin small letter E with grave"
+ 0xE2, // 3B: 226 'â' "Latin small letter A with circumflex"
+ 0xEA, // 3C: 234 'ê' "Latin small letter E with circumflex"
+ 0xEE, // 3D: 238 'î' "Latin small letter I with circumflex"
+ 0xF4, // 3E: 244 'ô' "Latin small letter O with circumflex"
+ 0xFB // 3F: 251 'û' "Latin small letter U with circumflex"
+ };
+
+ // Extended Spanish/Miscellaneous and French char set.
+ private static final int[] SPECIAL_ES_FR_CHARACTER_SET = new int[] {
+ // Spanish and misc.
+ 0xC1, 0xC9, 0xD3, 0xDA, 0xDC, 0xFC, 0x2018, 0xA1,
+ 0x2A, 0x27, 0x2014, 0xA9, 0x2120, 0x2022, 0x201C, 0x201D,
+ // French.
+ 0xC0, 0xC2, 0xC7, 0xC8, 0xCA, 0xCB, 0xEB, 0xCE,
+ 0xCF, 0xEF, 0xD4, 0xD9, 0xF9, 0xDB, 0xAB, 0xBB
+ };
+
+ //Extended Portuguese and German/Danish char set.
+ private static final int[] SPECIAL_PT_DE_CHARACTER_SET = new int[] {
+ // Portuguese.
+ 0xC3, 0xE3, 0xCD, 0xCC, 0xEC, 0xD2, 0xF2, 0xD5,
+ 0xF5, 0x7B, 0x7D, 0x5C, 0x5E, 0x5F, 0x7C, 0x7E,
+ // German/Danish.
+ 0xC4, 0xE4, 0xD6, 0xF6, 0xDF, 0xA5, 0xA4, 0x2502,
+ 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 int selectedChannel;
+ private final ArrayList<CueBuilder> cueBuilders;
+
+ private CueBuilder currentCueBuilder;
+ private List<Cue> cues;
+ private List<Cue> lastCues;
+
+ 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 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 1:
+ selectedChannel = NTSC_CC_CHANNEL_1;
+ selectedField = NTSC_CC_FIELD_1;
+ break;
+ case 2:
+ 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:
+ 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
+ public String getName() {
+ return "Cea608Decoder";
+ }
+
+ @Override
+ public void flush() {
+ super.flush();
+ cues = null;
+ lastCues = null;
+ setCaptionMode(CC_MODE_UNKNOWN);
+ setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT);
+ resetCueBuilders();
+ isCaptionValid = false;
+ repeatableControlSet = false;
+ repeatableControlCc1 = 0;
+ repeatableControlCc2 = 0;
+ currentChannel = NTSC_CC_CHANNEL_1;
+ isInCaptionService = true;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ protected boolean isNewSubtitleDataAvailable() {
+ return cues != lastCues;
+ }
+
+ @Override
+ protected Subtitle createSubtitle() {
+ lastCues = cues;
+ return new CeaSubtitle(cues);
+ }
+
+ @SuppressWarnings("ByteBufferBackingArray")
+ @Override
+ protected void decode(SubtitleInputBuffer inputBuffer) {
+ ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit());
+ boolean captionDataProcessed = false;
+ while (ccData.bytesLeft() >= packetLength) {
+ byte ccHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER
+ : (byte) ccData.readUnsignedByte();
+ int ccByte1 = ccData.readUnsignedByte();
+ int ccByte2 = ccData.readUnsignedByte();
+
+ // 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 ((ccHeader & CC_TYPE_FLAG) != 0) {
+ // Do not process anything that is not part of the 608 byte stream.
+ continue;
+ }
+
+ if ((ccHeader & CC_FIELD_FLAG) != selectedField) {
+ // Do not process packets not within the selected field.
+ continue;
+ }
+
+ // 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;
+ }
+
+ boolean previousIsCaptionValid = isCaptionValid;
+ isCaptionValid =
+ (ccHeader & CC_VALID_FLAG) == CC_VALID_FLAG
+ && ODD_PARITY_BYTE_TABLE[ccByte1]
+ && ODD_PARITY_BYTE_TABLE[ccByte2];
+
+ if (isRepeatedCommand(isCaptionValid, ccData1, ccData2)) {
+ // Ignore repeated valid commands.
+ continue;
+ }
+
+ if (!isCaptionValid) {
+ if (previousIsCaptionValid) {
+ // The encoder has flipped the validity bit to indicate captions are being turned off.
+ resetCueBuilders();
+ captionDataProcessed = true;
+ }
+ continue;
+ }
+
+ maybeUpdateIsInCaptionService(ccData1, ccData2);
+ if (!isInCaptionService) {
+ // Only the Captioning service is supported. Drop all other bytes.
+ continue;
+ }
+
+ 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 (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
+ cues = getDisplayCues();
+ }
+ }
+ }
+
+ private boolean updateAndVerifyCurrentChannel(byte cc1) {
+ if (isCtrlCode(cc1)) {
+ currentChannel = getChannel(cc1);
+ }
+ return currentChannel == selectedChannel;
+ }
+
+ 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 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;
+ }
+ return false;
+ }
+
+ private void handleMidrowCtrl(byte cc2) {
+ // TODO: support the extended styles (i.e. backgrounds and transparencies)
+
+ // A midrow control code advances the cursor.
+ currentCueBuilder.append(' ');
+
+ // 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: support the extended address and style
+
+ // cc2 - 0|1|N|ATTRBTE|U
+ // N is the next row down toggle, ATTRBTE is the 4-byte encoded attribute, and U is the
+ // underline toggle.
+ boolean nextRowDown = (cc2 & 0x20) != 0;
+ if (nextRowDown) {
+ row++;
+ }
+
+ if (row != currentCueBuilder.row) {
+ if (captionMode != CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) {
+ currentCueBuilder = new CueBuilder(captionMode, captionRowCount);
+ cueBuilders.add(currentCueBuilder);
+ }
+ currentCueBuilder.row = row;
+ }
+
+ // cc2 - 0|1|N|0|STYLE|U
+ // cc2 - 0|1|N|1|CURSR|U
+ 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:
+ setCaptionMode(CC_MODE_ROLL_UP);
+ setCaptionRowCount(2);
+ return;
+ case CTRL_ROLL_UP_CAPTIONS_3_ROWS:
+ setCaptionMode(CC_MODE_ROLL_UP);
+ setCaptionRowCount(3);
+ return;
+ case CTRL_ROLL_UP_CAPTIONS_4_ROWS:
+ setCaptionMode(CC_MODE_ROLL_UP);
+ setCaptionRowCount(4);
+ return;
+ case CTRL_RESUME_CAPTION_LOADING:
+ setCaptionMode(CC_MODE_POP_ON);
+ return;
+ case CTRL_RESUME_DIRECT_CAPTIONING:
+ setCaptionMode(CC_MODE_PAINT_ON);
+ return;
+ default:
+ // Fall through.
+ break;
+ }
+
+ if (captionMode == CC_MODE_UNKNOWN) {
+ return;
+ }
+
+ switch (cc2) {
+ case CTRL_ERASE_DISPLAYED_MEMORY:
+ cues = Collections.emptyList();
+ if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
+ resetCueBuilders();
+ }
+ break;
+ case CTRL_ERASE_NON_DISPLAYED_MEMORY:
+ resetCueBuilders();
+ break;
+ case CTRL_END_OF_CAPTION:
+ cues = getDisplayCues();
+ resetCueBuilders();
+ break;
+ case CTRL_CARRIAGE_RETURN:
+ // carriage returns only apply to rollup captions; don't bother if we don't have anything
+ // to add a carriage return to
+ if (captionMode == CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) {
+ currentCueBuilder.rollUp();
+ }
+ break;
+ case CTRL_BACKSPACE:
+ currentCueBuilder.backspace();
+ break;
+ case CTRL_DELETE_TO_END_OF_ROW:
+ // TODO: implement
+ break;
+ default:
+ // Fall through.
+ break;
+ }
+ }
+
+ private List<Cue> getDisplayCues() {
+ // 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<Cue> 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<Cue> 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;
+ }
+
+ private void setCaptionMode(int captionMode) {
+ if (this.captionMode == captionMode) {
+ return;
+ }
+
+ 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 = Collections.emptyList();
+ }
+ }
+
+ private void setCaptionRowCount(int captionRowCount) {
+ this.captionRowCount = captionRowCount;
+ currentCueBuilder.setCaptionRowCount(captionRowCount);
+ }
+
+ private void resetCueBuilders() {
+ currentCueBuilder.reset(captionMode);
+ cueBuilders.clear();
+ cueBuilders.add(currentCueBuilder);
+ }
+
+ 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 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];
+ }
+
+ private static char getExtendedPtDeChar(byte ccData) {
+ int index = ccData & 0x1F;
+ 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
+ return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x20);
+ }
+
+ private static boolean isPreambleAddressCode(byte cc1, byte cc2) {
+ // cc1 - 0|0|0|1|C|X|X|X
+ // cc2 - 0|1|X|X|X|X|X|X
+ return ((cc1 & 0xF0) == 0x10) && ((cc2 & 0xC0) == 0x40);
+ }
+
+ private static boolean isTabCtrlCode(byte cc1, byte cc2) {
+ // cc1 - 0|0|0|1|C|1|1|1
+ // cc2 - 0|0|1|0|0|0|0|1 to 0|0|1|0|0|0|1|1
+ return ((cc1 & 0xF7) == 0x17) && (cc2 >= 0x21 && cc2 <= 0x23);
+ }
+
+ private static boolean isMiscCode(byte cc1, byte cc2) {
+ // cc1 - 0|0|0|1|C|1|0|F
+ // cc2 - 0|0|1|0|X|X|X|X
+ return ((cc1 & 0xF6) == 0x14) && ((cc2 & 0xF0) == 0x20);
+ }
+
+ private static boolean isRepeatable(byte cc1) {
+ // cc1 - 0|0|0|1|X|X|X|X
+ return (cc1 & 0xF0) == 0x10;
+ }
+
+ private static boolean isXdsControlCode(byte cc1) {
+ return 0x01 <= cc1 && cc1 <= 0x0F;
+ }
+
+ 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<CueStyle> cueStyles;
+ private final List<SpannableString> rolledUpCaptions;
+ private final StringBuilder captionStringBuilder;
+
+ private int row;
+ private int indent;
+ private int tabOffset;
+ private int captionMode;
+ private int captionRowCount;
+
+ public CueBuilder(int captionMode, int captionRowCount) {
+ cueStyles = new ArrayList<>();
+ rolledUpCaptions = new ArrayList<>();
+ captionStringBuilder = new StringBuilder();
+ reset(captionMode);
+ setCaptionRowCount(captionRowCount);
+ }
+
+ public void reset(int captionMode) {
+ this.captionMode = captionMode;
+ cueStyles.clear();
+ rolledUpCaptions.clear();
+ captionStringBuilder.setLength(0);
+ row = BASE_ROW;
+ indent = 0;
+ tabOffset = 0;
+ }
+
+ public boolean 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);
+ // 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;
+ }
+ }
+ }
+ }
+
+ public void append(char text) {
+ captionStringBuilder.append(text);
+ }
+
+ public void rollUp() {
+ rolledUpCaptions.add(buildCurrentLine());
+ captionStringBuilder.setLength(0);
+ cueStyles.clear();
+ int numRows = Math.min(captionRowCount, row);
+ while (rolledUpCaptions.size() >= numRows) {
+ rolledUpCaptions.remove(0);
+ }
+ }
+
+ 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++) {
+ cueString.append(rolledUpCaptions.get(i));
+ cueString.append('\n');
+ }
+ // Add the current line.
+ cueString.append(buildCurrentLine());
+
+ if (cueString.length() == 0) {
+ // The cue is empty.
+ return null;
+ }
+
+ 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 (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.
+ positionAnchor = Cue.ANCHOR_TYPE_END;
+ } else {
+ // For all other cases assume start aligned.
+ 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].
+ if (captionMode == CC_MODE_ROLL_UP || row > (BASE_ROW / 2)) {
+ lineAnchor = Cue.ANCHOR_TYPE_END;
+ line = row - BASE_ROW;
+ // Two line adjustments. The first is because line indices from the bottom of the window
+ // start from -1 rather than 0. The second is a blank row to act as the safe area.
+ line -= 2;
+ } else {
+ lineAnchor = Cue.ANCHOR_TYPE_START;
+ // Line indices from the top of the window start from 0, but we want a blank row to act as
+ // the safe area. As a result no adjustment is necessary.
+ line = row;
+ }
+
+ return new Cue(
+ cueString,
+ Alignment.ALIGN_NORMAL,
+ line,
+ Cue.LINE_TYPE_NUMBER,
+ lineAnchor,
+ position,
+ positionAnchor,
+ Cue.DIMEN_UNSET);
+ }
+
+ 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 int style;
+ public final boolean underline;
+
+ public int start;
+
+ public CueStyle(int style, boolean underline, int start) {
+ this.style = style;
+ this.underline = underline;
+ this.start = start;
+ }
+
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Cue.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Cue.java
new file mode 100644
index 0000000000..268b6baec0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Cue.java
@@ -0,0 +1,62 @@
+/*
+ * 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.text.cea;
+
+import android.text.Layout.Alignment;
+import androidx.annotation.NonNull;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue;
+
+/**
+ * A {@link Cue} for CEA-708.
+ */
+/* package */ final class Cea708Cue extends Cue implements Comparable<Cea708Cue> {
+
+ /**
+ * The priority of the cue box.
+ */
+ public final int priority;
+
+ /**
+ * @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 windowColorSet See {@link #windowColorSet}.
+ * @param windowColor See {@link #windowColor}.
+ * @param priority See (@link #priority}.
+ */
+ public Cea708Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType,
+ @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size,
+ boolean windowColorSet, int windowColor, int priority) {
+ super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size,
+ windowColorSet, windowColor);
+ this.priority = priority;
+ }
+
+ @Override
+ public int compareTo(@NonNull Cea708Cue other) {
+ if (other.priority < priority) {
+ return -1;
+ } else if (other.priority > priority) {
+ return 1;
+ }
+ return 0;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Decoder.java
new file mode 100644
index 0000000000..c8af0ed350
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Decoder.java
@@ -0,0 +1,1255 @@
+/*
+ * 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.text.cea;
+
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.text.Layout.Alignment;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+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;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue.AnchorType;
+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.List;
+
+/**
+ * A {@link SubtitleDecoder} for CEA-708 (also known as "EIA-708").
+ */
+public final class Cea708Decoder extends CeaDecoder {
+
+ private static final String TAG = "Cea708Decoder";
+
+ private static final int NUM_WINDOWS = 8;
+
+ private static final int DTVCC_PACKET_DATA = 0x02;
+ private static final int DTVCC_PACKET_START = 0x03;
+ private static final int CC_VALID_FLAG = 0x04;
+
+ // Base Commands
+ private static final int GROUP_C0_END = 0x1F; // Miscellaneous Control Codes
+ private static final int GROUP_G0_END = 0x7F; // ASCII Printable Characters
+ private static final int GROUP_C1_END = 0x9F; // Captioning Command Control Codes
+ private static final int GROUP_G1_END = 0xFF; // ISO 8859-1 LATIN-1 Character Set
+
+ // Extended Commands
+ private static final int GROUP_C2_END = 0x1F; // Extended Control Code Set 1
+ private static final int GROUP_G2_END = 0x7F; // Extended Miscellaneous Characters
+ private static final int GROUP_C3_END = 0x9F; // Extended Control Code Set 2
+ private static final int GROUP_G3_END = 0xFF; // Future Expansion
+
+ // Group C0 Commands
+ private static final int COMMAND_NUL = 0x00; // Nul
+ private static final int COMMAND_ETX = 0x03; // EndOfText
+ private static final int COMMAND_BS = 0x08; // Backspace
+ private static final int COMMAND_FF = 0x0C; // FormFeed (Flush)
+ private static final int COMMAND_CR = 0x0D; // CarriageReturn
+ private static final int COMMAND_HCR = 0x0E; // ClearLine
+ private static final int COMMAND_EXT1 = 0x10; // Extended Control Code Flag
+ private static final int COMMAND_EXT1_START = 0x11;
+ private static final int COMMAND_EXT1_END = 0x17;
+ private static final int COMMAND_P16_START = 0x18;
+ private static final int COMMAND_P16_END = 0x1F;
+
+ // Group C1 Commands
+ private static final int COMMAND_CW0 = 0x80; // SetCurrentWindow to 0
+ private static final int COMMAND_CW1 = 0x81; // SetCurrentWindow to 1
+ private static final int COMMAND_CW2 = 0x82; // SetCurrentWindow to 2
+ private static final int COMMAND_CW3 = 0x83; // SetCurrentWindow to 3
+ private static final int COMMAND_CW4 = 0x84; // SetCurrentWindow to 4
+ private static final int COMMAND_CW5 = 0x85; // SetCurrentWindow to 5
+ private static final int COMMAND_CW6 = 0x86; // SetCurrentWindow to 6
+ private static final int COMMAND_CW7 = 0x87; // SetCurrentWindow to 7
+ private static final int COMMAND_CLW = 0x88; // ClearWindows (+1 byte)
+ private static final int COMMAND_DSW = 0x89; // DisplayWindows (+1 byte)
+ private static final int COMMAND_HDW = 0x8A; // HideWindows (+1 byte)
+ private static final int COMMAND_TGW = 0x8B; // ToggleWindows (+1 byte)
+ private static final int COMMAND_DLW = 0x8C; // DeleteWindows (+1 byte)
+ private static final int COMMAND_DLY = 0x8D; // Delay (+1 byte)
+ private static final int COMMAND_DLC = 0x8E; // DelayCancel
+ private static final int COMMAND_RST = 0x8F; // Reset
+ private static final int COMMAND_SPA = 0x90; // SetPenAttributes (+2 bytes)
+ private static final int COMMAND_SPC = 0x91; // SetPenColor (+3 bytes)
+ private static final int COMMAND_SPL = 0x92; // SetPenLocation (+2 bytes)
+ private static final int COMMAND_SWA = 0x97; // SetWindowAttributes (+4 bytes)
+ private static final int COMMAND_DF0 = 0x98; // DefineWindow 0 (+6 bytes)
+ 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_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)
+
+ // G0 Table Special Chars
+ private static final int CHARACTER_MN = 0x7F; // MusicNote
+
+ // G2 Table Special Chars
+ private static final int CHARACTER_TSP = 0x20;
+ private static final int CHARACTER_NBTSP = 0x21;
+ private static final int CHARACTER_ELLIPSIS = 0x25;
+ private static final int CHARACTER_BIG_CARONS = 0x2A;
+ private static final int CHARACTER_BIG_OE = 0x2C;
+ private static final int CHARACTER_SOLID_BLOCK = 0x30;
+ private static final int CHARACTER_OPEN_SINGLE_QUOTE = 0x31;
+ private static final int CHARACTER_CLOSE_SINGLE_QUOTE = 0x32;
+ private static final int CHARACTER_OPEN_DOUBLE_QUOTE = 0x33;
+ private static final int CHARACTER_CLOSE_DOUBLE_QUOTE = 0x34;
+ private static final int CHARACTER_BOLD_BULLET = 0x35;
+ private static final int CHARACTER_TM = 0x39;
+ private static final int CHARACTER_SMALL_CARONS = 0x3A;
+ private static final int CHARACTER_SMALL_OE = 0x3C;
+ private static final int CHARACTER_SM = 0x3D;
+ private static final int CHARACTER_DIAERESIS_Y = 0x3F;
+ private static final int CHARACTER_ONE_EIGHTH = 0x76;
+ private static final int CHARACTER_THREE_EIGHTHS = 0x77;
+ private static final int CHARACTER_FIVE_EIGHTHS = 0x78;
+ private static final int CHARACTER_SEVEN_EIGHTHS = 0x79;
+ private static final int CHARACTER_VERTICAL_BORDER = 0x7A;
+ private static final int CHARACTER_UPPER_RIGHT_BORDER = 0x7B;
+ private static final int CHARACTER_LOWER_LEFT_BORDER = 0x7C;
+ private static final int CHARACTER_HORIZONTAL_BORDER = 0x7D;
+ private static final int CHARACTER_LOWER_RIGHT_BORDER = 0x7E;
+ private static final int CHARACTER_UPPER_LEFT_BORDER = 0x7F;
+
+ private final ParsableByteArray ccData;
+ private final ParsableBitArray serviceBlockPacket;
+
+ private final int selectedServiceNumber;
+ private final CueBuilder[] cueBuilders;
+
+ private CueBuilder currentCueBuilder;
+ private List<Cue> cues;
+ private List<Cue> lastCues;
+
+ private DtvCcPacket currentDtvCcPacket;
+ private int currentWindow;
+
+ // TODO: Retrieve isWideAspectRatio from initializationData and use it.
+ public Cea708Decoder(int accessibilityChannel, @Nullable List<byte[]> initializationData) {
+ ccData = new ParsableByteArray();
+ serviceBlockPacket = new ParsableBitArray();
+ selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel;
+
+ cueBuilders = new CueBuilder[NUM_WINDOWS];
+ for (int i = 0; i < NUM_WINDOWS; i++) {
+ cueBuilders[i] = new CueBuilder();
+ }
+
+ currentCueBuilder = cueBuilders[0];
+ resetCueBuilders();
+ }
+
+ @Override
+ public String getName() {
+ return "Cea708Decoder";
+ }
+
+ @Override
+ public void flush() {
+ super.flush();
+ cues = null;
+ lastCues = null;
+ currentWindow = 0;
+ currentCueBuilder = cueBuilders[currentWindow];
+ resetCueBuilders();
+ currentDtvCcPacket = null;
+ }
+
+ @Override
+ protected boolean isNewSubtitleDataAvailable() {
+ return cues != lastCues;
+ }
+
+ @Override
+ protected Subtitle createSubtitle() {
+ lastCues = cues;
+ return new CeaSubtitle(cues);
+ }
+
+ @Override
+ protected void decode(SubtitleInputBuffer inputBuffer) {
+ // 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);
+
+ int ccType = ccTypeAndValid & (DTVCC_PACKET_DATA | DTVCC_PACKET_START);
+ boolean ccValid = (ccTypeAndValid & CC_VALID_FLAG) == CC_VALID_FLAG;
+ byte ccData1 = (byte) ccData.readUnsignedByte();
+ byte ccData2 = (byte) ccData.readUnsignedByte();
+
+ // Ignore any non-CEA-708 data
+ if (ccType != DTVCC_PACKET_DATA && ccType != DTVCC_PACKET_START) {
+ continue;
+ }
+
+ if (!ccValid) {
+ // This byte-pair isn't valid, ignore it and continue.
+ continue;
+ }
+
+ if (ccType == DTVCC_PACKET_START) {
+ finalizeCurrentPacket();
+
+ int sequenceNumber = (ccData1 & 0xC0) >> 6; // first 2 bits
+ int packetSize = ccData1 & 0x3F; // last 6 bits
+ if (packetSize == 0) {
+ packetSize = 64;
+ }
+
+ currentDtvCcPacket = new DtvCcPacket(sequenceNumber, packetSize);
+ currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2;
+ } else {
+ // The only remaining valid packet type is DTVCC_PACKET_DATA
+ Assertions.checkArgument(ccType == DTVCC_PACKET_DATA);
+
+ if (currentDtvCcPacket == null) {
+ Log.e(TAG, "Encountered DTVCC_PACKET_DATA before DTVCC_PACKET_START");
+ continue;
+ }
+
+ currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData1;
+ currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2;
+ }
+
+ if (currentDtvCcPacket.currentIndex == (currentDtvCcPacket.packetSize * 2 - 1)) {
+ finalizeCurrentPacket();
+ }
+ }
+ }
+
+ private void finalizeCurrentPacket() {
+ if (currentDtvCcPacket == null) {
+ // No packet to finalize;
+ return;
+ }
+
+ processCurrentPacket();
+ currentDtvCcPacket = null;
+ }
+
+ private void processCurrentPacket() {
+ if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) {
+ Log.w(TAG, "DtvCcPacket ended prematurely; size is " + (currentDtvCcPacket.packetSize * 2 - 1)
+ + ", but current index is " + currentDtvCcPacket.currentIndex + " (sequence number "
+ + currentDtvCcPacket.sequenceNumber + "); ignoring packet");
+ return;
+ }
+
+ serviceBlockPacket.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex);
+
+ int serviceNumber = serviceBlockPacket.readBits(3);
+ int blockSize = serviceBlockPacket.readBits(5);
+ if (serviceNumber == 7) {
+ // extended service numbers
+ serviceBlockPacket.skipBits(2);
+ serviceNumber = serviceBlockPacket.readBits(6);
+ if (serviceNumber < 7) {
+ Log.w(TAG, "Invalid extended service number: " + serviceNumber);
+ }
+ }
+
+ // Ignore packets in which blockSize is 0
+ if (blockSize == 0) {
+ if (serviceNumber != 0) {
+ Log.w(TAG, "serviceNumber is non-zero (" + serviceNumber + ") when blockSize is 0");
+ }
+ return;
+ }
+
+ if (serviceNumber != selectedServiceNumber) {
+ return;
+ }
+
+ // The cues should be updated if we receive a C0 ETX command, any C1 command, or if after
+ // processing the service block any text has been added to the buffer. See CEA-708-B Section
+ // 8.10.4 for more details.
+ boolean cuesNeedUpdate = false;
+
+ while (serviceBlockPacket.bitsLeft() > 0) {
+ int command = serviceBlockPacket.readBits(8);
+ if (command != COMMAND_EXT1) {
+ if (command <= GROUP_C0_END) {
+ handleC0Command(command);
+ // If the C0 command was an ETX command, the cues are updated in handleC0Command.
+ } else if (command <= GROUP_G0_END) {
+ handleG0Character(command);
+ cuesNeedUpdate = true;
+ } else if (command <= GROUP_C1_END) {
+ handleC1Command(command);
+ cuesNeedUpdate = true;
+ } else if (command <= GROUP_G1_END) {
+ handleG1Character(command);
+ cuesNeedUpdate = true;
+ } else {
+ Log.w(TAG, "Invalid base command: " + command);
+ }
+ } else {
+ // Read the extended command
+ command = serviceBlockPacket.readBits(8);
+ if (command <= GROUP_C2_END) {
+ handleC2Command(command);
+ } else if (command <= GROUP_G2_END) {
+ handleG2Character(command);
+ cuesNeedUpdate = true;
+ } else if (command <= GROUP_C3_END) {
+ handleC3Command(command);
+ } else if (command <= GROUP_G3_END) {
+ handleG3Character(command);
+ cuesNeedUpdate = true;
+ } else {
+ Log.w(TAG, "Invalid extended command: " + command);
+ }
+ }
+ }
+
+ if (cuesNeedUpdate) {
+ cues = getDisplayCues();
+ }
+ }
+
+ private void handleC0Command(int command) {
+ switch (command) {
+ case COMMAND_NUL:
+ // Do nothing.
+ break;
+ case COMMAND_ETX:
+ cues = getDisplayCues();
+ break;
+ case COMMAND_BS:
+ currentCueBuilder.backspace();
+ break;
+ case COMMAND_FF:
+ resetCueBuilders();
+ break;
+ case COMMAND_CR:
+ currentCueBuilder.append('\n');
+ break;
+ case COMMAND_HCR:
+ // TODO: Add support for this command.
+ break;
+ default:
+ if (command >= COMMAND_EXT1_START && command <= COMMAND_EXT1_END) {
+ Log.w(TAG, "Currently unsupported COMMAND_EXT1 Command: " + command);
+ serviceBlockPacket.skipBits(8);
+ } else if (command >= COMMAND_P16_START && command <= COMMAND_P16_END) {
+ Log.w(TAG, "Currently unsupported COMMAND_P16 Command: " + command);
+ serviceBlockPacket.skipBits(16);
+ } else {
+ Log.w(TAG, "Invalid C0 command: " + command);
+ }
+ }
+ }
+
+ private void handleC1Command(int command) {
+ int window;
+ switch (command) {
+ case COMMAND_CW0:
+ case COMMAND_CW1:
+ case COMMAND_CW2:
+ case COMMAND_CW3:
+ case COMMAND_CW4:
+ case COMMAND_CW5:
+ case COMMAND_CW6:
+ case COMMAND_CW7:
+ window = (command - COMMAND_CW0);
+ if (currentWindow != window) {
+ currentWindow = window;
+ currentCueBuilder = cueBuilders[window];
+ }
+ break;
+ case COMMAND_CLW:
+ for (int i = 1; i <= NUM_WINDOWS; i++) {
+ if (serviceBlockPacket.readBit()) {
+ cueBuilders[NUM_WINDOWS - i].clear();
+ }
+ }
+ break;
+ case COMMAND_DSW:
+ for (int i = 1; i <= NUM_WINDOWS; i++) {
+ if (serviceBlockPacket.readBit()) {
+ cueBuilders[NUM_WINDOWS - i].setVisibility(true);
+ }
+ }
+ break;
+ case COMMAND_HDW:
+ for (int i = 1; i <= NUM_WINDOWS; i++) {
+ if (serviceBlockPacket.readBit()) {
+ cueBuilders[NUM_WINDOWS - i].setVisibility(false);
+ }
+ }
+ break;
+ case COMMAND_TGW:
+ for (int i = 1; i <= NUM_WINDOWS; i++) {
+ if (serviceBlockPacket.readBit()) {
+ CueBuilder cueBuilder = cueBuilders[NUM_WINDOWS - i];
+ cueBuilder.setVisibility(!cueBuilder.isVisible());
+ }
+ }
+ break;
+ case COMMAND_DLW:
+ for (int i = 1; i <= NUM_WINDOWS; i++) {
+ if (serviceBlockPacket.readBit()) {
+ cueBuilders[NUM_WINDOWS - i].reset();
+ }
+ }
+ break;
+ case COMMAND_DLY:
+ // TODO: Add support for delay commands.
+ serviceBlockPacket.skipBits(8);
+ break;
+ case COMMAND_DLC:
+ // TODO: Add support for delay commands.
+ break;
+ case COMMAND_RST:
+ resetCueBuilders();
+ break;
+ case COMMAND_SPA:
+ if (!currentCueBuilder.isDefined()) {
+ // ignore this command if the current window/cue isn't defined
+ serviceBlockPacket.skipBits(16);
+ } else {
+ handleSetPenAttributes();
+ }
+ break;
+ case COMMAND_SPC:
+ if (!currentCueBuilder.isDefined()) {
+ // ignore this command if the current window/cue isn't defined
+ serviceBlockPacket.skipBits(24);
+ } else {
+ handleSetPenColor();
+ }
+ break;
+ case COMMAND_SPL:
+ if (!currentCueBuilder.isDefined()) {
+ // ignore this command if the current window/cue isn't defined
+ serviceBlockPacket.skipBits(16);
+ } else {
+ handleSetPenLocation();
+ }
+ break;
+ case COMMAND_SWA:
+ if (!currentCueBuilder.isDefined()) {
+ // ignore this command if the current window/cue isn't defined
+ serviceBlockPacket.skipBits(32);
+ } else {
+ handleSetWindowAttributes();
+ }
+ break;
+ case COMMAND_DF0:
+ case COMMAND_DF1:
+ case COMMAND_DF2:
+ case COMMAND_DF3:
+ case COMMAND_DF4:
+ case COMMAND_DF5:
+ case COMMAND_DF6:
+ case COMMAND_DF7:
+ window = (command - COMMAND_DF0);
+ handleDefineWindow(window);
+ // We also set the current window to the newly defined window.
+ if (currentWindow != window) {
+ currentWindow = window;
+ currentCueBuilder = cueBuilders[window];
+ }
+ break;
+ default:
+ Log.w(TAG, "Invalid C1 command: " + command);
+ }
+ }
+
+ private void handleC2Command(int command) {
+ // C2 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes
+ if (command <= 0x07) {
+ // Do nothing.
+ } else if (command <= 0x0F) {
+ serviceBlockPacket.skipBits(8);
+ } else if (command <= 0x17) {
+ serviceBlockPacket.skipBits(16);
+ } else if (command <= 0x1F) {
+ serviceBlockPacket.skipBits(24);
+ }
+ }
+
+ private void handleC3Command(int command) {
+ // C3 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes
+ if (command <= 0x87) {
+ serviceBlockPacket.skipBits(32);
+ } else if (command <= 0x8F) {
+ serviceBlockPacket.skipBits(40);
+ } else if (command <= 0x9F) {
+ // 90-9F are variable length codes; the first byte defines the header with the first
+ // 2 bits specifying the type and the last 6 bits specifying the remaining length of the
+ // command in bytes
+ serviceBlockPacket.skipBits(2);
+ int length = serviceBlockPacket.readBits(6);
+ serviceBlockPacket.skipBits(8 * length);
+ }
+ }
+
+ private void handleG0Character(int characterCode) {
+ if (characterCode == CHARACTER_MN) {
+ currentCueBuilder.append('\u266B');
+ } else {
+ currentCueBuilder.append((char) (characterCode & 0xFF));
+ }
+ }
+
+ private void handleG1Character(int characterCode) {
+ currentCueBuilder.append((char) (characterCode & 0xFF));
+ }
+
+ private void handleG2Character(int characterCode) {
+ switch (characterCode) {
+ case CHARACTER_TSP:
+ currentCueBuilder.append('\u0020');
+ break;
+ case CHARACTER_NBTSP:
+ currentCueBuilder.append('\u00A0');
+ break;
+ case CHARACTER_ELLIPSIS:
+ currentCueBuilder.append('\u2026');
+ break;
+ case CHARACTER_BIG_CARONS:
+ currentCueBuilder.append('\u0160');
+ break;
+ case CHARACTER_BIG_OE:
+ currentCueBuilder.append('\u0152');
+ break;
+ case CHARACTER_SOLID_BLOCK:
+ currentCueBuilder.append('\u2588');
+ break;
+ case CHARACTER_OPEN_SINGLE_QUOTE:
+ currentCueBuilder.append('\u2018');
+ break;
+ case CHARACTER_CLOSE_SINGLE_QUOTE:
+ currentCueBuilder.append('\u2019');
+ break;
+ case CHARACTER_OPEN_DOUBLE_QUOTE:
+ currentCueBuilder.append('\u201C');
+ break;
+ case CHARACTER_CLOSE_DOUBLE_QUOTE:
+ currentCueBuilder.append('\u201D');
+ break;
+ case CHARACTER_BOLD_BULLET:
+ currentCueBuilder.append('\u2022');
+ break;
+ case CHARACTER_TM:
+ currentCueBuilder.append('\u2122');
+ break;
+ case CHARACTER_SMALL_CARONS:
+ currentCueBuilder.append('\u0161');
+ break;
+ case CHARACTER_SMALL_OE:
+ currentCueBuilder.append('\u0153');
+ break;
+ case CHARACTER_SM:
+ currentCueBuilder.append('\u2120');
+ break;
+ case CHARACTER_DIAERESIS_Y:
+ currentCueBuilder.append('\u0178');
+ break;
+ case CHARACTER_ONE_EIGHTH:
+ currentCueBuilder.append('\u215B');
+ break;
+ case CHARACTER_THREE_EIGHTHS:
+ currentCueBuilder.append('\u215C');
+ break;
+ case CHARACTER_FIVE_EIGHTHS:
+ currentCueBuilder.append('\u215D');
+ break;
+ case CHARACTER_SEVEN_EIGHTHS:
+ currentCueBuilder.append('\u215E');
+ break;
+ case CHARACTER_VERTICAL_BORDER:
+ currentCueBuilder.append('\u2502');
+ break;
+ case CHARACTER_UPPER_RIGHT_BORDER:
+ currentCueBuilder.append('\u2510');
+ break;
+ case CHARACTER_LOWER_LEFT_BORDER:
+ currentCueBuilder.append('\u2514');
+ break;
+ case CHARACTER_HORIZONTAL_BORDER:
+ currentCueBuilder.append('\u2500');
+ break;
+ case CHARACTER_LOWER_RIGHT_BORDER:
+ currentCueBuilder.append('\u2518');
+ break;
+ case CHARACTER_UPPER_LEFT_BORDER:
+ currentCueBuilder.append('\u250C');
+ break;
+ default:
+ Log.w(TAG, "Invalid G2 character: " + characterCode);
+ // The CEA-708 specification doesn't specify what to do in the case of an unexpected
+ // value in the G2 character range, so we ignore it.
+ }
+ }
+
+ private void handleG3Character(int characterCode) {
+ if (characterCode == 0xA0) {
+ currentCueBuilder.append('\u33C4');
+ } else {
+ Log.w(TAG, "Invalid G3 character: " + characterCode);
+ // Substitute any unsupported G3 character with an underscore as per CEA-708 specification.
+ currentCueBuilder.append('_');
+ }
+ }
+
+ private void handleSetPenAttributes() {
+ // the SetPenAttributes command contains 2 bytes of data
+ // first byte
+ int textTag = serviceBlockPacket.readBits(4);
+ int offset = serviceBlockPacket.readBits(2);
+ int penSize = serviceBlockPacket.readBits(2);
+ // second byte
+ boolean italicsToggle = serviceBlockPacket.readBit();
+ boolean underlineToggle = serviceBlockPacket.readBit();
+ int edgeType = serviceBlockPacket.readBits(3);
+ int fontStyle = serviceBlockPacket.readBits(3);
+
+ currentCueBuilder.setPenAttributes(textTag, offset, penSize, italicsToggle, underlineToggle,
+ edgeType, fontStyle);
+ }
+
+ private void handleSetPenColor() {
+ // the SetPenColor command contains 3 bytes of data
+ // first byte
+ int foregroundO = serviceBlockPacket.readBits(2);
+ int foregroundR = serviceBlockPacket.readBits(2);
+ int foregroundG = serviceBlockPacket.readBits(2);
+ int foregroundB = serviceBlockPacket.readBits(2);
+ int foregroundColor = CueBuilder.getArgbColorFromCeaColor(foregroundR, foregroundG, foregroundB,
+ foregroundO);
+ // second byte
+ int backgroundO = serviceBlockPacket.readBits(2);
+ int backgroundR = serviceBlockPacket.readBits(2);
+ int backgroundG = serviceBlockPacket.readBits(2);
+ int backgroundB = serviceBlockPacket.readBits(2);
+ int backgroundColor = CueBuilder.getArgbColorFromCeaColor(backgroundR, backgroundG, backgroundB,
+ backgroundO);
+ // third byte
+ serviceBlockPacket.skipBits(2); // null padding
+ int edgeR = serviceBlockPacket.readBits(2);
+ int edgeG = serviceBlockPacket.readBits(2);
+ int edgeB = serviceBlockPacket.readBits(2);
+ int edgeColor = CueBuilder.getArgbColorFromCeaColor(edgeR, edgeG, edgeB);
+
+ currentCueBuilder.setPenColor(foregroundColor, backgroundColor, edgeColor);
+ }
+
+ private void handleSetPenLocation() {
+ // the SetPenLocation command contains 2 bytes of data
+ // first byte
+ serviceBlockPacket.skipBits(4);
+ int row = serviceBlockPacket.readBits(4);
+ // second byte
+ serviceBlockPacket.skipBits(2);
+ int column = serviceBlockPacket.readBits(6);
+
+ currentCueBuilder.setPenLocation(row, column);
+ }
+
+ private void handleSetWindowAttributes() {
+ // the SetWindowAttributes command contains 4 bytes of data
+ // first byte
+ int fillO = serviceBlockPacket.readBits(2);
+ int fillR = serviceBlockPacket.readBits(2);
+ int fillG = serviceBlockPacket.readBits(2);
+ int fillB = serviceBlockPacket.readBits(2);
+ int fillColor = CueBuilder.getArgbColorFromCeaColor(fillR, fillG, fillB, fillO);
+ // second byte
+ int borderType = serviceBlockPacket.readBits(2); // only the lower 2 bits of borderType
+ int borderR = serviceBlockPacket.readBits(2);
+ int borderG = serviceBlockPacket.readBits(2);
+ int borderB = serviceBlockPacket.readBits(2);
+ int borderColor = CueBuilder.getArgbColorFromCeaColor(borderR, borderG, borderB);
+ // third byte
+ if (serviceBlockPacket.readBit()) {
+ borderType |= 0x04; // set the top bit of the 3-bit borderType
+ }
+ boolean wordWrapToggle = serviceBlockPacket.readBit();
+ int printDirection = serviceBlockPacket.readBits(2);
+ int scrollDirection = serviceBlockPacket.readBits(2);
+ int justification = serviceBlockPacket.readBits(2);
+ // fourth byte
+ // Note that we don't intend to support display effects
+ serviceBlockPacket.skipBits(8); // effectSpeed(4), effectDirection(2), displayEffect(2)
+
+ currentCueBuilder.setWindowAttributes(fillColor, borderColor, wordWrapToggle, borderType,
+ printDirection, scrollDirection, justification);
+ }
+
+ private void handleDefineWindow(int window) {
+ CueBuilder cueBuilder = cueBuilders[window];
+
+ // the DefineWindow command contains 6 bytes of data
+ // first byte
+ serviceBlockPacket.skipBits(2); // null padding
+ boolean visible = serviceBlockPacket.readBit();
+ boolean rowLock = serviceBlockPacket.readBit();
+ boolean columnLock = serviceBlockPacket.readBit();
+ int priority = serviceBlockPacket.readBits(3);
+ // second byte
+ boolean relativePositioning = serviceBlockPacket.readBit();
+ int verticalAnchor = serviceBlockPacket.readBits(7);
+ // third byte
+ int horizontalAnchor = serviceBlockPacket.readBits(8);
+ // fourth byte
+ int anchorId = serviceBlockPacket.readBits(4);
+ int rowCount = serviceBlockPacket.readBits(4);
+ // fifth byte
+ serviceBlockPacket.skipBits(2); // null padding
+ int columnCount = serviceBlockPacket.readBits(6);
+ // sixth byte
+ serviceBlockPacket.skipBits(2); // null padding
+ int windowStyle = serviceBlockPacket.readBits(3);
+ int penStyle = serviceBlockPacket.readBits(3);
+
+ cueBuilder.defineWindow(visible, rowLock, columnLock, priority, relativePositioning,
+ verticalAnchor, horizontalAnchor, rowCount, columnCount, anchorId, windowStyle, penStyle);
+ }
+
+ private List<Cue> getDisplayCues() {
+ List<Cea708Cue> displayCues = new ArrayList<>();
+ for (int i = 0; i < NUM_WINDOWS; i++) {
+ if (!cueBuilders[i].isEmpty() && cueBuilders[i].isVisible()) {
+ displayCues.add(cueBuilders[i].build());
+ }
+ }
+ Collections.sort(displayCues);
+ return Collections.unmodifiableList(displayCues);
+ }
+
+ private void resetCueBuilders() {
+ for (int i = 0; i < NUM_WINDOWS; i++) {
+ cueBuilders[i].reset();
+ }
+ }
+
+ private static final class DtvCcPacket {
+
+ public final int sequenceNumber;
+ public final int packetSize;
+ public final byte[] packetData;
+
+ int currentIndex;
+
+ public DtvCcPacket(int sequenceNumber, int packetSize) {
+ this.sequenceNumber = sequenceNumber;
+ this.packetSize = packetSize;
+ packetData = new byte[2 * packetSize - 1];
+ currentIndex = 0;
+ }
+
+ }
+
+ // TODO: There is a lot of overlap between Cea708Decoder.CueBuilder and Cea608Decoder.CueBuilder
+ // which could be refactored into a separate class.
+ private static final class CueBuilder {
+
+ private static final int RELATIVE_CUE_SIZE = 99;
+ private static final int VERTICAL_SIZE = 74;
+ private static final int HORIZONTAL_SIZE = 209;
+
+ private static final int DEFAULT_PRIORITY = 4;
+
+ private static final int MAXIMUM_ROW_COUNT = 15;
+
+ private static final int JUSTIFICATION_LEFT = 0;
+ private static final int JUSTIFICATION_RIGHT = 1;
+ private static final int JUSTIFICATION_CENTER = 2;
+ private static final int JUSTIFICATION_FULL = 3;
+
+ private static final int DIRECTION_LEFT_TO_RIGHT = 0;
+ private static final int DIRECTION_RIGHT_TO_LEFT = 1;
+ private static final int DIRECTION_TOP_TO_BOTTOM = 2;
+ private static final int DIRECTION_BOTTOM_TO_TOP = 3;
+
+ // TODO: Add other border/edge types when utilized.
+ private static final int BORDER_AND_EDGE_TYPE_NONE = 0;
+ private static final int BORDER_AND_EDGE_TYPE_UNIFORM = 3;
+
+ public static final int COLOR_SOLID_WHITE = getArgbColorFromCeaColor(2, 2, 2, 0);
+ public static final int COLOR_SOLID_BLACK = getArgbColorFromCeaColor(0, 0, 0, 0);
+ public static final int COLOR_TRANSPARENT = getArgbColorFromCeaColor(0, 0, 0, 3);
+
+ // TODO: Add other sizes when utilized.
+ private static final int PEN_SIZE_STANDARD = 1;
+
+ // TODO: Add other pen font styles when utilized.
+ private static final int PEN_FONT_STYLE_DEFAULT = 0;
+ private static final int PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS = 1;
+ private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS = 2;
+ private static final int PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS = 3;
+ private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS = 4;
+
+ // TODO: Add other pen offsets when utilized.
+ 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[] {
+ JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT,
+ JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_CENTER,
+ JUSTIFICATION_LEFT
+ };
+ 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[] {
+ 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[] {
+ false, false, false, true, true, true, false
+ };
+ 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[] {
+ 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[] {
+ 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[] {
+ COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK,
+ COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_TRANSPARENT};
+
+ private final List<SpannableString> rolledUpCaptions;
+ private final SpannableStringBuilder captionStringBuilder;
+
+ // Window/Cue properties
+ private boolean defined;
+ private boolean visible;
+ private int priority;
+ private boolean relativePositioning;
+ private int verticalAnchor;
+ private int horizontalAnchor;
+ private int anchorId;
+ private int rowCount;
+ private boolean rowLock;
+ private int justification;
+ private int windowStyleId;
+ private int penStyleId;
+ private int windowFillColor;
+
+ // Pen/Text properties
+ private int italicsStartPosition;
+ private int underlineStartPosition;
+ private int foregroundColorStartPosition;
+ private int foregroundColor;
+ private int backgroundColorStartPosition;
+ private int backgroundColor;
+ private int row;
+
+ public CueBuilder() {
+ rolledUpCaptions = new ArrayList<>();
+ captionStringBuilder = new SpannableStringBuilder();
+ reset();
+ }
+
+ public boolean isEmpty() {
+ return !isDefined() || (rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0);
+ }
+
+ public void reset() {
+ clear();
+
+ defined = false;
+ visible = false;
+ priority = DEFAULT_PRIORITY;
+ relativePositioning = false;
+ verticalAnchor = 0;
+ horizontalAnchor = 0;
+ anchorId = 0;
+ rowCount = MAXIMUM_ROW_COUNT;
+ rowLock = true;
+ justification = JUSTIFICATION_LEFT;
+ windowStyleId = 0;
+ penStyleId = 0;
+ windowFillColor = COLOR_SOLID_BLACK;
+
+ foregroundColor = COLOR_SOLID_WHITE;
+ backgroundColor = COLOR_SOLID_BLACK;
+ }
+
+ public void clear() {
+ rolledUpCaptions.clear();
+ captionStringBuilder.clear();
+ italicsStartPosition = C.POSITION_UNSET;
+ underlineStartPosition = C.POSITION_UNSET;
+ foregroundColorStartPosition = C.POSITION_UNSET;
+ backgroundColorStartPosition = C.POSITION_UNSET;
+ row = 0;
+ }
+
+ public boolean isDefined() {
+ return defined;
+ }
+
+ public void setVisibility(boolean visible) {
+ this.visible = visible;
+ }
+
+ public boolean isVisible() {
+ return visible;
+ }
+
+ public void defineWindow(boolean visible, boolean rowLock, boolean columnLock, int priority,
+ boolean relativePositioning, int verticalAnchor, int horizontalAnchor, int rowCount,
+ int columnCount, int anchorId, int windowStyleId, int penStyleId) {
+ this.defined = true;
+ this.visible = visible;
+ this.rowLock = rowLock;
+ this.priority = priority;
+ this.relativePositioning = relativePositioning;
+ this.verticalAnchor = verticalAnchor;
+ this.horizontalAnchor = horizontalAnchor;
+ this.anchorId = anchorId;
+
+ // Decoders must add one to rowCount to get the desired number of rows.
+ if (this.rowCount != rowCount + 1) {
+ this.rowCount = rowCount + 1;
+
+ // Trim any rolled up captions that are no longer valid, if applicable.
+ while ((rowLock && (rolledUpCaptions.size() >= this.rowCount))
+ || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) {
+ rolledUpCaptions.remove(0);
+ }
+ }
+
+ // TODO: Add support for column lock and count.
+
+ if (windowStyleId != 0 && this.windowStyleId != windowStyleId) {
+ this.windowStyleId = windowStyleId;
+ // windowStyleId is 1-based.
+ int windowStyleIdIndex = windowStyleId - 1;
+ // Note that Border type and border color are the same for all window styles.
+ setWindowAttributes(WINDOW_STYLE_FILL[windowStyleIdIndex], COLOR_TRANSPARENT,
+ WINDOW_STYLE_WORD_WRAP[windowStyleIdIndex], BORDER_AND_EDGE_TYPE_NONE,
+ WINDOW_STYLE_PRINT_DIRECTION[windowStyleIdIndex],
+ WINDOW_STYLE_SCROLL_DIRECTION[windowStyleIdIndex],
+ WINDOW_STYLE_JUSTIFICATION[windowStyleIdIndex]);
+ }
+
+ if (penStyleId != 0 && this.penStyleId != penStyleId) {
+ this.penStyleId = penStyleId;
+ // penStyleId is 1-based.
+ int penStyleIdIndex = penStyleId - 1;
+ // Note that pen size, offset, italics, underline, foreground color, and foreground
+ // opacity are the same for all pen styles.
+ setPenAttributes(0, PEN_OFFSET_NORMAL, PEN_SIZE_STANDARD, false, false,
+ PEN_STYLE_EDGE_TYPE[penStyleIdIndex], PEN_STYLE_FONT_STYLE[penStyleIdIndex]);
+ setPenColor(COLOR_SOLID_WHITE, PEN_STYLE_BACKGROUND[penStyleIdIndex], COLOR_SOLID_BLACK);
+ }
+ }
+
+
+ public void setWindowAttributes(int fillColor, int borderColor, boolean wordWrapToggle,
+ int borderType, int printDirection, int scrollDirection, int justification) {
+ this.windowFillColor = fillColor;
+ // TODO: Add support for border color and types.
+ // TODO: Add support for word wrap.
+ // TODO: Add support for other scroll directions.
+ // TODO: Add support for other print directions.
+ this.justification = justification;
+
+ }
+
+ public void setPenAttributes(int textTag, int offset, int penSize, boolean italicsToggle,
+ boolean underlineToggle, int edgeType, int fontStyle) {
+ // TODO: Add support for text tags.
+ // TODO: Add support for other offsets.
+ // TODO: Add support for other pen sizes.
+
+ if (italicsStartPosition != C.POSITION_UNSET) {
+ if (!italicsToggle) {
+ captionStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition,
+ captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ italicsStartPosition = C.POSITION_UNSET;
+ }
+ } else if (italicsToggle) {
+ italicsStartPosition = captionStringBuilder.length();
+ }
+
+ if (underlineStartPosition != C.POSITION_UNSET) {
+ if (!underlineToggle) {
+ captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,
+ captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ underlineStartPosition = C.POSITION_UNSET;
+ }
+ } else if (underlineToggle) {
+ underlineStartPosition = captionStringBuilder.length();
+ }
+
+ // TODO: Add support for edge types.
+ // TODO: Add support for other font styles.
+ }
+
+ public void setPenColor(int foregroundColor, int backgroundColor, int edgeColor) {
+ if (foregroundColorStartPosition != C.POSITION_UNSET) {
+ if (this.foregroundColor != foregroundColor) {
+ captionStringBuilder.setSpan(new ForegroundColorSpan(this.foregroundColor),
+ foregroundColorStartPosition, captionStringBuilder.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ if (foregroundColor != COLOR_SOLID_WHITE) {
+ foregroundColorStartPosition = captionStringBuilder.length();
+ this.foregroundColor = foregroundColor;
+ }
+
+ if (backgroundColorStartPosition != C.POSITION_UNSET) {
+ if (this.backgroundColor != backgroundColor) {
+ captionStringBuilder.setSpan(new BackgroundColorSpan(this.backgroundColor),
+ backgroundColorStartPosition, captionStringBuilder.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ if (backgroundColor != COLOR_SOLID_BLACK) {
+ backgroundColorStartPosition = captionStringBuilder.length();
+ this.backgroundColor = backgroundColor;
+ }
+
+ // TODO: Add support for edge color.
+ }
+
+ public void setPenLocation(int row, int column) {
+ // TODO: Support moving the pen location with a window properly.
+
+ // Until we support proper pen locations, if we encounter a row that's different from the
+ // previous one, we should append a new line. Otherwise, we'll see strings that should be
+ // on new lines concatenated with the previous, resulting in 2 words being combined, as
+ // well as potentially drawing beyond the width of the window/screen.
+ if (this.row != row) {
+ append('\n');
+ }
+ this.row = row;
+ }
+
+ public void backspace() {
+ int length = captionStringBuilder.length();
+ if (length > 0) {
+ captionStringBuilder.delete(length - 1, length);
+ }
+ }
+
+ public void append(char text) {
+ if (text == '\n') {
+ rolledUpCaptions.add(buildSpannableString());
+ captionStringBuilder.clear();
+
+ if (italicsStartPosition != C.POSITION_UNSET) {
+ italicsStartPosition = 0;
+ }
+ if (underlineStartPosition != C.POSITION_UNSET) {
+ underlineStartPosition = 0;
+ }
+ if (foregroundColorStartPosition != C.POSITION_UNSET) {
+ foregroundColorStartPosition = 0;
+ }
+ if (backgroundColorStartPosition != C.POSITION_UNSET) {
+ backgroundColorStartPosition = 0;
+ }
+
+ while ((rowLock && (rolledUpCaptions.size() >= rowCount))
+ || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) {
+ rolledUpCaptions.remove(0);
+ }
+ } else {
+ captionStringBuilder.append(text);
+ }
+ }
+
+ public SpannableString buildSpannableString() {
+ SpannableStringBuilder spannableStringBuilder =
+ new SpannableStringBuilder(captionStringBuilder);
+ int length = spannableStringBuilder.length();
+
+ if (length > 0) {
+ if (italicsStartPosition != C.POSITION_UNSET) {
+ spannableStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition,
+ length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ if (underlineStartPosition != C.POSITION_UNSET) {
+ spannableStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,
+ length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ if (foregroundColorStartPosition != C.POSITION_UNSET) {
+ spannableStringBuilder.setSpan(new ForegroundColorSpan(foregroundColor),
+ foregroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ if (backgroundColorStartPosition != C.POSITION_UNSET) {
+ spannableStringBuilder.setSpan(new BackgroundColorSpan(backgroundColor),
+ backgroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ return new SpannableString(spannableStringBuilder);
+ }
+
+ public Cea708Cue build() {
+ if (isEmpty()) {
+ // The cue is empty.
+ return null;
+ }
+
+ SpannableStringBuilder cueString = new SpannableStringBuilder();
+
+ // Add any rolled up captions, separated by new lines.
+ for (int i = 0; i < rolledUpCaptions.size(); i++) {
+ cueString.append(rolledUpCaptions.get(i));
+ cueString.append('\n');
+ }
+ // Add the current line.
+ cueString.append(buildSpannableString());
+
+ // TODO: Add support for right-to-left languages (i.e. where right would correspond to normal
+ // alignment).
+ Alignment alignment;
+ switch (justification) {
+ case JUSTIFICATION_FULL:
+ // TODO: Add support for full justification.
+ case JUSTIFICATION_LEFT:
+ alignment = Alignment.ALIGN_NORMAL;
+ break;
+ case JUSTIFICATION_RIGHT:
+ alignment = Alignment.ALIGN_OPPOSITE;
+ break;
+ case JUSTIFICATION_CENTER:
+ alignment = Alignment.ALIGN_CENTER;
+ break;
+ default:
+ throw new IllegalArgumentException("Unexpected justification value: " + justification);
+ }
+
+ float position;
+ float line;
+ if (relativePositioning) {
+ position = (float) horizontalAnchor / RELATIVE_CUE_SIZE;
+ line = (float) verticalAnchor / RELATIVE_CUE_SIZE;
+ } else {
+ position = (float) horizontalAnchor / HORIZONTAL_SIZE;
+ line = (float) verticalAnchor / VERTICAL_SIZE;
+ }
+ // Apply screen-edge padding to the line and position.
+ position = (position * 0.9f) + 0.05f;
+ line = (line * 0.9f) + 0.05f;
+
+ // anchorId specifies where the anchor should be placed on the caption cue/window. The 9
+ // possible configurations are as follows:
+ // 0-----1-----2
+ // | |
+ // 3 4 5
+ // | |
+ // 6-----7-----8
+ @AnchorType int verticalAnchorType;
+ if (anchorId % 3 == 0) {
+ verticalAnchorType = Cue.ANCHOR_TYPE_START;
+ } else if (anchorId % 3 == 1) {
+ verticalAnchorType = Cue.ANCHOR_TYPE_MIDDLE;
+ } else {
+ verticalAnchorType = Cue.ANCHOR_TYPE_END;
+ }
+ // TODO: Add support for right-to-left languages (i.e. where start is on the right).
+ @AnchorType int horizontalAnchorType;
+ if (anchorId / 3 == 0) {
+ horizontalAnchorType = Cue.ANCHOR_TYPE_START;
+ } else if (anchorId / 3 == 1) {
+ horizontalAnchorType = Cue.ANCHOR_TYPE_MIDDLE;
+ } else {
+ horizontalAnchorType = Cue.ANCHOR_TYPE_END;
+ }
+
+ boolean windowColorSet = (windowFillColor != COLOR_SOLID_BLACK);
+
+ return new Cea708Cue(cueString, alignment, line, Cue.LINE_TYPE_FRACTION, verticalAnchorType,
+ position, horizontalAnchorType, Cue.DIMEN_UNSET, windowColorSet, windowFillColor,
+ priority);
+ }
+
+ public static int getArgbColorFromCeaColor(int red, int green, int blue) {
+ return getArgbColorFromCeaColor(red, green, blue, 0);
+ }
+
+ public static int getArgbColorFromCeaColor(int red, int green, int blue, int opacity) {
+ Assertions.checkIndex(red, 0, 4);
+ Assertions.checkIndex(green, 0, 4);
+ Assertions.checkIndex(blue, 0, 4);
+ Assertions.checkIndex(opacity, 0, 4);
+
+ int alpha;
+ switch (opacity) {
+ case 0:
+ case 1:
+ // Note the value of '1' is actually FLASH, but we don't support that.
+ alpha = 255;
+ break;
+ case 2:
+ alpha = 127;
+ break;
+ case 3:
+ alpha = 0;
+ break;
+ default:
+ alpha = 255;
+ }
+
+ // TODO: Add support for the Alternative Minimum Color List or the full 64 RGB combinations.
+
+ // Return values based on the Minimum Color List
+ return Color.argb(alpha,
+ (red > 1 ? 255 : 0),
+ (green > 1 ? 255 : 0),
+ (blue > 1 ? 255 : 0));
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java
new file mode 100644
index 0000000000..5d63ca8e82
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<byte[]> 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<byte[]> 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<byte[]> buildData(boolean isWideAspectRatio) {
+ return Collections.singletonList(new byte[] {(byte) (isWideAspectRatio ? 1 : 0)});
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaDecoder.java
new file mode 100644
index 0000000000..42fa915fc5
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaDecoder.java
@@ -0,0 +1,204 @@
+/*
+ * 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.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;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleDecoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleDecoderException;
+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.ArrayDeque;
+import java.util.PriorityQueue;
+
+/**
+ * Base class for subtitle parsers for CEA captions.
+ */
+/* package */ abstract class CeaDecoder implements SubtitleDecoder {
+
+ private static final int NUM_INPUT_BUFFERS = 10;
+ private static final int NUM_OUTPUT_BUFFERS = 2;
+
+ private final ArrayDeque<CeaInputBuffer> availableInputBuffers;
+ private final ArrayDeque<SubtitleOutputBuffer> availableOutputBuffers;
+ private final PriorityQueue<CeaInputBuffer> queuedInputBuffers;
+
+ private CeaInputBuffer dequeuedInputBuffer;
+ private long playbackPositionUs;
+ private long queuedInputBufferCount;
+
+ public CeaDecoder() {
+ availableInputBuffers = new ArrayDeque<>();
+ for (int i = 0; i < NUM_INPUT_BUFFERS; i++) {
+ availableInputBuffers.add(new CeaInputBuffer());
+ }
+ availableOutputBuffers = new ArrayDeque<>();
+ for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) {
+ availableOutputBuffers.add(new CeaOutputBuffer());
+ }
+ queuedInputBuffers = new PriorityQueue<>();
+ }
+
+ @Override
+ public abstract String getName();
+
+ @Override
+ public void setPositionUs(long positionUs) {
+ playbackPositionUs = positionUs;
+ }
+
+ @Override
+ public SubtitleInputBuffer dequeueInputBuffer() throws SubtitleDecoderException {
+ Assertions.checkState(dequeuedInputBuffer == null);
+ if (availableInputBuffers.isEmpty()) {
+ return null;
+ }
+ dequeuedInputBuffer = availableInputBuffers.pollFirst();
+ return dequeuedInputBuffer;
+ }
+
+ @Override
+ public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException {
+ 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(dequeuedInputBuffer);
+ } else {
+ dequeuedInputBuffer.queuedInputBufferCount = queuedInputBufferCount++;
+ queuedInputBuffers.add(dequeuedInputBuffer);
+ }
+ dequeuedInputBuffer = null;
+ }
+
+ @Override
+ public SubtitleOutputBuffer dequeueOutputBuffer() throws SubtitleDecoderException {
+ 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.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
+ if (inputBuffer.isEndOfStream()) {
+ SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst();
+ outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
+ releaseInputBuffer(inputBuffer);
+ return outputBuffer;
+ }
+
+ decode(inputBuffer);
+
+ // check if we have any caption updates to report
+ if (isNewSubtitleDataAvailable()) {
+ // Even if the subtitle is decode-only; we need to generate it to consume the data so it
+ // isn't accidentally prepended to the next subtitle
+ Subtitle subtitle = createSubtitle();
+ if (!inputBuffer.isDecodeOnly()) {
+ SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst();
+ outputBuffer.setContent(inputBuffer.timeUs, subtitle, Format.OFFSET_SAMPLE_RELATIVE);
+ releaseInputBuffer(inputBuffer);
+ return outputBuffer;
+ }
+ }
+
+ releaseInputBuffer(inputBuffer);
+ }
+
+ return null;
+ }
+
+ private void releaseInputBuffer(CeaInputBuffer inputBuffer) {
+ inputBuffer.clear();
+ availableInputBuffers.add(inputBuffer);
+ }
+
+ protected void releaseOutputBuffer(SubtitleOutputBuffer outputBuffer) {
+ outputBuffer.clear();
+ availableOutputBuffers.add(outputBuffer);
+ }
+
+ @Override
+ public void flush() {
+ queuedInputBufferCount = 0;
+ playbackPositionUs = 0;
+ while (!queuedInputBuffers.isEmpty()) {
+ releaseInputBuffer(queuedInputBuffers.poll());
+ }
+ if (dequeuedInputBuffer != null) {
+ releaseInputBuffer(dequeuedInputBuffer);
+ dequeuedInputBuffer = null;
+ }
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ /**
+ * Returns whether there is data available to create a new {@link Subtitle}.
+ */
+ protected abstract boolean isNewSubtitleDataAvailable();
+
+ /**
+ * Creates a {@link Subtitle} from the available data.
+ */
+ protected abstract Subtitle createSubtitle();
+
+ /**
+ * Filters and processes the raw data, providing {@link Subtitle}s via {@link #createSubtitle()}
+ * when sufficient data has been processed.
+ */
+ protected abstract void decode(SubtitleInputBuffer inputBuffer);
+
+ private static final class CeaInputBuffer extends SubtitleInputBuffer
+ implements Comparable<CeaInputBuffer> {
+
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaSubtitle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaSubtitle.java
new file mode 100644
index 0000000000..f4649c4c4b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaSubtitle.java
@@ -0,0 +1,60 @@
+/*
+ * 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.text.cea;
+
+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 java.util.Collections;
+import java.util.List;
+
+/**
+ * A representation of a CEA subtitle.
+ */
+/* package */ final class CeaSubtitle implements Subtitle {
+
+ private final List<Cue> cues;
+
+ /**
+ * @param cues The subtitle cues.
+ */
+ public CeaSubtitle(List<Cue> cues) {
+ this.cues = cues;
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ return timeUs < 0 ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return 1;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ Assertions.checkArgument(index == 0);
+ return 0;
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ return timeUs >= 0 ? cues : Collections.emptyList();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaUtil.java
new file mode 100644
index 0000000000..ced169ba17
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaUtil.java
@@ -0,0 +1,138 @@
+/*
+ * 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.cea;
+
+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. 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_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
+ * as samples to all of the provided outputs.
+ *
+ * @param presentationTimeUs The presentation time in microseconds for any samples.
+ * @param seiBuffer The unescaped SEI NAL unit data, excluding the NAL unit start code and type.
+ * @param outputs The outputs to which any samples should be written.
+ */
+ public static void consume(long presentationTimeUs, ParsableByteArray seiBuffer,
+ TrackOutput[] outputs) {
+ 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.");
+ 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);
+ }
+ }
+ 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);
+ }
+ }
+
+ /**
+ * Reads a value from the provided buffer consisting of zero or more 0xFF bytes followed by a
+ * terminating byte not equal to 0xFF. The returned value is ((0xFF * N) + T), where N is the
+ * number of 0xFF bytes and T is the value of the terminating byte.
+ *
+ * @param buffer The buffer from which to read the value.
+ * @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;
+ int value = 0;
+ do {
+ if (buffer.bytesLeft() == 0) {
+ return -1;
+ }
+ b = buffer.readUnsignedByte();
+ value += b;
+ } while (b == 0xFF);
+ return value;
+ }
+
+ private CeaUtil() {}
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/package-info.java
new file mode 100644
index 0000000000..e80d06586a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbDecoder.java
new file mode 100644
index 0000000000..063872ae2e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbDecoder.java
@@ -0,0 +1,49 @@
+/*
+ * 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.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. */
+public final class DvbDecoder extends SimpleSubtitleDecoder {
+
+ private final DvbParser parser;
+
+ /**
+ * @param initializationData The initialization data for the decoder. The initialization data
+ * must consist of a single byte array containing 5 bytes: flag_pes_stripped (1),
+ * composition_page (2), ancillary_page (2).
+ */
+ public DvbDecoder(List<byte[]> initializationData) {
+ super("DvbDecoder");
+ ParsableByteArray data = new ParsableByteArray(initializationData.get(0));
+ int subtitleCompositionPage = data.readUnsignedShort();
+ int subtitleAncillaryPage = data.readUnsignedShort();
+ parser = new DvbParser(subtitleCompositionPage, subtitleAncillaryPage);
+ }
+
+ @Override
+ protected Subtitle decode(byte[] data, int length, boolean reset) {
+ if (reset) {
+ parser.reset();
+ }
+ return new DvbSubtitle(parser.decode(data, length));
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbParser.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbParser.java
new file mode 100644
index 0000000000..839c206ad7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbParser.java
@@ -0,0 +1,1059 @@
+/*
+ * 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.dvb;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+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.
+ */
+/* package */ final class DvbParser {
+
+ private static final String TAG = "DvbParser";
+
+ // Segment types, as defined by ETSI EN 300 743 Table 2
+ private static final int SEGMENT_TYPE_PAGE_COMPOSITION = 0x10;
+ private static final int SEGMENT_TYPE_REGION_COMPOSITION = 0x11;
+ private static final int SEGMENT_TYPE_CLUT_DEFINITION = 0x12;
+ private static final int SEGMENT_TYPE_OBJECT_DATA = 0x13;
+ private static final int SEGMENT_TYPE_DISPLAY_DEFINITION = 0x14;
+
+ // Page states, as defined by ETSI EN 300 743 Table 3
+ private static final int PAGE_STATE_NORMAL = 0; // Update. Only changed elements.
+ // private static final int PAGE_STATE_ACQUISITION = 1; // Refresh. All elements.
+ // private static final int PAGE_STATE_CHANGE = 2; // New. All elements.
+
+ // Region depths, as defined by ETSI EN 300 743 Table 5
+ // private static final int REGION_DEPTH_2_BIT = 1;
+ private static final int REGION_DEPTH_4_BIT = 2;
+ private static final int REGION_DEPTH_8_BIT = 3;
+
+ // Object codings, as defined by ETSI EN 300 743 Table 8
+ private static final int OBJECT_CODING_PIXELS = 0;
+ private static final int OBJECT_CODING_STRING = 1;
+
+ // Pixel-data types, as defined by ETSI EN 300 743 Table 9
+ private static final int DATA_TYPE_2BP_CODE_STRING = 0x10;
+ private static final int DATA_TYPE_4BP_CODE_STRING = 0x11;
+ private static final int DATA_TYPE_8BP_CODE_STRING = 0x12;
+ private static final int DATA_TYPE_24_TABLE_DATA = 0x20;
+ private static final int DATA_TYPE_28_TABLE_DATA = 0x21;
+ private static final int DATA_TYPE_48_TABLE_DATA = 0x22;
+ private static final int DATA_TYPE_END_LINE = 0xF0;
+
+ // Clut mapping tables, as defined by ETSI EN 300 743 10.4, 10.5, 10.6
+ private static final byte[] defaultMap2To4 = {
+ (byte) 0x00, (byte) 0x07, (byte) 0x08, (byte) 0x0F};
+ private static final byte[] defaultMap2To8 = {
+ (byte) 0x00, (byte) 0x77, (byte) 0x88, (byte) 0xFF};
+ private static final byte[] defaultMap4To8 = {
+ (byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33,
+ (byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77,
+ (byte) 0x88, (byte) 0x99, (byte) 0xAA, (byte) 0xBB,
+ (byte) 0xCC, (byte) 0xDD, (byte) 0xEE, (byte) 0xFF};
+
+ private final Paint defaultPaint;
+ private final Paint fillRegionPaint;
+ private final Canvas canvas;
+ private final DisplayDefinition defaultDisplayDefinition;
+ private final ClutDefinition defaultClutDefinition;
+ private final SubtitleService subtitleService;
+
+ @MonotonicNonNull private Bitmap bitmap;
+
+ /**
+ * Construct an instance for the given subtitle and ancillary page ids.
+ *
+ * @param subtitlePageId The id of the subtitle page carrying the subtitle to be parsed.
+ * @param ancillaryPageId The id of the ancillary page containing additional data.
+ */
+ public DvbParser(int subtitlePageId, int ancillaryPageId) {
+ defaultPaint = new Paint();
+ defaultPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+ defaultPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+ defaultPaint.setPathEffect(null);
+ fillRegionPaint = new Paint();
+ fillRegionPaint.setStyle(Paint.Style.FILL);
+ fillRegionPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER));
+ fillRegionPaint.setPathEffect(null);
+ canvas = new Canvas();
+ defaultDisplayDefinition = new DisplayDefinition(719, 575, 0, 719, 0, 575);
+ defaultClutDefinition = new ClutDefinition(0, generateDefault2BitClutEntries(),
+ generateDefault4BitClutEntries(), generateDefault8BitClutEntries());
+ subtitleService = new SubtitleService(subtitlePageId, ancillaryPageId);
+ }
+
+ /**
+ * Resets the parser.
+ */
+ public void reset() {
+ subtitleService.reset();
+ }
+
+ /**
+ * Decodes a subtitling packet, returning a list of parsed {@link Cue}s.
+ *
+ * @param data The subtitling packet data to decode.
+ * @param limit The limit in {@code data} at which to stop decoding.
+ * @return The parsed {@link Cue}s.
+ */
+ public List<Cue> decode(byte[] data, int limit) {
+ // Parse the input data.
+ ParsableBitArray dataBitArray = new ParsableBitArray(data, limit);
+ while (dataBitArray.bitsLeft() >= 48 // sync_byte (8) + segment header (40)
+ && dataBitArray.readBits(8) == 0x0F) {
+ parseSubtitlingSegment(dataBitArray, subtitleService);
+ }
+
+ @Nullable PageComposition pageComposition = subtitleService.pageComposition;
+ if (pageComposition == null) {
+ return Collections.emptyList();
+ }
+
+ // Update the canvas bitmap if necessary.
+ DisplayDefinition displayDefinition = subtitleService.displayDefinition != null
+ ? subtitleService.displayDefinition : defaultDisplayDefinition;
+ if (bitmap == null || displayDefinition.width + 1 != bitmap.getWidth()
+ || displayDefinition.height + 1 != bitmap.getHeight()) {
+ bitmap = Bitmap.createBitmap(displayDefinition.width + 1, displayDefinition.height + 1,
+ Bitmap.Config.ARGB_8888);
+ canvas.setBitmap(bitmap);
+ }
+
+ // Build the cues.
+ List<Cue> cues = new ArrayList<>();
+ SparseArray<PageRegion> 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);
+
+ // Clip drawing to the current region and display definition window.
+ int baseHorizontalAddress = pageRegion.horizontalAddress
+ + displayDefinition.horizontalPositionMinimum;
+ int baseVerticalAddress = pageRegion.verticalAddress
+ + displayDefinition.verticalPositionMinimum;
+ int clipRight = Math.min(baseHorizontalAddress + regionComposition.width,
+ displayDefinition.horizontalPositionMaximum);
+ int clipBottom = Math.min(baseVerticalAddress + regionComposition.height,
+ displayDefinition.verticalPositionMaximum);
+ canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom);
+ ClutDefinition clutDefinition = subtitleService.cluts.get(regionComposition.clutId);
+ if (clutDefinition == null) {
+ clutDefinition = subtitleService.ancillaryCluts.get(regionComposition.clutId);
+ if (clutDefinition == null) {
+ clutDefinition = defaultClutDefinition;
+ }
+ }
+
+ SparseArray<RegionObject> regionObjects = regionComposition.regionObjects;
+ for (int j = 0; j < regionObjects.size(); j++) {
+ int objectId = regionObjects.keyAt(j);
+ RegionObject regionObject = regionObjects.valueAt(j);
+ ObjectData objectData = subtitleService.objects.get(objectId);
+ if (objectData == null) {
+ objectData = subtitleService.ancillaryObjects.get(objectId);
+ }
+ if (objectData != null) {
+ @Nullable Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint;
+ paintPixelDataSubBlocks(objectData, clutDefinition, regionComposition.depth,
+ baseHorizontalAddress + regionObject.horizontalPosition,
+ baseVerticalAddress + regionObject.verticalPosition, paint, canvas);
+ }
+ }
+
+ if (regionComposition.fillFlag) {
+ int color;
+ if (regionComposition.depth == REGION_DEPTH_8_BIT) {
+ color = clutDefinition.clutEntries8Bit[regionComposition.pixelCode8Bit];
+ } else if (regionComposition.depth == REGION_DEPTH_4_BIT) {
+ color = clutDefinition.clutEntries4Bit[regionComposition.pixelCode4Bit];
+ } else {
+ color = clutDefinition.clutEntries2Bit[regionComposition.pixelCode2Bit];
+ }
+ fillRegionPaint.setColor(color);
+ canvas.drawRect(baseHorizontalAddress, baseVerticalAddress,
+ baseHorizontalAddress + regionComposition.width,
+ baseVerticalAddress + regionComposition.height,
+ fillRegionPaint);
+ }
+
+ Bitmap cueBitmap = Bitmap.createBitmap(bitmap, baseHorizontalAddress, baseVerticalAddress,
+ regionComposition.width, regionComposition.height);
+ cues.add(new Cue(cueBitmap, (float) baseHorizontalAddress / displayDefinition.width,
+ Cue.ANCHOR_TYPE_START, (float) baseVerticalAddress / displayDefinition.height,
+ Cue.ANCHOR_TYPE_START, (float) regionComposition.width / displayDefinition.width,
+ (float) regionComposition.height / displayDefinition.height));
+
+ canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
+ // Restore clean clipping state.
+ canvas.restore();
+ }
+
+ return Collections.unmodifiableList(cues);
+ }
+
+ // Static parsing.
+
+ /**
+ * Parses a subtitling segment, as defined by ETSI EN 300 743 7.2
+ * <p>
+ * The {@link SubtitleService} is updated with the parsed segment data.
+ */
+ private static void parseSubtitlingSegment(ParsableBitArray data, SubtitleService service) {
+ int segmentType = data.readBits(8);
+ int pageId = data.readBits(16);
+ int dataFieldLength = data.readBits(16);
+ int dataFieldLimit = data.getBytePosition() + dataFieldLength;
+
+ if ((dataFieldLength * 8) > data.bitsLeft()) {
+ Log.w(TAG, "Data field length exceeds limit");
+ // Skip to the very end.
+ data.skipBits(data.bitsLeft());
+ return;
+ }
+
+ switch (segmentType) {
+ case SEGMENT_TYPE_DISPLAY_DEFINITION:
+ if (pageId == service.subtitlePageId) {
+ service.displayDefinition = parseDisplayDefinition(data);
+ }
+ break;
+ case SEGMENT_TYPE_PAGE_COMPOSITION:
+ if (pageId == service.subtitlePageId) {
+ @Nullable PageComposition current = service.pageComposition;
+ PageComposition pageComposition = parsePageComposition(data, dataFieldLength);
+ if (pageComposition.state != PAGE_STATE_NORMAL) {
+ service.pageComposition = pageComposition;
+ service.regions.clear();
+ service.cluts.clear();
+ service.objects.clear();
+ } else if (current != null && current.version != pageComposition.version) {
+ service.pageComposition = pageComposition;
+ }
+ }
+ break;
+ case SEGMENT_TYPE_REGION_COMPOSITION:
+ @Nullable PageComposition pageComposition = service.pageComposition;
+ if (pageId == service.subtitlePageId && pageComposition != null) {
+ RegionComposition regionComposition = parseRegionComposition(data, dataFieldLength);
+ if (pageComposition.state == PAGE_STATE_NORMAL) {
+ @Nullable
+ RegionComposition existingRegionComposition = service.regions.get(regionComposition.id);
+ if (existingRegionComposition != null) {
+ regionComposition.mergeFrom(existingRegionComposition);
+ }
+ }
+ service.regions.put(regionComposition.id, regionComposition);
+ }
+ break;
+ case SEGMENT_TYPE_CLUT_DEFINITION:
+ if (pageId == service.subtitlePageId) {
+ ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength);
+ service.cluts.put(clutDefinition.id, clutDefinition);
+ } else if (pageId == service.ancillaryPageId) {
+ ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength);
+ service.ancillaryCluts.put(clutDefinition.id, clutDefinition);
+ }
+ break;
+ case SEGMENT_TYPE_OBJECT_DATA:
+ if (pageId == service.subtitlePageId) {
+ ObjectData objectData = parseObjectData(data);
+ service.objects.put(objectData.id, objectData);
+ } else if (pageId == service.ancillaryPageId) {
+ ObjectData objectData = parseObjectData(data);
+ service.ancillaryObjects.put(objectData.id, objectData);
+ }
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+
+ // Skip to the next segment.
+ data.skipBytes(dataFieldLimit - data.getBytePosition());
+ }
+
+ /**
+ * Parses a display definition segment, as defined by ETSI EN 300 743 7.2.1.
+ */
+ private static DisplayDefinition parseDisplayDefinition(ParsableBitArray data) {
+ data.skipBits(4); // dds_version_number (4).
+ boolean displayWindowFlag = data.readBit();
+ data.skipBits(3); // Skip reserved.
+ int width = data.readBits(16);
+ int height = data.readBits(16);
+
+ int horizontalPositionMinimum;
+ int horizontalPositionMaximum;
+ int verticalPositionMinimum;
+ int verticalPositionMaximum;
+ if (displayWindowFlag) {
+ horizontalPositionMinimum = data.readBits(16);
+ horizontalPositionMaximum = data.readBits(16);
+ verticalPositionMinimum = data.readBits(16);
+ verticalPositionMaximum = data.readBits(16);
+ } else {
+ horizontalPositionMinimum = 0;
+ horizontalPositionMaximum = width;
+ verticalPositionMinimum = 0;
+ verticalPositionMaximum = height;
+ }
+
+ return new DisplayDefinition(width, height, horizontalPositionMinimum,
+ horizontalPositionMaximum, verticalPositionMinimum, verticalPositionMaximum);
+ }
+
+ /**
+ * Parses a page composition segment, as defined by ETSI EN 300 743 7.2.2.
+ */
+ private static PageComposition parsePageComposition(ParsableBitArray data, int length) {
+ int timeoutSecs = data.readBits(8);
+ int version = data.readBits(4);
+ int state = data.readBits(2);
+ data.skipBits(2);
+ int remainingLength = length - 2;
+
+ SparseArray<PageRegion> regions = new SparseArray<>();
+ while (remainingLength > 0) {
+ int regionId = data.readBits(8);
+ data.skipBits(8); // Skip reserved.
+ int regionHorizontalAddress = data.readBits(16);
+ int regionVerticalAddress = data.readBits(16);
+ remainingLength -= 6;
+ regions.put(regionId, new PageRegion(regionHorizontalAddress, regionVerticalAddress));
+ }
+
+ return new PageComposition(timeoutSecs, version, state, regions);
+ }
+
+ /**
+ * Parses a region composition segment, as defined by ETSI EN 300 743 7.2.3.
+ */
+ private static RegionComposition parseRegionComposition(ParsableBitArray data, int length) {
+ int id = data.readBits(8);
+ data.skipBits(4); // Skip region_version_number
+ boolean fillFlag = data.readBit();
+ data.skipBits(3); // Skip reserved.
+ int width = data.readBits(16);
+ int height = data.readBits(16);
+ int levelOfCompatibility = data.readBits(3);
+ int depth = data.readBits(3);
+ data.skipBits(2); // Skip reserved.
+ int clutId = data.readBits(8);
+ int pixelCode8Bit = data.readBits(8);
+ int pixelCode4Bit = data.readBits(4);
+ int pixelCode2Bit = data.readBits(2);
+ data.skipBits(2); // Skip reserved
+ int remainingLength = length - 10;
+
+ SparseArray<RegionObject> regionObjects = new SparseArray<>();
+ while (remainingLength > 0) {
+ int objectId = data.readBits(16);
+ int objectType = data.readBits(2);
+ int objectProvider = data.readBits(2);
+ int objectHorizontalPosition = data.readBits(12);
+ data.skipBits(4); // Skip reserved.
+ int objectVerticalPosition = data.readBits(12);
+ remainingLength -= 6;
+
+ int foregroundPixelCode = 0;
+ int backgroundPixelCode = 0;
+ if (objectType == 0x01 || objectType == 0x02) { // Only seems to affect to char subtitles.
+ foregroundPixelCode = data.readBits(8);
+ backgroundPixelCode = data.readBits(8);
+ remainingLength -= 2;
+ }
+
+ regionObjects.put(objectId, new RegionObject(objectType, objectProvider,
+ objectHorizontalPosition, objectVerticalPosition, foregroundPixelCode,
+ backgroundPixelCode));
+ }
+
+ return new RegionComposition(id, fillFlag, width, height, levelOfCompatibility, depth, clutId,
+ pixelCode8Bit, pixelCode4Bit, pixelCode2Bit, regionObjects);
+ }
+
+ /**
+ * Parses a CLUT definition segment, as defined by ETSI EN 300 743 7.2.4.
+ */
+ private static ClutDefinition parseClutDefinition(ParsableBitArray data, int length) {
+ int clutId = data.readBits(8);
+ data.skipBits(8); // Skip clut_version_number (4), reserved (4)
+ int remainingLength = length - 2;
+
+ int[] clutEntries2Bit = generateDefault2BitClutEntries();
+ int[] clutEntries4Bit = generateDefault4BitClutEntries();
+ int[] clutEntries8Bit = generateDefault8BitClutEntries();
+
+ while (remainingLength > 0) {
+ int entryId = data.readBits(8);
+ int entryFlags = data.readBits(8);
+ remainingLength -= 2;
+
+ int[] clutEntries;
+ if ((entryFlags & 0x80) != 0) {
+ clutEntries = clutEntries2Bit;
+ } else if ((entryFlags & 0x40) != 0) {
+ clutEntries = clutEntries4Bit;
+ } else {
+ clutEntries = clutEntries8Bit;
+ }
+
+ int y;
+ int cr;
+ int cb;
+ int t;
+ if ((entryFlags & 0x01) != 0) {
+ y = data.readBits(8);
+ cr = data.readBits(8);
+ cb = data.readBits(8);
+ t = data.readBits(8);
+ remainingLength -= 4;
+ } else {
+ y = data.readBits(6) << 2;
+ cr = data.readBits(4) << 4;
+ cb = data.readBits(4) << 4;
+ t = data.readBits(2) << 6;
+ remainingLength -= 2;
+ }
+
+ if (y == 0x00) {
+ cr = 0x00;
+ cb = 0x00;
+ t = 0xFF;
+ }
+
+ int a = (byte) (0xFF - (t & 0xFF));
+ 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)));
+ clutEntries[entryId] = getColor(a, Util.constrainValue(r, 0, 255),
+ Util.constrainValue(g, 0, 255), Util.constrainValue(b, 0, 255));
+ }
+
+ return new ClutDefinition(clutId, clutEntries2Bit, clutEntries4Bit, clutEntries8Bit);
+ }
+
+ /**
+ * Parses an object data segment, as defined by ETSI EN 300 743 7.2.5.
+ *
+ * @return The parsed object data.
+ */
+ private static ObjectData parseObjectData(ParsableBitArray data) {
+ int objectId = data.readBits(16);
+ data.skipBits(4); // Skip object_version_number
+ int objectCodingMethod = data.readBits(2);
+ boolean nonModifyingColorFlag = data.readBit();
+ data.skipBits(1); // Skip reserved.
+
+ @Nullable byte[] topFieldData = null;
+ @Nullable byte[] bottomFieldData = null;
+
+ if (objectCodingMethod == OBJECT_CODING_STRING) {
+ int numberOfCodes = data.readBits(8);
+ // TODO: Parse and use character_codes.
+ data.skipBits(numberOfCodes * 16); // Skip character_codes.
+ } else if (objectCodingMethod == OBJECT_CODING_PIXELS) {
+ int topFieldDataLength = data.readBits(16);
+ int bottomFieldDataLength = data.readBits(16);
+ if (topFieldDataLength > 0) {
+ topFieldData = new byte[topFieldDataLength];
+ data.readBytes(topFieldData, 0, topFieldDataLength);
+ }
+ if (bottomFieldDataLength > 0) {
+ bottomFieldData = new byte[bottomFieldDataLength];
+ data.readBytes(bottomFieldData, 0, bottomFieldDataLength);
+ } else {
+ bottomFieldData = topFieldData;
+ }
+ }
+
+ return new ObjectData(objectId, nonModifyingColorFlag, topFieldData, bottomFieldData);
+ }
+
+ private static int[] generateDefault2BitClutEntries() {
+ int[] entries = new int[4];
+ entries[0] = 0x00000000;
+ entries[1] = 0xFFFFFFFF;
+ entries[2] = 0xFF000000;
+ entries[3] = 0xFF7F7F7F;
+ return entries;
+ }
+
+ private static int[] generateDefault4BitClutEntries() {
+ int[] entries = new int[16];
+ entries[0] = 0x00000000;
+ for (int i = 1; i < entries.length; i++) {
+ if (i < 8) {
+ entries[i] = getColor(
+ 0xFF,
+ ((i & 0x01) != 0 ? 0xFF : 0x00),
+ ((i & 0x02) != 0 ? 0xFF : 0x00),
+ ((i & 0x04) != 0 ? 0xFF : 0x00));
+ } else {
+ entries[i] = getColor(
+ 0xFF,
+ ((i & 0x01) != 0 ? 0x7F : 0x00),
+ ((i & 0x02) != 0 ? 0x7F : 0x00),
+ ((i & 0x04) != 0 ? 0x7F : 0x00));
+ }
+ }
+ return entries;
+ }
+
+ private static int[] generateDefault8BitClutEntries() {
+ int[] entries = new int[256];
+ entries[0] = 0x00000000;
+ for (int i = 0; i < entries.length; i++) {
+ if (i < 8) {
+ entries[i] = getColor(
+ 0x3F,
+ ((i & 0x01) != 0 ? 0xFF : 0x00),
+ ((i & 0x02) != 0 ? 0xFF : 0x00),
+ ((i & 0x04) != 0 ? 0xFF : 0x00));
+ } else {
+ switch (i & 0x88) {
+ case 0x00:
+ entries[i] = getColor(
+ 0xFF,
+ (((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)),
+ (((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)),
+ (((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00)));
+ break;
+ case 0x08:
+ entries[i] = getColor(
+ 0x7F,
+ (((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)),
+ (((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)),
+ (((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00)));
+ break;
+ case 0x80:
+ entries[i] = getColor(
+ 0xFF,
+ (127 + ((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)),
+ (127 + ((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)),
+ (127 + ((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00)));
+ break;
+ case 0x88:
+ entries[i] = getColor(
+ 0xFF,
+ (((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)),
+ (((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)),
+ (((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00)));
+ break;
+ }
+ }
+ }
+ return entries;
+ }
+
+ private static int getColor(int a, int r, int g, int b) {
+ return (a << 24) | (r << 16) | (g << 8) | b;
+ }
+
+ // 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,
+ @Nullable Paint paint,
+ Canvas canvas) {
+ int[] clutEntries;
+ if (regionDepth == REGION_DEPTH_8_BIT) {
+ clutEntries = clutDefinition.clutEntries8Bit;
+ } else if (regionDepth == REGION_DEPTH_4_BIT) {
+ clutEntries = clutDefinition.clutEntries4Bit;
+ } else {
+ clutEntries = clutDefinition.clutEntries2Bit;
+ }
+ paintPixelDataSubBlock(objectData.topFieldData, clutEntries, regionDepth, horizontalAddress,
+ verticalAddress, paint, canvas);
+ paintPixelDataSubBlock(objectData.bottomFieldData, clutEntries, regionDepth, horizontalAddress,
+ 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,
+ @Nullable Paint paint,
+ Canvas canvas) {
+ ParsableBitArray data = new ParsableBitArray(pixelData);
+ int column = horizontalAddress;
+ int line = verticalAddress;
+ @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:
+ @Nullable byte[] clutMapTable2ToX;
+ if (regionDepth == REGION_DEPTH_8_BIT) {
+ clutMapTable2ToX = clutMapTable2To8 == null ? defaultMap2To8 : clutMapTable2To8;
+ } else if (regionDepth == REGION_DEPTH_4_BIT) {
+ clutMapTable2ToX = clutMapTable2To4 == null ? defaultMap2To4 : clutMapTable2To4;
+ } else {
+ clutMapTable2ToX = null;
+ }
+ column = paint2BitPixelCodeString(data, clutEntries, clutMapTable2ToX, column, line,
+ paint, canvas);
+ data.byteAlign();
+ break;
+ case DATA_TYPE_4BP_CODE_STRING:
+ @Nullable byte[] clutMapTable4ToX;
+ if (regionDepth == REGION_DEPTH_8_BIT) {
+ clutMapTable4ToX = clutMapTable4To8 == null ? defaultMap4To8 : clutMapTable4To8;
+ } else {
+ clutMapTable4ToX = null;
+ }
+ column = paint4BitPixelCodeString(data, clutEntries, clutMapTable4ToX, column, line,
+ paint, canvas);
+ data.byteAlign();
+ break;
+ case DATA_TYPE_8BP_CODE_STRING:
+ column =
+ paint8BitPixelCodeString(
+ data, clutEntries, /* clutMapTable= */ null, column, line, paint, canvas);
+ break;
+ case DATA_TYPE_24_TABLE_DATA:
+ clutMapTable2To4 = buildClutMapTable(4, 4, data);
+ break;
+ case DATA_TYPE_28_TABLE_DATA:
+ clutMapTable2To8 = buildClutMapTable(4, 8, data);
+ break;
+ case DATA_TYPE_48_TABLE_DATA:
+ clutMapTable4To8 = buildClutMapTable(16, 8, data);
+ break;
+ case DATA_TYPE_END_LINE:
+ column = horizontalAddress;
+ line += 2;
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+ }
+ }
+
+ /** 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 (peek != 0x00) {
+ runLength = 1;
+ clutIndex = peek;
+ } else if (data.readBit()) {
+ runLength = 3 + data.readBits(3);
+ clutIndex = data.readBits(2);
+ } else if (data.readBit()) {
+ runLength = 1;
+ } else {
+ switch (data.readBits(2)) {
+ case 0x00:
+ endOfPixelCodeString = true;
+ break;
+ case 0x01:
+ runLength = 2;
+ break;
+ case 0x02:
+ runLength = 12 + data.readBits(4);
+ clutIndex = data.readBits(2);
+ break;
+ case 0x03:
+ runLength = 29 + data.readBits(8);
+ clutIndex = data.readBits(2);
+ break;
+ }
+ }
+
+ if (runLength != 0 && paint != null) {
+ paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]);
+ canvas.drawRect(column, line, column + runLength, line + 1, paint);
+ }
+
+ column += runLength;
+ } while (!endOfPixelCodeString);
+
+ 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,
+ @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(4);
+ if (peek != 0x00) {
+ runLength = 1;
+ clutIndex = peek;
+ } else if (!data.readBit()) {
+ peek = data.readBits(3);
+ if (peek != 0x00) {
+ runLength = 2 + peek;
+ clutIndex = 0x00;
+ } else {
+ endOfPixelCodeString = true;
+ }
+ } else if (!data.readBit()) {
+ runLength = 4 + data.readBits(2);
+ clutIndex = data.readBits(4);
+ } else {
+ switch (data.readBits(2)) {
+ case 0x00:
+ runLength = 1;
+ break;
+ case 0x01:
+ runLength = 2;
+ break;
+ case 0x02:
+ runLength = 9 + data.readBits(4);
+ clutIndex = data.readBits(4);
+ break;
+ case 0x03:
+ runLength = 25 + data.readBits(8);
+ clutIndex = data.readBits(4);
+ break;
+ }
+ }
+
+ if (runLength != 0 && paint != null) {
+ paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]);
+ canvas.drawRect(column, line, column + runLength, line + 1, paint);
+ }
+
+ column += runLength;
+ } while (!endOfPixelCodeString);
+
+ 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,
+ @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(8);
+ if (peek != 0x00) {
+ runLength = 1;
+ clutIndex = peek;
+ } else {
+ if (!data.readBit()) {
+ peek = data.readBits(7);
+ if (peek != 0x00) {
+ runLength = peek;
+ clutIndex = 0x00;
+ } else {
+ endOfPixelCodeString = true;
+ }
+ } else {
+ runLength = data.readBits(7);
+ clutIndex = data.readBits(8);
+ }
+ }
+
+ if (runLength != 0 && paint != null) {
+ paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]);
+ canvas.drawRect(column, line, column + runLength, line + 1, paint);
+ }
+ column += runLength;
+ } while (!endOfPixelCodeString);
+
+ return column;
+ }
+
+ private static byte[] buildClutMapTable(int length, int bitsPerEntry, ParsableBitArray data) {
+ byte[] clutMapTable = new byte[length];
+ for (int i = 0; i < length; i++) {
+ clutMapTable[i] = (byte) data.readBits(bitsPerEntry);
+ }
+ return clutMapTable;
+ }
+
+ // Private inner classes.
+
+ /**
+ * The subtitle service definition.
+ */
+ private static final class SubtitleService {
+
+ public final int subtitlePageId;
+ public final int ancillaryPageId;
+
+ public final SparseArray<RegionComposition> regions;
+ public final SparseArray<ClutDefinition> cluts;
+ public final SparseArray<ObjectData> objects;
+ public final SparseArray<ClutDefinition> ancillaryCluts;
+ public final SparseArray<ObjectData> ancillaryObjects;
+
+ @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() {
+ regions.clear();
+ cluts.clear();
+ objects.clear();
+ ancillaryCluts.clear();
+ ancillaryObjects.clear();
+ displayDefinition = null;
+ pageComposition = null;
+ }
+
+ }
+
+ /**
+ * Contains the geometry and active area of the subtitle service.
+ * <p>
+ * See ETSI EN 300 743 7.2.1
+ */
+ private static final class DisplayDefinition {
+
+ public final int width;
+ public final int height;
+
+ public final int horizontalPositionMinimum;
+ public final int horizontalPositionMaximum;
+ public final int verticalPositionMinimum;
+ public final int verticalPositionMaximum;
+
+ public DisplayDefinition(int width, int height, int horizontalPositionMinimum,
+ int horizontalPositionMaximum, int verticalPositionMinimum, int verticalPositionMaximum) {
+ this.width = width;
+ this.height = height;
+ this.horizontalPositionMinimum = horizontalPositionMinimum;
+ this.horizontalPositionMaximum = horizontalPositionMaximum;
+ this.verticalPositionMinimum = verticalPositionMinimum;
+ this.verticalPositionMaximum = verticalPositionMaximum;
+ }
+
+ }
+
+ /**
+ * The page is the definition and arrangement of regions in the screen.
+ * <p>
+ * See ETSI EN 300 743 7.2.2
+ */
+ private static final class PageComposition {
+
+ public final int timeOutSecs; // TODO: Use this or remove it.
+ public final int version;
+ public final int state;
+ public final SparseArray<PageRegion> regions;
+
+ public PageComposition(int timeoutSecs, int version, int state,
+ SparseArray<PageRegion> regions) {
+ this.timeOutSecs = timeoutSecs;
+ this.version = version;
+ this.state = state;
+ this.regions = regions;
+ }
+
+ }
+
+ /**
+ * A region within a {@link PageComposition}.
+ * <p>
+ * See ETSI EN 300 743 7.2.2
+ */
+ private static final class PageRegion {
+
+ public final int horizontalAddress;
+ public final int verticalAddress;
+
+ public PageRegion(int horizontalAddress, int verticalAddress) {
+ this.horizontalAddress = horizontalAddress;
+ this.verticalAddress = verticalAddress;
+ }
+
+ }
+
+ /**
+ * An area of the page composed of a list of objects and a CLUT.
+ * <p>
+ * See ETSI EN 300 743 7.2.3
+ */
+ private static final class RegionComposition {
+
+ public final int id;
+ public final boolean fillFlag;
+ public final int width;
+ public final int height;
+ public final int levelOfCompatibility; // TODO: Use this or remove it.
+ public final int depth;
+ public final int clutId;
+ public final int pixelCode8Bit;
+ public final int pixelCode4Bit;
+ public final int pixelCode2Bit;
+ public final SparseArray<RegionObject> regionObjects;
+
+ public RegionComposition(int id, boolean fillFlag, int width, int height,
+ int levelOfCompatibility, int depth, int clutId, int pixelCode8Bit, int pixelCode4Bit,
+ int pixelCode2Bit, SparseArray<RegionObject> regionObjects) {
+ this.id = id;
+ this.fillFlag = fillFlag;
+ this.width = width;
+ this.height = height;
+ this.levelOfCompatibility = levelOfCompatibility;
+ this.depth = depth;
+ this.clutId = clutId;
+ this.pixelCode8Bit = pixelCode8Bit;
+ this.pixelCode4Bit = pixelCode4Bit;
+ this.pixelCode2Bit = pixelCode2Bit;
+ this.regionObjects = regionObjects;
+ }
+
+ public void mergeFrom(RegionComposition otherRegionComposition) {
+ SparseArray<RegionObject> otherRegionObjects = otherRegionComposition.regionObjects;
+ for (int i = 0; i < otherRegionObjects.size(); i++) {
+ regionObjects.put(otherRegionObjects.keyAt(i), otherRegionObjects.valueAt(i));
+ }
+ }
+
+ }
+
+ /**
+ * An object within a {@link RegionComposition}.
+ * <p>
+ * See ETSI EN 300 743 7.2.3
+ */
+ private static final class RegionObject {
+
+ public final int type; // TODO: Use this or remove it.
+ public final int provider; // TODO: Use this or remove it.
+ public final int horizontalPosition;
+ public final int verticalPosition;
+ public final int foregroundPixelCode; // TODO: Use this or remove it.
+ public final int backgroundPixelCode; // TODO: Use this or remove it.
+
+ public RegionObject(int type, int provider, int horizontalPosition,
+ int verticalPosition, int foregroundPixelCode, int backgroundPixelCode) {
+ this.type = type;
+ this.provider = provider;
+ this.horizontalPosition = horizontalPosition;
+ this.verticalPosition = verticalPosition;
+ this.foregroundPixelCode = foregroundPixelCode;
+ this.backgroundPixelCode = backgroundPixelCode;
+ }
+
+ }
+
+ /**
+ * CLUT family definition containing the color tables for the three bit depths defined
+ * <p>
+ * See ETSI EN 300 743 7.2.4
+ */
+ private static final class ClutDefinition {
+
+ public final int id;
+ public final int[] clutEntries2Bit;
+ public final int[] clutEntries4Bit;
+ public final int[] clutEntries8Bit;
+
+ public ClutDefinition(int id, int[] clutEntries2Bit, int[] clutEntries4Bit,
+ int[] clutEntries8bit) {
+ this.id = id;
+ this.clutEntries2Bit = clutEntries2Bit;
+ this.clutEntries4Bit = clutEntries4Bit;
+ this.clutEntries8Bit = clutEntries8bit;
+ }
+
+ }
+
+ /**
+ * The textual or graphical representation of an object.
+ * <p>
+ * See ETSI EN 300 743 7.2.5
+ */
+ private static final class ObjectData {
+
+ public final int id;
+ public final boolean nonModifyingColorFlag;
+ public final byte[] topFieldData;
+ public final byte[] bottomFieldData;
+
+ public ObjectData(int id, boolean nonModifyingColorFlag, byte[] topFieldData,
+ byte[] bottomFieldData) {
+ this.id = id;
+ this.nonModifyingColorFlag = nonModifyingColorFlag;
+ this.topFieldData = topFieldData;
+ this.bottomFieldData = bottomFieldData;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbSubtitle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbSubtitle.java
new file mode 100644
index 0000000000..a624ddaeae
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbSubtitle.java
@@ -0,0 +1,54 @@
+/*
+ * 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.dvb;
+
+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 DVB subtitle.
+ */
+/* package */ final class DvbSubtitle implements Subtitle {
+
+ private final List<Cue> cues;
+
+ public DvbSubtitle(List<Cue> 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<Cue> getCues(long timeUs) {
+ return cues;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/package-info.java
new file mode 100644
index 0000000000..be6b16c5e6
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/package-info.java
new file mode 100644
index 0000000000..0b6e0d1f8c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/PgsDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/PgsDecoder.java
new file mode 100644
index 0000000000..859d240e9b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<Cue> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java
new file mode 100644
index 0000000000..e875763a45
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<Cue> cues;
+
+ public PgsSubtitle(List<Cue> 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<Cue> getCues(long timeUs) {
+ return cues;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/pgs/package-info.java
new file mode 100644
index 0000000000..ce385ea085
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaDecoder.java
new file mode 100644
index 0000000000..8f878a998e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<String, SsaStyle> styles;
+
+ /**
+ * The horizontal resolution used by the subtitle author - all cue positions are relative to this.
+ *
+ * <p>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.
+ *
+ * <p>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<byte[]> 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<List<Cue>> cues = new ArrayList<>();
+ List<Long> 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.
+ *
+ * <p>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.
+ *
+ * <p>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<String, SsaStyle> parseStyles(ParsableByteArray data) {
+ Map<String, SsaStyle> 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<List<Cue>> cues, List<Long> 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<List<Cue>> cues, List<Long> 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.
+ *
+ * <p>If it's inserted, we also insert a matching entry to {@code cues}.
+ */
+ private static int addCuePlacerholderByTime(
+ long timeUs, List<Long> sortedCueTimesUs, List<List<Cue>> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java
new file mode 100644
index 0000000000..312c779e23
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaStyle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaStyle.java
new file mode 100644
index 0000000000..3c3639a3fb
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>Allowed values:
+ *
+ * <ul>
+ * <li>{@link #SSA_ALIGNMENT_UNKNOWN}
+ * <li>{@link #SSA_ALIGNMENT_BOTTOM_LEFT}
+ * <li>{@link #SSA_ALIGNMENT_BOTTOM_CENTER}
+ * <li>{@link #SSA_ALIGNMENT_BOTTOM_RIGHT}
+ * <li>{@link #SSA_ALIGNMENT_MIDDLE_LEFT}
+ * <li>{@link #SSA_ALIGNMENT_MIDDLE_CENTER}
+ * <li>{@link #SSA_ALIGNMENT_MIDDLE_RIGHT}
+ * <li>{@link #SSA_ALIGNMENT_TOP_LEFT}
+ * <li>{@link #SSA_ALIGNMENT_TOP_CENTER}
+ * <li>{@link #SSA_ALIGNMENT_TOP_RIGHT}
+ * </ul>
+ */
+ @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
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java
new file mode 100644
index 0000000000..fb0544156d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<List<Cue>> cues;
+ private final List<Long> cueTimesUs;
+
+ /**
+ * @param cues The cues in the subtitle.
+ * @param cueTimesUs The cue times, in microseconds.
+ */
+ public SsaSubtitle(List<List<Cue>> cues, List<Long> 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<Cue> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ssa/package-info.java
new file mode 100644
index 0000000000..bc4b625d77
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/SubripDecoder.java
new file mode 100644
index 0000000000..36ebf6ead0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/SubripDecoder.java
@@ -0,0 +1,259 @@
+/*
+ * 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.text.subrip;
+
+import android.text.Html;
+import android.text.Spanned;
+import android.text.TextUtils;
+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;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link SimpleSubtitleDecoder} for SubRip.
+ */
+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";
+
+ // 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*");
+
+ // 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<String> tags;
+
+ public SubripDecoder() {
+ super("SubripDecoder");
+ textBuilder = new StringBuilder();
+ tags = new ArrayList<>();
+ }
+
+ @Override
+ protected Subtitle decode(byte[] bytes, int length, boolean reset) {
+ ArrayList<Cue> cues = new ArrayList<>();
+ LongArray cueTimesUs = new LongArray();
+ ParsableByteArray subripData = new ParsableByteArray(bytes, length);
+
+ @Nullable String currentLine;
+ while ((currentLine = subripData.readLine()) != null) {
+ if (currentLine.length() == 0) {
+ // Skip blank lines.
+ continue;
+ }
+
+ // Parse the index line as a sanity check.
+ try {
+ Integer.parseInt(currentLine);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Skipping invalid index: " + currentLine);
+ continue;
+ }
+
+ // Read and parse the timing line.
+ 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, /* groupOffset= */ 1));
+ cueTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 6));
+ } else {
+ Log.w(TAG, "Skipping invalid timing: " + currentLine);
+ continue;
+ }
+
+ // Read and parse the text and tags.
+ textBuilder.setLength(0);
+ tags.clear();
+ currentLine = subripData.readLine();
+ while (!TextUtils.isEmpty(currentLine)) {
+ if (textBuilder.length() > 0) {
+ textBuilder.append("<br>");
+ }
+ textBuilder.append(processLine(currentLine, tags));
+ currentLine = subripData.readLine();
+ }
+
+ Spanned text = Html.fromHtml(textBuilder.toString());
+
+ @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()];
+ cues.toArray(cuesArray);
+ long[] cueTimesUsArray = cueTimesUs.toArray();
+ 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<String> 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) {
+ @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;
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java
new file mode 100644
index 0000000000..d011f5d7c5
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java
@@ -0,0 +1,72 @@
+/*
+ * 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.text.subrip;
+
+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 a SubRip subtitle.
+ */
+/* package */ final class SubripSubtitle implements Subtitle {
+
+ private final Cue[] cues;
+ private final long[] cueTimesUs;
+
+ /**
+ * @param cues The cues in the subtitle.
+ * @param cueTimesUs The cue times, in microseconds.
+ */
+ public SubripSubtitle(Cue[] cues, long[] cueTimesUs) {
+ this.cues = cues;
+ this.cueTimesUs = cueTimesUs;
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false);
+ return index < cueTimesUs.length ? index : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return cueTimesUs.length;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ Assertions.checkArgument(index >= 0);
+ Assertions.checkArgument(index < cueTimesUs.length);
+ return cueTimesUs[index];
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false);
+ 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 {
+ return Collections.singletonList(cues[index]);
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/subrip/package-info.java
new file mode 100644
index 0000000000..fb990cb748
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
new file mode 100644
index 0000000000..502281c2de
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
@@ -0,0 +1,756 @@
+/*
+ * 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.text.ttml;
+
+import android.text.Layout;
+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.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+/**
+ * A {@link SimpleSubtitleDecoder} for TTML supporting the DFXP presentation profile. Features
+ * supported by this decoder are:
+ *
+ * <ul>
+ * <li>content
+ * <li>core
+ * <li>presentation
+ * <li>profile
+ * <li>structure
+ * <li>time-offset
+ * <li>timing
+ * <li>tickRate
+ * <li>time-clock-with-frames
+ * <li>time-clock
+ * <li>time-offset-with-frames
+ * <li>time-offset-with-ticks
+ * <li>cell-resolution
+ * </ul>
+ *
+ * @see <a href="http://www.w3.org/TR/ttaf1-dfxp/">TTML specification</a>
+ */
+public final class TtmlDecoder extends SimpleSubtitleDecoder {
+
+ private static final String TAG = "TtmlDecoder";
+
+ private static final String TTP = "http://www.w3.org/ns/ttml#parameter";
+
+ private static final String ATTR_BEGIN = "begin";
+ private static final String ATTR_DURATION = "dur";
+ 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])"
+ + "(?:(\\.[0-9]+)|:([0-9][0-9])(?:\\.([0-9]+))?)?$");
+ private static final Pattern OFFSET_TIME =
+ Pattern.compile("^([0-9]+(?:\\.[0-9]+)?)(h|m|s|ms|f|t)$");
+ 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;
+
+ public TtmlDecoder() {
+ super("TtmlDecoder");
+ try {
+ xmlParserFactory = XmlPullParserFactory.newInstance();
+ xmlParserFactory.setNamespaceAware(true);
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e);
+ }
+ }
+
+ @Override
+ protected Subtitle decode(byte[] bytes, int length, boolean reset)
+ throws SubtitleDecoderException {
+ try {
+ XmlPullParser xmlParser = xmlParserFactory.newPullParser();
+ Map<String, TtmlStyle> globalStyles = new HashMap<>();
+ Map<String, TtmlRegion> regionMap = new HashMap<>();
+ Map<String, String> 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;
+ ArrayDeque<TtmlNode> 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.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, cellResolution, ttsExtent, regionMap, imageMap);
+ } else {
+ try {
+ TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate);
+ nodeStack.push(node);
+ if (parent != null) {
+ parent.addChild(node);
+ }
+ } catch (SubtitleDecoderException e) {
+ Log.w(TAG, "Suppressing parser error", e);
+ // Treat the node (and by extension, all of its children) as unsupported.
+ unsupportedNodeDepth++;
+ }
+ }
+ } else if (eventType == XmlPullParser.TEXT) {
+ parent.addChild(TtmlNode.buildTextNode(xmlParser.getText()));
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (xmlParser.getName().equals(TtmlNode.TAG_TT)) {
+ ttmlSubtitle = new TtmlSubtitle(nodeStack.peek(), globalStyles, regionMap, imageMap);
+ }
+ nodeStack.pop();
+ }
+ } else {
+ if (eventType == XmlPullParser.START_TAG) {
+ unsupportedNodeDepth++;
+ } else if (eventType == XmlPullParser.END_TAG) {
+ unsupportedNodeDepth--;
+ }
+ }
+ xmlParser.next();
+ eventType = xmlParser.getEventType();
+ }
+ return ttmlSubtitle;
+ } catch (XmlPullParserException xppe) {
+ throw new SubtitleDecoderException("Unable to decode source", xppe);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unexpected error when reading input.", e);
+ }
+ }
+
+ private FrameAndTickRate parseFrameAndTickRates(XmlPullParser xmlParser)
+ throws SubtitleDecoderException {
+ int frameRate = DEFAULT_FRAME_RATE;
+ String frameRateString = xmlParser.getAttributeValue(TTP, "frameRate");
+ if (frameRateString != null) {
+ frameRate = Integer.parseInt(frameRateString);
+ }
+
+ float frameRateMultiplier = 1;
+ String frameRateMultiplierString = xmlParser.getAttributeValue(TTP, "frameRateMultiplier");
+ if (frameRateMultiplierString != null) {
+ String[] parts = Util.split(frameRateMultiplierString, " ");
+ if (parts.length != 2) {
+ throw new SubtitleDecoderException("frameRateMultiplier doesn't have 2 parts");
+ }
+ float numerator = Integer.parseInt(parts[0]);
+ float denominator = Integer.parseInt(parts[1]);
+ frameRateMultiplier = numerator / denominator;
+ }
+
+ int subFrameRate = DEFAULT_FRAME_AND_TICK_RATE.subFrameRate;
+ String subFrameRateString = xmlParser.getAttributeValue(TTP, "subFrameRate");
+ if (subFrameRateString != null) {
+ subFrameRate = Integer.parseInt(subFrameRateString);
+ }
+
+ int tickRate = DEFAULT_FRAME_AND_TICK_RATE.tickRate;
+ String tickRateString = xmlParser.getAttributeValue(TTP, "tickRate");
+ if (tickRateString != null) {
+ tickRate = Integer.parseInt(tickRateString);
+ }
+ return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate);
+ }
+
+ 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<String, TtmlStyle> parseHeader(
+ XmlPullParser xmlParser,
+ Map<String, TtmlStyle> globalStyles,
+ CellResolution cellResolution,
+ TtsExtent ttsExtent,
+ Map<String, TtmlRegion> globalRegions,
+ Map<String, String> imageMap)
+ throws IOException, XmlPullParserException {
+ do {
+ xmlParser.next();
+ if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_STYLE)) {
+ String parentStyleId = XmlPullParserUtil.getAttributeValue(xmlParser, ATTR_STYLE);
+ TtmlStyle style = parseStyleAttributes(xmlParser, new TtmlStyle());
+ if (parentStyleId != null) {
+ for (String id : parseStyleIds(parentStyleId)) {
+ style.chain(globalStyles.get(id));
+ }
+ }
+ if (style.getId() != null) {
+ globalStyles.put(style.getId(), style);
+ }
+ } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) {
+ 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;
+ }
+
+ private void parseMetadata(XmlPullParser xmlParser, Map<String, String> 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.
+ *
+ * <p>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;
+ }
+
+ 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) {
+ parentStyleIds = parentStyleIds.trim();
+ return parentStyleIds.isEmpty() ? new String[0] : Util.split(parentStyleIds, "\\s+");
+ }
+
+ private TtmlStyle parseStyleAttributes(XmlPullParser parser, TtmlStyle style) {
+ int attributeCount = parser.getAttributeCount();
+ for (int i = 0; i < attributeCount; i++) {
+ String attributeValue = parser.getAttributeValue(i);
+ switch (parser.getAttributeName(i)) {
+ case TtmlNode.ATTR_ID:
+ if (TtmlNode.TAG_STYLE.equals(parser.getName())) {
+ style = createIfNull(style).setId(attributeValue);
+ }
+ break;
+ case TtmlNode.ATTR_TTS_BACKGROUND_COLOR:
+ style = createIfNull(style);
+ try {
+ style.setBackgroundColor(ColorParser.parseTtmlColor(attributeValue));
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Failed parsing background value: " + attributeValue);
+ }
+ break;
+ case TtmlNode.ATTR_TTS_COLOR:
+ style = createIfNull(style);
+ try {
+ style.setFontColor(ColorParser.parseTtmlColor(attributeValue));
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Failed parsing color value: " + attributeValue);
+ }
+ break;
+ case TtmlNode.ATTR_TTS_FONT_FAMILY:
+ style = createIfNull(style).setFontFamily(attributeValue);
+ break;
+ case TtmlNode.ATTR_TTS_FONT_SIZE:
+ try {
+ style = createIfNull(style);
+ parseFontSize(attributeValue, style);
+ } catch (SubtitleDecoderException e) {
+ Log.w(TAG, "Failed parsing fontSize value: " + attributeValue);
+ }
+ break;
+ case TtmlNode.ATTR_TTS_FONT_WEIGHT:
+ style = createIfNull(style).setBold(
+ TtmlNode.BOLD.equalsIgnoreCase(attributeValue));
+ break;
+ case TtmlNode.ATTR_TTS_FONT_STYLE:
+ style = createIfNull(style).setItalic(
+ TtmlNode.ITALIC.equalsIgnoreCase(attributeValue));
+ break;
+ case TtmlNode.ATTR_TTS_TEXT_ALIGN:
+ switch (Util.toLowerInvariant(attributeValue)) {
+ case TtmlNode.LEFT:
+ style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_NORMAL);
+ break;
+ case TtmlNode.START:
+ style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_NORMAL);
+ break;
+ case TtmlNode.RIGHT:
+ style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_OPPOSITE);
+ break;
+ case TtmlNode.END:
+ style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_OPPOSITE);
+ break;
+ case TtmlNode.CENTER:
+ style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_CENTER);
+ break;
+ }
+ break;
+ case TtmlNode.ATTR_TTS_TEXT_DECORATION:
+ switch (Util.toLowerInvariant(attributeValue)) {
+ case TtmlNode.LINETHROUGH:
+ style = createIfNull(style).setLinethrough(true);
+ break;
+ case TtmlNode.NO_LINETHROUGH:
+ style = createIfNull(style).setLinethrough(false);
+ break;
+ case TtmlNode.UNDERLINE:
+ style = createIfNull(style).setUnderline(true);
+ break;
+ case TtmlNode.NO_UNDERLINE:
+ style = createIfNull(style).setUnderline(false);
+ break;
+ }
+ break;
+ default:
+ // ignore
+ break;
+ }
+ }
+ return style;
+ }
+
+ private TtmlStyle createIfNull(TtmlStyle style) {
+ return style == null ? new TtmlStyle() : style;
+ }
+
+ private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent,
+ Map<String, TtmlRegion> regionMap, FrameAndTickRate frameAndTickRate)
+ throws SubtitleDecoderException {
+ long duration = C.TIME_UNSET;
+ 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);
+ for (int i = 0; i < attributeCount; i++) {
+ String attr = parser.getAttributeName(i);
+ String value = parser.getAttributeValue(i);
+ switch (attr) {
+ case ATTR_BEGIN:
+ startTime = parseTimeExpression(value, frameAndTickRate);
+ break;
+ case ATTR_END:
+ endTime = parseTimeExpression(value, frameAndTickRate);
+ break;
+ case ATTR_DURATION:
+ duration = parseTimeExpression(value, frameAndTickRate);
+ break;
+ case ATTR_STYLE:
+ // IDREFS: potentially multiple space delimited ids
+ String[] ids = parseStyleIds(value);
+ if (ids.length > 0) {
+ styleIds = ids;
+ }
+ break;
+ case ATTR_REGION:
+ if (regionMap.containsKey(value)) {
+ // If the region has not been correctly declared or does not define a position, we use
+ // the anonymous region.
+ 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;
+ }
+ }
+ if (parent != null && parent.startTimeUs != C.TIME_UNSET) {
+ if (startTime != C.TIME_UNSET) {
+ startTime += parent.startTimeUs;
+ }
+ if (endTime != C.TIME_UNSET) {
+ endTime += parent.startTimeUs;
+ }
+ }
+ if (endTime == C.TIME_UNSET) {
+ if (duration != C.TIME_UNSET) {
+ // Infer the end time from the duration.
+ endTime = startTime + duration;
+ } else if (parent != null && parent.endTimeUs != C.TIME_UNSET) {
+ // If the end time remains unspecified, then it should be inherited from the parent.
+ endTime = parent.endTimeUs;
+ }
+ }
+ return TtmlNode.buildNode(
+ parser.getName(), startTime, endTime, style, styleIds, regionId, imageId);
+ }
+
+ private static boolean isSupportedTag(String tag) {
+ return tag.equals(TtmlNode.TAG_TT)
+ || tag.equals(TtmlNode.TAG_HEAD)
+ || tag.equals(TtmlNode.TAG_BODY)
+ || tag.equals(TtmlNode.TAG_DIV)
+ || tag.equals(TtmlNode.TAG_P)
+ || tag.equals(TtmlNode.TAG_SPAN)
+ || tag.equals(TtmlNode.TAG_BR)
+ || tag.equals(TtmlNode.TAG_STYLE)
+ || tag.equals(TtmlNode.TAG_STYLING)
+ || tag.equals(TtmlNode.TAG_LAYOUT)
+ || tag.equals(TtmlNode.TAG_REGION)
+ || tag.equals(TtmlNode.TAG_METADATA)
+ || 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 = Util.split(expression, "\\s+");
+ Matcher matcher;
+ if (expressions.length == 1) {
+ matcher = FONT_SIZE.matcher(expression);
+ } else if (expressions.length == 2){
+ matcher = FONT_SIZE.matcher(expressions[1]);
+ Log.w(TAG, "Multiple values in fontSize attribute. Picking the second value for vertical font"
+ + " size and ignoring the first.");
+ } else {
+ throw new SubtitleDecoderException("Invalid number of entries for fontSize: "
+ + expressions.length + ".");
+ }
+
+ if (matcher.matches()) {
+ String unit = matcher.group(3);
+ switch (unit) {
+ case "px":
+ out.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_PIXEL);
+ break;
+ case "em":
+ out.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_EM);
+ break;
+ case "%":
+ out.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_PERCENT);
+ break;
+ default:
+ throw new SubtitleDecoderException("Invalid unit for fontSize: '" + unit + "'.");
+ }
+ out.setFontSize(Float.valueOf(matcher.group(1)));
+ } else {
+ throw new SubtitleDecoderException("Invalid expression for fontSize: '" + expression + "'.");
+ }
+ }
+
+ /**
+ * Parses a time expression, returning the parsed timestamp.
+ * <p>
+ * For the format of a time expression, see:
+ * <a href="http://www.w3.org/TR/ttaf1-dfxp/#timing-value-timeExpression">timeExpression</a>
+ *
+ * @param time A string that includes the time expression.
+ * @param frameAndTickRate The effective frame and tick rates of the stream.
+ * @return The parsed timestamp in microseconds.
+ * @throws SubtitleDecoderException If the given string does not contain a valid time expression.
+ */
+ private static long parseTimeExpression(String time, FrameAndTickRate frameAndTickRate)
+ throws SubtitleDecoderException {
+ Matcher matcher = CLOCK_TIME.matcher(time);
+ if (matcher.matches()) {
+ String hours = matcher.group(1);
+ double durationSeconds = Long.parseLong(hours) * 3600;
+ String minutes = matcher.group(2);
+ durationSeconds += Long.parseLong(minutes) * 60;
+ String seconds = matcher.group(3);
+ durationSeconds += Long.parseLong(seconds);
+ String fraction = matcher.group(4);
+ durationSeconds += (fraction != null) ? Double.parseDouble(fraction) : 0;
+ String frames = matcher.group(5);
+ durationSeconds += (frames != null)
+ ? Long.parseLong(frames) / frameAndTickRate.effectiveFrameRate : 0;
+ String subframes = matcher.group(6);
+ durationSeconds += (subframes != null)
+ ? ((double) Long.parseLong(subframes)) / frameAndTickRate.subFrameRate
+ / frameAndTickRate.effectiveFrameRate
+ : 0;
+ return (long) (durationSeconds * C.MICROS_PER_SECOND);
+ }
+ matcher = OFFSET_TIME.matcher(time);
+ if (matcher.matches()) {
+ String timeValue = matcher.group(1);
+ double offsetSeconds = Double.parseDouble(timeValue);
+ String unit = matcher.group(2);
+ switch (unit) {
+ case "h":
+ offsetSeconds *= 3600;
+ break;
+ case "m":
+ offsetSeconds *= 60;
+ break;
+ case "s":
+ // Do nothing.
+ break;
+ case "ms":
+ offsetSeconds /= 1000;
+ break;
+ case "f":
+ offsetSeconds /= frameAndTickRate.effectiveFrameRate;
+ break;
+ case "t":
+ offsetSeconds /= frameAndTickRate.tickRate;
+ break;
+ }
+ return (long) (offsetSeconds * C.MICROS_PER_SECOND);
+ }
+ throw new SubtitleDecoderException("Malformed time expression: " + time);
+ }
+
+ private static final class FrameAndTickRate {
+ final float effectiveFrameRate;
+ final int subFrameRate;
+ final int tickRate;
+
+ FrameAndTickRate(float effectiveFrameRate, int subFrameRate, int tickRate) {
+ this.effectiveFrameRate = effectiveFrameRate;
+ this.subFrameRate = subFrameRate;
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlNode.java
new file mode 100644
index 0000000000..16d0f28f6b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlNode.java
@@ -0,0 +1,399 @@
+/*
+ * 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.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;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * A package internal representation of TTML node.
+ */
+/* package */ final class TtmlNode {
+
+ public static final String TAG_TT = "tt";
+ public static final String TAG_HEAD = "head";
+ public static final String TAG_BODY = "body";
+ public static final String TAG_DIV = "div";
+ public static final String TAG_P = "p";
+ public static final String TAG_SPAN = "span";
+ public static final String TAG_BR = "br";
+ public static final String TAG_STYLE = "style";
+ public static final String TAG_STYLING = "styling";
+ 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_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_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_TEXT_DECORATION = "textDecoration";
+ public static final String ATTR_TTS_TEXT_ALIGN = "textAlign";
+
+ public static final String LINETHROUGH = "linethrough";
+ public static final String NO_LINETHROUGH = "nolinethrough";
+ public static final String UNDERLINE = "underline";
+ public static final String NO_UNDERLINE = "nounderline";
+ public static final String ITALIC = "italic";
+ public static final String BOLD = "bold";
+
+ public static final String LEFT = "left";
+ public static final String CENTER = "center";
+ public static final String RIGHT = "right";
+ public static final String START = "start";
+ public static final String END = "end";
+
+ @Nullable public final String tag;
+ @Nullable public final String text;
+ public final boolean isTextNode;
+ public final long startTimeUs;
+ public final long endTimeUs;
+ @Nullable public final TtmlStyle style;
+ @Nullable private final String[] styleIds;
+ public final String regionId;
+ @Nullable public final String imageId;
+
+ private final HashMap<String, Integer> nodeStartsByRegion;
+ private final HashMap<String, Integer> nodeEndsByRegion;
+
+ private List<TtmlNode> children;
+
+ public static TtmlNode buildTextNode(String text) {
+ 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(
+ @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(
+ @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;
+ this.startTimeUs = startTimeUs;
+ this.endTimeUs = endTimeUs;
+ this.regionId = Assertions.checkNotNull(regionId);
+ nodeStartsByRegion = new HashMap<>();
+ nodeEndsByRegion = new HashMap<>();
+ }
+
+ public boolean isActive(long timeUs) {
+ return (startTimeUs == C.TIME_UNSET && endTimeUs == C.TIME_UNSET)
+ || (startTimeUs <= timeUs && endTimeUs == C.TIME_UNSET)
+ || (startTimeUs == C.TIME_UNSET && timeUs < endTimeUs)
+ || (startTimeUs <= timeUs && timeUs < endTimeUs);
+ }
+
+ public void addChild(TtmlNode child) {
+ if (children == null) {
+ children = new ArrayList<>();
+ }
+ children.add(child);
+ }
+
+ public TtmlNode getChild(int index) {
+ if (children == null) {
+ throw new IndexOutOfBoundsException();
+ }
+ return children.get(index);
+ }
+
+ public int getChildCount() {
+ return children == null ? 0 : children.size();
+ }
+
+ public long[] getEventTimesUs() {
+ TreeSet<Long> eventTimeSet = new TreeSet<>();
+ getEventTimes(eventTimeSet, false);
+ long[] eventTimes = new long[eventTimeSet.size()];
+ int i = 0;
+ for (long eventTimeUs : eventTimeSet) {
+ eventTimes[i++] = eventTimeUs;
+ }
+ return eventTimes;
+ }
+
+ private void getEventTimes(TreeSet<Long> out, boolean descendsPNode) {
+ boolean isPNode = TAG_P.equals(tag);
+ boolean isDivNode = TAG_DIV.equals(tag);
+ if (descendsPNode || isPNode || (isDivNode && imageId != null)) {
+ if (startTimeUs != C.TIME_UNSET) {
+ out.add(startTimeUs);
+ }
+ if (endTimeUs != C.TIME_UNSET) {
+ out.add(endTimeUs);
+ }
+ }
+ if (children == null) {
+ return;
+ }
+ for (int i = 0; i < children.size(); i++) {
+ children.get(i).getEventTimes(out, descendsPNode || isPNode);
+ }
+ }
+
+ public String[] getStyleIds() {
+ return styleIds;
+ }
+
+ public List<Cue> getCues(
+ long timeUs,
+ Map<String, TtmlStyle> globalStyles,
+ Map<String, TtmlRegion> regionMap,
+ Map<String, String> imageMap) {
+
+ List<Pair<String, String>> regionImageOutputs = new ArrayList<>();
+ traverseForImage(timeUs, regionId, regionImageOutputs);
+
+ TreeMap<String, SpannableStringBuilder> regionTextOutputs = new TreeMap<>();
+ traverseForText(timeUs, false, regionId, regionTextOutputs);
+ traverseForStyle(timeUs, globalStyles, regionTextOutputs);
+
+ List<Cue> cues = new ArrayList<>();
+
+ // Create image based cues.
+ for (Pair<String, String> 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<String, SpannableStringBuilder> 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 traverseForImage(
+ long timeUs, String inheritedRegion, List<Pair<String, String>> 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<String, SpannableStringBuilder> regionOutputs) {
+ nodeStartsByRegion.clear();
+ nodeEndsByRegion.clear();
+ 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 (isActive(timeUs)) {
+ // This is a container node, which can contain zero or more children.
+ for (Entry<String, SpannableStringBuilder> 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);
+ }
+ if (isPNode) {
+ TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs));
+ }
+
+ for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
+ nodeEndsByRegion.put(entry.getKey(), entry.getValue().length());
+ }
+ }
+ }
+
+ private static SpannableStringBuilder getRegionOutput(
+ String resolvedRegionId, Map<String, SpannableStringBuilder> regionOutputs) {
+ if (!regionOutputs.containsKey(resolvedRegionId)) {
+ regionOutputs.put(resolvedRegionId, new SpannableStringBuilder());
+ }
+ return regionOutputs.get(resolvedRegionId);
+ }
+
+ private void traverseForStyle(
+ long timeUs,
+ Map<String, TtmlStyle> globalStyles,
+ Map<String, SpannableStringBuilder> regionOutputs) {
+ if (!isActive(timeUs)) {
+ return;
+ }
+ for (Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) {
+ String regionId = entry.getKey();
+ int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0;
+ 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<String, TtmlStyle> globalStyles,
+ SpannableStringBuilder regionOutput,
+ int start,
+ int end) {
+ TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
+ if (resolvedStyle != null) {
+ TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle);
+ }
+ }
+
+ private SpannableStringBuilder cleanUpText(SpannableStringBuilder builder) {
+ // Having joined the text elements, we need to do some final cleanup on the result.
+ // 1. Collapse multiple consecutive spaces into a single space.
+ int builderLength = builder.length();
+ for (int i = 0; i < builderLength; i++) {
+ if (builder.charAt(i) == ' ') {
+ int j = i + 1;
+ while (j < builder.length() && builder.charAt(j) == ' ') {
+ j++;
+ }
+ int spacesToDelete = j - (i + 1);
+ if (spacesToDelete > 0) {
+ builder.delete(i, i + spacesToDelete);
+ builderLength -= spacesToDelete;
+ }
+ }
+ }
+ // 2. Remove any spaces from the start of each line.
+ if (builderLength > 0 && builder.charAt(0) == ' ') {
+ builder.delete(0, 1);
+ builderLength--;
+ }
+ for (int i = 0; i < builderLength - 1; i++) {
+ if (builder.charAt(i) == '\n' && builder.charAt(i + 1) == ' ') {
+ builder.delete(i + 1, i + 2);
+ builderLength--;
+ }
+ }
+ // 3. Remove any spaces from the end of each line.
+ if (builderLength > 0 && builder.charAt(builderLength - 1) == ' ') {
+ builder.delete(builderLength - 1, builderLength);
+ builderLength--;
+ }
+ for (int i = 0; i < builderLength - 1; i++) {
+ if (builder.charAt(i) == ' ' && builder.charAt(i + 1) == '\n') {
+ builder.delete(i, i + 1);
+ builderLength--;
+ }
+ }
+ // 4. Trim a trailing newline, if there is one.
+ if (builderLength > 0 && builder.charAt(builderLength - 1) == '\n') {
+ builder.delete(builderLength - 1, builderLength);
+ /*builderLength--;*/
+ }
+ return builder;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRegion.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRegion.java
new file mode 100644
index 0000000000..d14e547d49
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRegion.java
@@ -0,0 +1,69 @@
+/*
+ * 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.text.ttml;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue;
+
+/**
+ * Represents a TTML Region.
+ */
+/* package */ final class TtmlRegion {
+
+ public final String id;
+ public final float position;
+ public final float line;
+ 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(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(
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java
new file mode 100644
index 0000000000..f2387b6282
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java
@@ -0,0 +1,151 @@
+/*
+ * 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.text.ttml;
+
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.AlignmentSpan;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.TypefaceSpan;
+import android.text.style.UnderlineSpan;
+import java.util.Map;
+
+/**
+ * Package internal utility class to render styled <code>TtmlNode</code>s.
+ */
+/* package */ final class TtmlRenderUtil {
+
+ public static TtmlStyle resolveStyle(TtmlStyle style, String[] styleIds,
+ Map<String, TtmlStyle> globalStyles) {
+ if (style == null && styleIds == null) {
+ // No styles at all.
+ return null;
+ } else if (style == null && styleIds.length == 1) {
+ // Only one single referential style present.
+ return globalStyles.get(styleIds[0]);
+ } else if (style == null && styleIds.length > 1) {
+ // Only multiple referential styles present.
+ TtmlStyle chainedStyle = new TtmlStyle();
+ for (String id : styleIds) {
+ chainedStyle.chain(globalStyles.get(id));
+ }
+ return chainedStyle;
+ } else if (style != null && styleIds != null && styleIds.length == 1) {
+ // Merge a single referential style into inline style.
+ return style.chain(globalStyles.get(styleIds[0]));
+ } else if (style != null && styleIds != null && styleIds.length > 1) {
+ // Merge multiple referential styles into inline style.
+ for (String id : styleIds) {
+ style.chain(globalStyles.get(id));
+ }
+ return style;
+ }
+ // Only inline styles available.
+ return style;
+ }
+
+ public static void applyStylesToSpan(SpannableStringBuilder builder,
+ int start, int end, TtmlStyle style) {
+
+ if (style.getStyle() != TtmlStyle.UNSPECIFIED) {
+ builder.setSpan(new StyleSpan(style.getStyle()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.isLinethrough()) {
+ builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.isUnderline()) {
+ builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.hasFontColor()) {
+ builder.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.hasBackgroundColor()) {
+ builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.getFontFamily() != null) {
+ builder.setSpan(new TypefaceSpan(style.getFontFamily()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.getTextAlign() != null) {
+ builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ switch (style.getFontSizeUnit()) {
+ case TtmlStyle.FONT_SIZE_UNIT_PIXEL:
+ builder.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TtmlStyle.FONT_SIZE_UNIT_EM:
+ builder.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TtmlStyle.FONT_SIZE_UNIT_PERCENT:
+ builder.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TtmlStyle.UNSPECIFIED:
+ // Do nothing.
+ break;
+ }
+ }
+
+ /**
+ * Called when the end of a paragraph is encountered. Adds a newline if there are one or more
+ * non-space characters since the previous newline.
+ *
+ * @param builder The builder.
+ */
+ /* package */ static void endParagraph(SpannableStringBuilder builder) {
+ int position = builder.length() - 1;
+ while (position >= 0 && builder.charAt(position) == ' ') {
+ position--;
+ }
+ if (position >= 0 && builder.charAt(position) != '\n') {
+ builder.append('\n');
+ }
+ }
+
+ /**
+ * Applies the appropriate space policy to the given text element.
+ *
+ * @param in The text element to which the policy should be applied.
+ * @return The result of applying the policy to the text element.
+ */
+ /* package */ static String applyTextElementSpacePolicy(String in) {
+ // Removes carriage return followed by line feed. See: http://www.w3.org/TR/xml/#sec-line-ends
+ String out = in.replaceAll("\r\n", "\n");
+ // Apply suppress-at-line-break="auto" and
+ // white-space-treatment="ignore-if-surrounding-linefeed"
+ out = out.replaceAll(" *\n *", "\n");
+ // Apply linefeed-treatment="treat-as-space"
+ out = out.replaceAll("\n", " ");
+ // Apply white-space-collapse="true"
+ out = out.replaceAll("[ \t\\x0B\f\r]+", " ");
+ return out;
+ }
+
+ private TtmlRenderUtil() {}
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlStyle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlStyle.java
new file mode 100644
index 0000000000..57faaecb69
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlStyle.java
@@ -0,0 +1,268 @@
+/*
+ * 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.text.ttml;
+
+import android.graphics.Typeface;
+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;
+
+/**
+ * Style object of a <code>TtmlNode</code>
+ */
+/* package */ final class TtmlStyle {
+
+ public static final int UNSPECIFIED = -1;
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @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;
+
+ private String fontFamily;
+ private int fontColor;
+ private boolean hasFontColor;
+ private int backgroundColor;
+ private boolean hasBackgroundColor;
+ @OptionalBoolean private int linethrough;
+ @OptionalBoolean private int underline;
+ @OptionalBoolean private int bold;
+ @OptionalBoolean private int italic;
+ @FontSizeUnit private int fontSizeUnit;
+ private float fontSize;
+ private String id;
+ private TtmlStyle inheritableStyle;
+ private Layout.Alignment textAlign;
+
+ public TtmlStyle() {
+ linethrough = UNSPECIFIED;
+ underline = UNSPECIFIED;
+ bold = UNSPECIFIED;
+ italic = UNSPECIFIED;
+ fontSizeUnit = UNSPECIFIED;
+ }
+
+ /**
+ * Returns the style or {@link #UNSPECIFIED} when no style information is given.
+ *
+ * @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD}
+ * or {@link #STYLE_BOLD_ITALIC}.
+ */
+ @StyleFlags public int getStyle() {
+ if (bold == UNSPECIFIED && italic == UNSPECIFIED) {
+ return UNSPECIFIED;
+ }
+ return (bold == ON ? STYLE_BOLD : STYLE_NORMAL)
+ | (italic == ON ? STYLE_ITALIC : STYLE_NORMAL);
+ }
+
+ public boolean isLinethrough() {
+ return linethrough == ON;
+ }
+
+ public TtmlStyle setLinethrough(boolean linethrough) {
+ Assertions.checkState(inheritableStyle == null);
+ this.linethrough = linethrough ? ON : OFF;
+ return this;
+ }
+
+ public boolean isUnderline() {
+ return underline == ON;
+ }
+
+ public TtmlStyle setUnderline(boolean underline) {
+ Assertions.checkState(inheritableStyle == null);
+ this.underline = underline ? ON : OFF;
+ return this;
+ }
+
+ public TtmlStyle setBold(boolean bold) {
+ Assertions.checkState(inheritableStyle == null);
+ this.bold = bold ? ON : OFF;
+ return this;
+ }
+
+ public TtmlStyle setItalic(boolean italic) {
+ Assertions.checkState(inheritableStyle == null);
+ this.italic = italic ? ON : OFF;
+ return this;
+ }
+
+ public String getFontFamily() {
+ return fontFamily;
+ }
+
+ public TtmlStyle setFontFamily(String fontFamily) {
+ Assertions.checkState(inheritableStyle == null);
+ this.fontFamily = fontFamily;
+ return this;
+ }
+
+ public int getFontColor() {
+ if (!hasFontColor) {
+ throw new IllegalStateException("Font color has not been defined.");
+ }
+ return fontColor;
+ }
+
+ public TtmlStyle setFontColor(int fontColor) {
+ Assertions.checkState(inheritableStyle == null);
+ this.fontColor = fontColor;
+ hasFontColor = true;
+ return this;
+ }
+
+ public boolean hasFontColor() {
+ return hasFontColor;
+ }
+
+ public int getBackgroundColor() {
+ if (!hasBackgroundColor) {
+ throw new IllegalStateException("Background color has not been defined.");
+ }
+ return backgroundColor;
+ }
+
+ public TtmlStyle setBackgroundColor(int backgroundColor) {
+ this.backgroundColor = backgroundColor;
+ hasBackgroundColor = true;
+ return this;
+ }
+
+ public boolean hasBackgroundColor() {
+ return hasBackgroundColor;
+ }
+
+ /**
+ * Inherits from an ancestor style. Properties like <i>tts:backgroundColor</i> which
+ * are not inheritable are not inherited as well as properties which are already set locally
+ * are never overridden.
+ *
+ * @param ancestor the ancestor style to inherit from
+ */
+ public TtmlStyle inherit(TtmlStyle ancestor) {
+ return inherit(ancestor, false);
+ }
+
+ /**
+ * Chains this style to referential style. Local properties which are already set
+ * are never overridden.
+ *
+ * @param ancestor the referential style to inherit from
+ */
+ public TtmlStyle chain(TtmlStyle ancestor) {
+ return inherit(ancestor, true);
+ }
+
+ private TtmlStyle inherit(TtmlStyle ancestor, boolean chaining) {
+ if (ancestor != null) {
+ if (!hasFontColor && ancestor.hasFontColor) {
+ setFontColor(ancestor.fontColor);
+ }
+ if (bold == UNSPECIFIED) {
+ bold = ancestor.bold;
+ }
+ if (italic == UNSPECIFIED) {
+ italic = ancestor.italic;
+ }
+ if (fontFamily == null) {
+ fontFamily = ancestor.fontFamily;
+ }
+ if (linethrough == UNSPECIFIED) {
+ linethrough = ancestor.linethrough;
+ }
+ if (underline == UNSPECIFIED) {
+ underline = ancestor.underline;
+ }
+ if (textAlign == null) {
+ textAlign = ancestor.textAlign;
+ }
+ if (fontSizeUnit == UNSPECIFIED) {
+ fontSizeUnit = ancestor.fontSizeUnit;
+ fontSize = ancestor.fontSize;
+ }
+ // attributes not inherited as of http://www.w3.org/TR/ttml1/
+ if (chaining && !hasBackgroundColor && ancestor.hasBackgroundColor) {
+ setBackgroundColor(ancestor.backgroundColor);
+ }
+ }
+ return this;
+ }
+
+ public TtmlStyle setId(String id) {
+ this.id = id;
+ return this;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public Layout.Alignment getTextAlign() {
+ return textAlign;
+ }
+
+ public TtmlStyle setTextAlign(Layout.Alignment textAlign) {
+ this.textAlign = textAlign;
+ return this;
+ }
+
+ public TtmlStyle setFontSize(float fontSize) {
+ this.fontSize = fontSize;
+ return this;
+ }
+
+ public TtmlStyle setFontSizeUnit(int fontSizeUnit) {
+ this.fontSizeUnit = fontSizeUnit;
+ return this;
+ }
+
+ @FontSizeUnit public int getFontSizeUnit() {
+ return fontSizeUnit;
+ }
+
+ public float getFontSize() {
+ return fontSize;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java
new file mode 100644
index 0000000000..52bd389818
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java
@@ -0,0 +1,81 @@
+/*
+ * 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.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;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A representation of a TTML subtitle.
+ */
+/* package */ final class TtmlSubtitle implements Subtitle {
+
+ private final TtmlNode root;
+ private final long[] eventTimesUs;
+ private final Map<String, TtmlStyle> globalStyles;
+ private final Map<String, TtmlRegion> regionMap;
+ private final Map<String, String> imageMap;
+
+ public TtmlSubtitle(
+ TtmlNode root,
+ Map<String, TtmlStyle> globalStyles,
+ Map<String, TtmlRegion> regionMap,
+ Map<String, String> imageMap) {
+ this.root = root;
+ this.regionMap = regionMap;
+ this.imageMap = imageMap;
+ this.globalStyles =
+ globalStyles != null ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap();
+ this.eventTimesUs = root.getEventTimesUs();
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ int index = Util.binarySearchCeil(eventTimesUs, timeUs, false, false);
+ return index < eventTimesUs.length ? index : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return eventTimesUs.length;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ return eventTimesUs[index];
+ }
+
+ @VisibleForTesting
+ /* package */ TtmlNode getRoot() {
+ return root;
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ return root.getCues(timeUs, globalStyles, regionMap, imageMap);
+ }
+
+ @VisibleForTesting
+ /* package */ Map<String, TtmlStyle> getGlobalStyles() {
+ return globalStyles;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/package-info.java
new file mode 100644
index 0000000000..e6e7a5a8e3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java
new file mode 100644
index 0000000000..a6b9ab5c63
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.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.text.tx3g;
+
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.text.style.TypefaceSpan;
+import android.text.style.UnderlineSpan;
+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.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.nio.charset.Charset;
+import java.util.List;
+
+/**
+ * A {@link SimpleSubtitleDecoder} for tx3g.
+ * <p>
+ * Currently supports parsing of a single text track with embedded styles.
+ */
+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 = 0x7374796c;
+ private static final int TYPE_TBOX = 0x74626f78;
+ private static final String TX3G_SERIF = "Serif";
+
+ private static final int SIZE_ATOM_HEADER = 8;
+ private static final int SIZE_SHORT = 2;
+ private static final int SIZE_BOM_UTF16 = 2;
+ private static final int SIZE_STYLE_RECORD = 12;
+
+ private static final int FONT_FACE_BOLD = 0x0001;
+ 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 = 0;
+
+ private static final int DEFAULT_FONT_FACE = 0;
+ private static final int DEFAULT_COLOR = Color.WHITE;
+ private static final String DEFAULT_FONT_FAMILY = C.SANS_SERIF_NAME;
+ private static final float DEFAULT_VERTICAL_PLACEMENT = 0.85f;
+
+ private final ParsableByteArray parsableByteArray;
+
+ private boolean customVerticalPlacement;
+ private int defaultFontFace;
+ private int defaultColorRgba;
+ private String defaultFontFamily;
+ private float defaultVerticalPlacement;
+ private int calculatedVideoTrackHeight;
+
+ /**
+ * Sets up a new {@link Tx3gDecoder} with default values.
+ *
+ * @param initializationData Sample description atom ('stsd') data with default subtitle styles.
+ */
+ public Tx3gDecoder(List<byte[]> initializationData) {
+ super("Tx3gDecoder");
+ parsableByteArray = new ParsableByteArray();
+
+ if (initializationData != null && initializationData.size() == 1
+ && (initializationData.get(0).length == 48 || initializationData.get(0).length == 53)) {
+ byte[] initializationBytes = initializationData.get(0);
+ defaultFontFace = initializationBytes[24];
+ defaultColorRgba = ((initializationBytes[26] & 0xFF) << 24)
+ | ((initializationBytes[27] & 0xFF) << 16)
+ | ((initializationBytes[28] & 0xFF) << 8)
+ | (initializationBytes[29] & 0xFF);
+ 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];
+ customVerticalPlacement = (initializationBytes[0] & 0x20) != 0;
+ if (customVerticalPlacement) {
+ int requestedVerticalPlacement = ((initializationBytes[10] & 0xFF) << 8)
+ | (initializationBytes[11] & 0xFF);
+ defaultVerticalPlacement = (float) requestedVerticalPlacement / calculatedVideoTrackHeight;
+ defaultVerticalPlacement = Util.constrainValue(defaultVerticalPlacement, 0.0f, 0.95f);
+ } else {
+ defaultVerticalPlacement = DEFAULT_VERTICAL_PLACEMENT;
+ }
+ } else {
+ defaultFontFace = DEFAULT_FONT_FACE;
+ defaultColorRgba = DEFAULT_COLOR;
+ defaultFontFamily = DEFAULT_FONT_FAMILY;
+ customVerticalPlacement = false;
+ defaultVerticalPlacement = DEFAULT_VERTICAL_PLACEMENT;
+ }
+ }
+
+ @Override
+ protected Subtitle decode(byte[] bytes, int length, boolean reset)
+ throws SubtitleDecoderException {
+ parsableByteArray.reset(bytes, length);
+ String cueTextString = readSubtitleText(parsableByteArray);
+ if (cueTextString.isEmpty()) {
+ return Tx3gSubtitle.EMPTY;
+ }
+ // Attach default styles.
+ SpannableStringBuilder cueText = new SpannableStringBuilder(cueTextString);
+ attachFontFace(cueText, defaultFontFace, DEFAULT_FONT_FACE, 0, cueText.length(),
+ SPAN_PRIORITY_LOW);
+ attachColor(cueText, defaultColorRgba, DEFAULT_COLOR, 0, cueText.length(),
+ SPAN_PRIORITY_LOW);
+ attachFontFamily(cueText, defaultFontFamily, DEFAULT_FONT_FAMILY, 0, cueText.length(),
+ SPAN_PRIORITY_LOW);
+ float verticalPlacement = defaultVerticalPlacement;
+ // Find and attach additional styles.
+ while (parsableByteArray.bytesLeft() >= SIZE_ATOM_HEADER) {
+ int position = parsableByteArray.getPosition();
+ int atomSize = parsableByteArray.readInt();
+ int atomType = parsableByteArray.readInt();
+ if (atomType == TYPE_STYL) {
+ assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT);
+ int styleRecordCount = parsableByteArray.readUnsignedShort();
+ for (int i = 0; i < styleRecordCount; i++) {
+ applyStyleRecord(parsableByteArray, cueText);
+ }
+ } else if (atomType == TYPE_TBOX && customVerticalPlacement) {
+ assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT);
+ int requestedVerticalPlacement = parsableByteArray.readUnsignedShort();
+ verticalPlacement = (float) requestedVerticalPlacement / calculatedVideoTrackHeight;
+ verticalPlacement = Util.constrainValue(verticalPlacement, 0.0f, 0.95f);
+ }
+ parsableByteArray.setPosition(position + atomSize);
+ }
+ 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)
+ throws SubtitleDecoderException {
+ assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT);
+ int textLength = parsableByteArray.readUnsignedShort();
+ if (textLength == 0) {
+ return "";
+ }
+ if (parsableByteArray.bytesLeft() >= SIZE_BOM_UTF16) {
+ char firstChar = parsableByteArray.peekChar();
+ if (firstChar == BOM_UTF16_BE || firstChar == BOM_UTF16_LE) {
+ return parsableByteArray.readString(textLength, Charset.forName(C.UTF16_NAME));
+ }
+ }
+ return parsableByteArray.readString(textLength, Charset.forName(C.UTF8_NAME));
+ }
+
+ private void applyStyleRecord(ParsableByteArray parsableByteArray,
+ SpannableStringBuilder cueText) throws SubtitleDecoderException {
+ assertTrue(parsableByteArray.bytesLeft() >= SIZE_STYLE_RECORD);
+ int start = parsableByteArray.readUnsignedShort();
+ int end = parsableByteArray.readUnsignedShort();
+ parsableByteArray.skipBytes(2); // font identifier
+ int fontFace = parsableByteArray.readUnsignedByte();
+ parsableByteArray.skipBytes(1); // font size
+ int colorRgba = parsableByteArray.readInt();
+ attachFontFace(cueText, fontFace, defaultFontFace, start, end, SPAN_PRIORITY_HIGH);
+ attachColor(cueText, colorRgba, defaultColorRgba, start, end, SPAN_PRIORITY_HIGH);
+ }
+
+ private static void attachFontFace(SpannableStringBuilder cueText, int fontFace,
+ int defaultFontFace, int start, int end, int spanPriority) {
+ if (fontFace != defaultFontFace) {
+ final int flags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority;
+ boolean isBold = (fontFace & FONT_FACE_BOLD) != 0;
+ boolean isItalic = (fontFace & FONT_FACE_ITALIC) != 0;
+ if (isBold) {
+ if (isItalic) {
+ cueText.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), start, end, flags);
+ } else {
+ cueText.setSpan(new StyleSpan(Typeface.BOLD), start, end, flags);
+ }
+ } else if (isItalic) {
+ cueText.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flags);
+ }
+ boolean isUnderlined = (fontFace & FONT_FACE_UNDERLINE) != 0;
+ if (isUnderlined) {
+ cueText.setSpan(new UnderlineSpan(), start, end, flags);
+ }
+ if (!isUnderlined && !isBold && !isItalic) {
+ cueText.setSpan(new StyleSpan(Typeface.NORMAL), start, end, flags);
+ }
+ }
+ }
+
+ private static void attachColor(SpannableStringBuilder cueText, int colorRgba,
+ int defaultColorRgba, int start, int end, int spanPriority) {
+ if (colorRgba != defaultColorRgba) {
+ int colorArgb = ((colorRgba & 0xFF) << 24) | (colorRgba >>> 8);
+ cueText.setSpan(new ForegroundColorSpan(colorArgb), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority);
+ }
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ private static void attachFontFamily(SpannableStringBuilder cueText, String fontFamily,
+ String defaultFontFamily, int start, int end, int spanPriority) {
+ if (fontFamily != defaultFontFamily) {
+ cueText.setSpan(new TypefaceSpan(fontFamily), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority);
+ }
+ }
+
+ private static void assertTrue(boolean checkValue) throws SubtitleDecoderException {
+ if (!checkValue) {
+ throw new SubtitleDecoderException("Unexpected subtitle format.");
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java
new file mode 100644
index 0000000000..93bc6034d1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java
@@ -0,0 +1,63 @@
+/*
+ * 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.text.tx3g;
+
+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 java.util.Collections;
+import java.util.List;
+
+/**
+ * A representation of a tx3g subtitle.
+ */
+/* package */ final class Tx3gSubtitle implements Subtitle {
+
+ public static final Tx3gSubtitle EMPTY = new Tx3gSubtitle();
+
+ private final List<Cue> cues;
+
+ public Tx3gSubtitle(Cue cue) {
+ this.cues = Collections.singletonList(cue);
+ }
+
+ private Tx3gSubtitle() {
+ this.cues = Collections.emptyList();
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ return timeUs < 0 ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return 1;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ Assertions.checkArgument(index == 0);
+ return 0;
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ return timeUs >= 0 ? cues : Collections.emptyList();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/tx3g/package-info.java
new file mode 100644
index 0000000000..7bac8c12b6
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/CssParser.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/CssParser.java
new file mode 100644
index 0000000000..3337cc3481
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/CssParser.java
@@ -0,0 +1,347 @@
+/*
+ * 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.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 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;
+
+/**
+ * Provides a CSS parser for STYLE blocks in Webvtt files. Supports only a subset of the CSS
+ * features.
+ */
+/* package */ final class CssParser {
+
+ private static final String PROPERTY_BGCOLOR = "background-color";
+ private static final String PROPERTY_FONT_FAMILY = "font-family";
+ private static final String PROPERTY_FONT_WEIGHT = "font-weight";
+ 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 RULE_START = "{";
+ private static final String RULE_END = "}";
+ private static final String PROPERTY_FONT_STYLE = "font-style";
+ private static final String VALUE_ITALIC = "italic";
+
+ private static final Pattern VOICE_NAME_PATTERN = Pattern.compile("\\[voice=\"([^\"]*)\"\\]");
+
+ // Temporary utility data structures.
+ private final ParsableByteArray styleInput;
+ private final StringBuilder stringBuilder;
+
+ public CssParser() {
+ styleInput = new ParsableByteArray();
+ stringBuilder = new StringBuilder();
+ }
+
+ /**
+ * 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 list of {@link WebvttCssStyle}s that represents the parsed block, or a list
+ * containing the styles up to the parsing failure.
+ */
+ public List<WebvttCssStyle> parseBlock(ParsableByteArray input) {
+ stringBuilder.setLength(0);
+ int initialInputPosition = input.getPosition();
+ skipStyleBlock(input);
+ styleInput.reset(input.data, input.getPosition());
+ styleInput.setPosition(initialInputPosition);
+
+ List<WebvttCssStyle> 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 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.
+ *
+ * @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.
+ */
+ @Nullable
+ private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) {
+ skipWhitespaceAndComments(input);
+ if (input.bytesLeft() < 5) {
+ return null;
+ }
+ String cueSelector = input.readString(5);
+ if (!"::cue".equals(cueSelector)) {
+ return null;
+ }
+ int position = input.getPosition();
+ String token = parseNextToken(input, stringBuilder);
+ if (token == null) {
+ return null;
+ }
+ if (RULE_START.equals(token)) {
+ input.setPosition(position);
+ return "";
+ }
+ String target = null;
+ if ("(".equals(token)) {
+ target = readCueTarget(input);
+ }
+ token = parseNextToken(input, stringBuilder);
+ if (!")".equals(token)) {
+ return null;
+ }
+ return target;
+ }
+
+ /**
+ * Reads the contents of ::cue() and returns it as a string.
+ */
+ private static String readCueTarget(ParsableByteArray input) {
+ int position = input.getPosition();
+ int limit = input.limit();
+ boolean cueTargetEndFound = false;
+ while (position < limit && !cueTargetEndFound) {
+ char c = (char) input.data[position++];
+ cueTargetEndFound = c == ')';
+ }
+ return input.readString(--position - input.getPosition()).trim();
+ // --offset to return ')' to the input.
+ }
+
+ private static void parseStyleDeclaration(ParsableByteArray input, WebvttCssStyle style,
+ StringBuilder stringBuilder) {
+ skipWhitespaceAndComments(input);
+ String property = parseIdentifier(input, stringBuilder);
+ if ("".equals(property)) {
+ return;
+ }
+ if (!":".equals(parseNextToken(input, stringBuilder))) {
+ return;
+ }
+ skipWhitespaceAndComments(input);
+ String value = parsePropertyValue(input, stringBuilder);
+ if (value == null || "".equals(value)) {
+ return;
+ }
+ int position = input.getPosition();
+ String token = parseNextToken(input, stringBuilder);
+ if (";".equals(token)) {
+ // The style declaration is well formed.
+ } 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);
+ } else {
+ // The style declaration is not well formed.
+ return;
+ }
+ // At this point we have a presumably valid declaration, we need to parse it and fill the style.
+ if ("color".equals(property)) {
+ style.setFontColor(ColorParser.parseCssColor(value));
+ } else if (PROPERTY_BGCOLOR.equals(property)) {
+ style.setBackgroundColor(ColorParser.parseCssColor(value));
+ } else if (PROPERTY_TEXT_DECORATION.equals(property)) {
+ if (VALUE_UNDERLINE.equals(value)) {
+ style.setUnderline(true);
+ }
+ } else if (PROPERTY_FONT_FAMILY.equals(property)) {
+ style.setFontFamily(value);
+ } else if (PROPERTY_FONT_WEIGHT.equals(property)) {
+ if (VALUE_BOLD.equals(value)) {
+ style.setBold(true);
+ }
+ } else if (PROPERTY_FONT_STYLE.equals(property)) {
+ if (VALUE_ITALIC.equals(value)) {
+ style.setItalic(true);
+ }
+ }
+ // TODO: Fill remaining supported styles.
+ }
+
+ // Visible for testing.
+ /* package */ static void skipWhitespaceAndComments(ParsableByteArray input) {
+ boolean skipping = true;
+ while (input.bytesLeft() > 0 && skipping) {
+ skipping = maybeSkipWhitespace(input) || maybeSkipComment(input);
+ }
+ }
+
+ // Visible for testing.
+ @Nullable
+ /* package */ static String parseNextToken(ParsableByteArray input, StringBuilder stringBuilder) {
+ skipWhitespaceAndComments(input);
+ if (input.bytesLeft() == 0) {
+ return null;
+ }
+ String identifier = parseIdentifier(input, stringBuilder);
+ if (!"".equals(identifier)) {
+ return identifier;
+ }
+ // We found a delimiter.
+ return "" + (char) input.readUnsignedByte();
+ }
+
+ private static boolean maybeSkipWhitespace(ParsableByteArray input) {
+ switch(peekCharAtPosition(input, input.getPosition())) {
+ case '\t':
+ case '\r':
+ case '\n':
+ case '\f':
+ case ' ':
+ input.skipBytes(1);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ // Visible for testing.
+ /* package */ static void skipStyleBlock(ParsableByteArray input) {
+ // The style block cannot contain empty lines, so we assume the input ends when a empty line
+ // is found.
+ String line;
+ do {
+ line = input.readLine();
+ } while (!TextUtils.isEmpty(line));
+ }
+
+ private static char peekCharAtPosition(ParsableByteArray input, int position) {
+ return (char) input.data[position];
+ }
+
+ @Nullable
+ private static String parsePropertyValue(ParsableByteArray input, StringBuilder stringBuilder) {
+ StringBuilder expressionBuilder = new StringBuilder();
+ String token;
+ int position;
+ boolean expressionEndFound = false;
+ // TODO: Add support for "Strings in quotes with spaces".
+ while (!expressionEndFound) {
+ position = input.getPosition();
+ token = parseNextToken(input, stringBuilder);
+ if (token == null) {
+ // Syntax error.
+ return null;
+ }
+ if (RULE_END.equals(token) || ";".equals(token)) {
+ input.setPosition(position);
+ expressionEndFound = true;
+ } else {
+ expressionBuilder.append(token);
+ }
+ }
+ return expressionBuilder.toString();
+ }
+
+ private static boolean maybeSkipComment(ParsableByteArray input) {
+ int position = input.getPosition();
+ int limit = input.limit();
+ byte[] data = input.data;
+ if (position + 2 <= limit && data[position++] == '/' && data[position++] == '*') {
+ while (position + 1 < limit) {
+ char skippedChar = (char) data[position++];
+ if (skippedChar == '*') {
+ if (((char) data[position]) == '/') {
+ position++;
+ limit = position;
+ }
+ }
+ }
+ input.skipBytes(limit - input.getPosition());
+ return true;
+ }
+ return false;
+ }
+
+ private static String parseIdentifier(ParsableByteArray input, StringBuilder stringBuilder) {
+ stringBuilder.setLength(0);
+ int position = input.getPosition();
+ int limit = input.limit();
+ boolean identifierEndFound = false;
+ while (position < limit && !identifierEndFound) {
+ char c = (char) input.data[position];
+ if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '#'
+ || c == '-' || c == '.' || c == '_') {
+ position++;
+ stringBuilder.append(c);
+ } else {
+ identifierEndFound = true;
+ }
+ }
+ input.skipBytes(position - input.getPosition());
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Sets the target of a {@link WebvttCssStyle} by splitting a selector of the form
+ * {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional.
+ */
+ private void applySelectorToStyle(WebvttCssStyle style, String selector) {
+ if ("".equals(selector)) {
+ return; // Universal selector.
+ }
+ int voiceStartIndex = selector.indexOf('[');
+ if (voiceStartIndex != -1) {
+ Matcher matcher = VOICE_NAME_PATTERN.matcher(selector.substring(voiceStartIndex));
+ if (matcher.matches()) {
+ style.setTargetVoice(matcher.group(1));
+ }
+ selector = selector.substring(0, voiceStartIndex);
+ }
+ String[] classDivision = Util.split(selector, "\\.");
+ String tagAndIdDivision = classDivision[0];
+ int idPrefixIndex = tagAndIdDivision.indexOf('#');
+ if (idPrefixIndex != -1) {
+ style.setTargetTagName(tagAndIdDivision.substring(0, idPrefixIndex));
+ style.setTargetId(tagAndIdDivision.substring(idPrefixIndex + 1)); // We discard the '#'.
+ } else {
+ style.setTargetTagName(tagAndIdDivision);
+ }
+ if (classDivision.length > 1) {
+ style.setTargetClasses(Util.nullSafeArrayCopyOfRange(classDivision, 1, classDivision.length));
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java
new file mode 100644
index 0000000000..3df35c789b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java
@@ -0,0 +1,101 @@
+/*
+ * 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.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;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** 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;
+
+ @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;
+
+ public Mp4WebvttDecoder() {
+ super("Mp4WebvttDecoder");
+ sampleData = new ParsableByteArray();
+ builder = new WebvttCue.Builder();
+ }
+
+ @Override
+ 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.
+ sampleData.reset(bytes, length);
+ List<Cue> resultingCueList = new ArrayList<>();
+ while (sampleData.bytesLeft() > 0) {
+ if (sampleData.bytesLeft() < BOX_HEADER_SIZE) {
+ throw new SubtitleDecoderException("Incomplete Mp4Webvtt Top Level box header found.");
+ }
+ int boxSize = sampleData.readInt();
+ int boxType = sampleData.readInt();
+ if (boxType == TYPE_vttc) {
+ resultingCueList.add(parseVttCueBox(sampleData, builder, boxSize - BOX_HEADER_SIZE));
+ } else {
+ // Peers of the VTTCueBox are still not supported and are skipped.
+ sampleData.skipBytes(boxSize - BOX_HEADER_SIZE);
+ }
+ }
+ return new Mp4WebvttSubtitle(resultingCueList);
+ }
+
+ private static Cue parseVttCueBox(ParsableByteArray sampleData, WebvttCue.Builder builder,
+ int remainingCueBoxBytes) throws SubtitleDecoderException {
+ builder.reset();
+ while (remainingCueBoxBytes > 0) {
+ if (remainingCueBoxBytes < BOX_HEADER_SIZE) {
+ throw new SubtitleDecoderException("Incomplete vtt cue box header found.");
+ }
+ int boxSize = sampleData.readInt();
+ int boxType = sampleData.readInt();
+ remainingCueBoxBytes -= BOX_HEADER_SIZE;
+ int payloadLength = boxSize - BOX_HEADER_SIZE;
+ 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());
+ } else {
+ // Other VTTCueBox children are still not supported and are ignored.
+ }
+ }
+ return builder.build();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java
new file mode 100644
index 0000000000..545e8b2511
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java
@@ -0,0 +1,56 @@
+/*
+ * 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.text.webvtt;
+
+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 java.util.Collections;
+import java.util.List;
+
+/**
+ * Representation of a Webvtt subtitle embedded in a MP4 container file.
+ */
+/* package */ final class Mp4WebvttSubtitle implements Subtitle {
+
+ private final List<Cue> cues;
+
+ public Mp4WebvttSubtitle(List<Cue> cueList) {
+ cues = Collections.unmodifiableList(cueList);
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ return timeUs < 0 ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return 1;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ Assertions.checkArgument(index == 0);
+ return 0;
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ return timeUs >= 0 ? cues : Collections.emptyList();
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java
new file mode 100644
index 0000000000..da37cfbdf3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java
@@ -0,0 +1,329 @@
+/*
+ * 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.text.webvtt;
+
+import android.graphics.Typeface;
+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.
+ *
+ * @see <a href="https://w3c.github.io/webvtt/#applying-css-properties">W3C specification - Apply
+ * CSS properties</a>
+ */
+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})
+ 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;
+
+ // Selector properties.
+ private String targetId;
+ private String targetTag;
+ private List<String> targetClasses;
+ private String targetVoice;
+
+ // Style properties.
+ @Nullable private String fontFamily;
+ private int fontColor;
+ private boolean hasFontColor;
+ private int backgroundColor;
+ private boolean hasBackgroundColor;
+ @OptionalBoolean private int linethrough;
+ @OptionalBoolean private int underline;
+ @OptionalBoolean private int bold;
+ @OptionalBoolean private int italic;
+ @FontSizeUnit private int fontSizeUnit;
+ private float fontSize;
+ @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 = "";
+ targetClasses = Collections.emptyList();
+ targetVoice = "";
+ fontFamily = null;
+ hasFontColor = false;
+ hasBackgroundColor = false;
+ linethrough = UNSPECIFIED;
+ underline = UNSPECIFIED;
+ bold = UNSPECIFIED;
+ italic = UNSPECIFIED;
+ fontSizeUnit = UNSPECIFIED;
+ textAlign = null;
+ }
+
+ public void setTargetId(String targetId) {
+ this.targetId = targetId;
+ }
+
+ public void setTargetTagName(String targetTag) {
+ this.targetTag = targetTag;
+ }
+
+ public void setTargetClasses(String[] targetClasses) {
+ this.targetClasses = Arrays.asList(targetClasses);
+ }
+
+ public void setTargetVoice(String targetVoice) {
+ this.targetVoice = targetVoice;
+ }
+
+ /**
+ * Returns a value in a score system compliant with the CSS Specificity rules.
+ *
+ * @see <a href="https://www.w3.org/TR/CSS2/cascade.html">CSS Cascading</a>
+ * <p>The score works as follows:
+ * <ul>
+ * <li>Id match adds 0x40000000 to the score.
+ * <li>Each class and voice match adds 4 to the score.
+ * <li>Tag matching adds 2 to the score.
+ * <li>Universal selector matching scores 1.
+ * </ul>
+ *
+ * @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.
+ * @param classes An array containing the classes the tag belongs to. Must not be null.
+ * @param voice Annotated voice if present, {@code null} otherwise.
+ * @return The score of the match, zero if there is no match.
+ */
+ 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 TextUtils.isEmpty(tag) ? 1 : 0;
+ }
+ int score = 0;
+ score = updateScoreForMatch(score, targetId, id, 0x40000000);
+ score = updateScoreForMatch(score, targetTag, tag, 2);
+ score = updateScoreForMatch(score, targetVoice, voice, 4);
+ if (score == -1 || !Arrays.asList(classes).containsAll(targetClasses)) {
+ return 0;
+ } else {
+ score += targetClasses.size() * 4;
+ }
+ return score;
+ }
+
+ /**
+ * Returns the style or {@link #UNSPECIFIED} when no style information is given.
+ *
+ * @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD}
+ * or {@link #STYLE_BOLD_ITALIC}.
+ */
+ @StyleFlags public int getStyle() {
+ if (bold == UNSPECIFIED && italic == UNSPECIFIED) {
+ return UNSPECIFIED;
+ }
+ return (bold == ON ? STYLE_BOLD : STYLE_NORMAL)
+ | (italic == ON ? STYLE_ITALIC : STYLE_NORMAL);
+ }
+
+ public boolean isLinethrough() {
+ return linethrough == ON;
+ }
+
+ public WebvttCssStyle setLinethrough(boolean linethrough) {
+ this.linethrough = linethrough ? ON : OFF;
+ return this;
+ }
+
+ public boolean isUnderline() {
+ return underline == ON;
+ }
+
+ public WebvttCssStyle setUnderline(boolean underline) {
+ this.underline = underline ? ON : OFF;
+ return this;
+ }
+ public WebvttCssStyle setBold(boolean bold) {
+ this.bold = bold ? ON : OFF;
+ return this;
+ }
+
+ public WebvttCssStyle setItalic(boolean italic) {
+ this.italic = italic ? ON : OFF;
+ return this;
+ }
+
+ @Nullable
+ public String getFontFamily() {
+ return fontFamily;
+ }
+
+ public WebvttCssStyle setFontFamily(@Nullable String fontFamily) {
+ this.fontFamily = Util.toLowerInvariant(fontFamily);
+ return this;
+ }
+
+ public int getFontColor() {
+ if (!hasFontColor) {
+ throw new IllegalStateException("Font color not defined");
+ }
+ return fontColor;
+ }
+
+ public WebvttCssStyle setFontColor(int color) {
+ this.fontColor = color;
+ hasFontColor = true;
+ return this;
+ }
+
+ public boolean hasFontColor() {
+ return hasFontColor;
+ }
+
+ public int getBackgroundColor() {
+ if (!hasBackgroundColor) {
+ throw new IllegalStateException("Background color not defined.");
+ }
+ return backgroundColor;
+ }
+
+ public WebvttCssStyle setBackgroundColor(int backgroundColor) {
+ this.backgroundColor = backgroundColor;
+ hasBackgroundColor = true;
+ return this;
+ }
+
+ public boolean hasBackgroundColor() {
+ return hasBackgroundColor;
+ }
+
+ @Nullable
+ public Layout.Alignment getTextAlign() {
+ return textAlign;
+ }
+
+ public WebvttCssStyle setTextAlign(@Nullable Layout.Alignment textAlign) {
+ this.textAlign = textAlign;
+ return this;
+ }
+
+ public WebvttCssStyle setFontSize(float fontSize) {
+ this.fontSize = fontSize;
+ return this;
+ }
+
+ public WebvttCssStyle setFontSizeUnit(short unit) {
+ this.fontSizeUnit = unit;
+ return this;
+ }
+
+ @FontSizeUnit public int getFontSizeUnit() {
+ return fontSizeUnit;
+ }
+
+ public float getFontSize() {
+ return fontSize;
+ }
+
+ public void cascadeFrom(WebvttCssStyle style) {
+ if (style.hasFontColor) {
+ setFontColor(style.fontColor);
+ }
+ if (style.bold != UNSPECIFIED) {
+ bold = style.bold;
+ }
+ if (style.italic != UNSPECIFIED) {
+ italic = style.italic;
+ }
+ if (style.fontFamily != null) {
+ fontFamily = style.fontFamily;
+ }
+ if (linethrough == UNSPECIFIED) {
+ linethrough = style.linethrough;
+ }
+ if (underline == UNSPECIFIED) {
+ underline = style.underline;
+ }
+ if (textAlign == null) {
+ textAlign = style.textAlign;
+ }
+ if (fontSizeUnit == UNSPECIFIED) {
+ fontSizeUnit = style.fontSizeUnit;
+ fontSize = style.fontSize;
+ }
+ if (style.hasBackgroundColor) {
+ setBackgroundColor(style.backgroundColor);
+ }
+ }
+
+ private static int updateScoreForMatch(
+ int currentScore, String target, @Nullable String actual, int score) {
+ if (target.isEmpty() || currentScore == -1) {
+ return currentScore;
+ }
+ return target.equals(actual) ? currentScore + score : -1;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCue.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCue.java
new file mode 100644
index 0000000000..af701d8f54
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCue.java
@@ -0,0 +1,319 @@
+/*
+ * 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.text.webvtt;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+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;
+
+ 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;
+ }
+
+ /**
+ * Returns whether or not this cue should be placed in the default position and rolled-up with
+ * the other "normal" cues.
+ *
+ * @return Whether this cue should be placed in the default position.
+ */
+ public boolean isNormalCue() {
+ return (line == DIMEN_UNSET && position == DEFAULT_POSITION);
+ }
+
+ /** Builder for WebVTT cues. */
+ @SuppressWarnings("hiding")
+ public static class Builder {
+
+ /**
+ * Valid values for {@link #setTextAlignment(int)}.
+ *
+ * <p>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)}.
+ *
+ * <p>These correspond to the valid values for the 'align' cue setting in the <a
+ * href="https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment">WebVTT spec</a>.
+ */
+ @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 <a
+ * href="https://www.w3.org/TR/webvtt1/#webvtt-cue-start-alignment">align:start</a>.
+ */
+ public static final int TEXT_ALIGNMENT_START = 1;
+
+ /**
+ * See WebVTT's <a
+ * href="https://www.w3.org/TR/webvtt1/#webvtt-cue-center-alignment">align:center</a>.
+ */
+ public static final int TEXT_ALIGNMENT_CENTER = 2;
+
+ /**
+ * See WebVTT's <a href="https://www.w3.org/TR/webvtt1/#webvtt-cue-end-alignment">align:end</a>.
+ */
+ public static final int TEXT_ALIGNMENT_END = 3;
+
+ /**
+ * See WebVTT's <a href="https://www.w3.org/TR/webvtt1/#webvtt-cue-left-alignment">align:left</a>.
+ */
+ public static final int TEXT_ALIGNMENT_LEFT = 4;
+
+ /**
+ * See WebVTT's <a
+ * href="https://www.w3.org/TR/webvtt1/#webvtt-cue-right-alignment">align:right</a>.
+ */
+ public static final int TEXT_ALIGNMENT_RIGHT = 5;
+
+ private static final String TAG = "WebvttCueBuilder";
+
+ private long startTime;
+ private long endTime;
+ @Nullable private CharSequence text;
+ @TextAlignment private int textAlignment;
+ private float line;
+ // 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;
+ @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();
+ }
+
+ public void reset() {
+ startTime = 0;
+ endTime = 0;
+ text = null;
+ // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment
+ textAlignment = TEXT_ALIGNMENT_CENTER;
+ line = Cue.DIMEN_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;
+ // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-size
+ width = 1.0f;
+ }
+
+ // Construction methods.
+
+ public WebvttCue build() {
+ line = computeLine(line, lineType);
+
+ if (position == Cue.DIMEN_UNSET) {
+ position = derivePosition(textAlignment);
+ }
+
+ 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) {
+ startTime = time;
+ return this;
+ }
+
+ public Builder setEndTime(long time) {
+ endTime = time;
+ return this;
+ }
+
+ public Builder setText(CharSequence text) {
+ this.text = text;
+ return this;
+ }
+
+ public Builder setTextAlignment(@TextAlignment int textAlignment) {
+ this.textAlignment = textAlignment;
+ return this;
+ }
+
+ public Builder setLine(float line) {
+ this.line = line;
+ return this;
+ }
+
+ public Builder setLineType(@LineType int lineType) {
+ this.lineType = lineType;
+ return this;
+ }
+
+ public Builder setLineAnchor(@AnchorType int lineAnchor) {
+ this.lineAnchor = lineAnchor;
+ return this;
+ }
+
+ public Builder setPosition(float position) {
+ this.position = position;
+ return this;
+ }
+
+ public Builder setPositionAnchor(@AnchorType int positionAnchor) {
+ this.positionAnchor = positionAnchor;
+ return this;
+ }
+
+ public Builder setWidth(float width) {
+ this.width = width;
+ return this;
+ }
+
+ // 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 {
+ // Steps 4 - 10 (stacking multiple simultaneous cues) are handled by WebvttSubtitle#getCues
+ // and WebvttCue#isNormalCue.
+ return DIMEN_UNSET;
+ }
+ }
+
+ // 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java
new file mode 100644
index 0000000000..b370e67792
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java
@@ -0,0 +1,550 @@
+/*
+ * 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.text.webvtt;
+
+import android.graphics.Typeface;
+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;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.TypefaceSpan;
+import android.text.style.UnderlineSpan;
+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.Collections;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** 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+)(.*)?$");
+
+ private static final Pattern CUE_SETTING_PATTERN = Pattern.compile("(\\S+?):(\\S+)");
+
+ private static final char CHAR_LESS_THAN = '<';
+ private static final char CHAR_GREATER_THAN = '>';
+ private static final char CHAR_SLASH = '/';
+ private static final char CHAR_AMPERSAND = '&';
+ private static final char CHAR_SEMI_COLON = ';';
+ private static final char CHAR_SPACE = ' ';
+
+ private static final String ENTITY_LESS_THAN = "lt";
+ private static final String ENTITY_GREATER_THAN = "gt";
+ private static final String ENTITY_AMPERSAND = "amp";
+ private static final String ENTITY_NON_BREAK_SPACE = "nbsp";
+
+ private static final String TAG_BOLD = "b";
+ private static final String TAG_ITALIC = "i";
+ private static final String TAG_UNDERLINE = "u";
+ private static final String TAG_CLASS = "c";
+ private static final String TAG_VOICE = "v";
+ private static final String TAG_LANG = "lang";
+
+ private static final int STYLE_BOLD = Typeface.BOLD;
+ private static final int STYLE_ITALIC = Typeface.ITALIC;
+
+ private static final String TAG = "WebvttCueParser";
+
+ private final StringBuilder textBuilder;
+
+ public WebvttCueParser() {
+ textBuilder = new StringBuilder();
+ }
+
+ /**
+ * 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 (output parameter).
+ * @param styles List of styles defined by the CSS style blocks preceding the cues.
+ * @return Whether a valid Cue was found.
+ */
+ public boolean parseCue(
+ ParsableByteArray webvttData, WebvttCue.Builder builder, List<WebvttCssStyle> 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);
+ }
+ // 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;
+ }
+
+ /**
+ * Parses a string containing a list of cue settings.
+ *
+ * @param cueSettingsList String containing the settings for a given cue.
+ * @param builder The {@link WebvttCue.Builder} where incremental construction takes place.
+ */
+ /* package */ static void parseCueSettingsList(String cueSettingsList,
+ WebvttCue.Builder builder) {
+ // Parse the cue settings list.
+ Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueSettingsList);
+ while (cueSettingMatcher.find()) {
+ String name = cueSettingMatcher.group(1);
+ String value = cueSettingMatcher.group(2);
+ try {
+ if ("line".equals(name)) {
+ parseLineAttribute(value, builder);
+ } else if ("align".equals(name)) {
+ builder.setTextAlignment(parseTextAlignment(value));
+ } else if ("position".equals(name)) {
+ parsePositionAttribute(value, builder);
+ } else if ("size".equals(name)) {
+ builder.setWidth(WebvttParserUtil.parsePercentage(value));
+ } else {
+ Log.w(TAG, "Unknown cue setting " + name + ":" + value);
+ }
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Skipping bad cue setting: " + cueSettingMatcher.group());
+ }
+ }
+ }
+
+ /**
+ * Parses the text payload of a WebVTT Cue and applies modifications on {@link WebvttCue.Builder}.
+ *
+ * @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 preceding the cues.
+ * @param builder Output builder.
+ */
+ /* package */ static void parseCueText(
+ @Nullable String id, String markup, WebvttCue.Builder builder, List<WebvttCssStyle> styles) {
+ SpannableStringBuilder spannedText = new SpannableStringBuilder();
+ ArrayDeque<StartTag> startTagStack = new ArrayDeque<>();
+ List<StyleMatch> scratchStyleMatches = new ArrayList<>();
+ int pos = 0;
+ while (pos < markup.length()) {
+ char curr = markup.charAt(pos);
+ switch (curr) {
+ case CHAR_LESS_THAN:
+ if (pos + 1 >= markup.length()) {
+ pos++;
+ break; // avoid ArrayOutOfBoundsException
+ }
+ int ltPos = pos;
+ boolean isClosingTag = markup.charAt(ltPos + 1) == CHAR_SLASH;
+ pos = findEndOfTag(markup, ltPos + 1);
+ 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 (!isSupportedTag(tagName)) {
+ continue;
+ }
+ if (isClosingTag) {
+ StartTag startTag;
+ do {
+ if (startTagStack.isEmpty()) {
+ break;
+ }
+ startTag = startTagStack.pop();
+ applySpansForTag(id, startTag, spannedText, styles, scratchStyleMatches);
+ } while(!startTag.name.equals(tagName));
+ } else if (!isVoidTag) {
+ startTagStack.push(StartTag.buildStartTag(fullTagExpression, spannedText.length()));
+ }
+ break;
+ case CHAR_AMPERSAND:
+ int semiColonEndIndex = markup.indexOf(CHAR_SEMI_COLON, pos + 1);
+ int spaceEndIndex = markup.indexOf(CHAR_SPACE, pos + 1);
+ int entityEndIndex = semiColonEndIndex == -1 ? spaceEndIndex
+ : (spaceEndIndex == -1 ? semiColonEndIndex
+ : Math.min(semiColonEndIndex, spaceEndIndex));
+ if (entityEndIndex != -1) {
+ applyEntity(markup.substring(pos + 1, entityEndIndex), spannedText);
+ if (entityEndIndex == spaceEndIndex) {
+ spannedText.append(" ");
+ }
+ pos = entityEndIndex + 1;
+ } else {
+ spannedText.append(curr);
+ pos++;
+ }
+ break;
+ default:
+ spannedText.append(curr);
+ pos++;
+ break;
+ }
+ }
+ // apply unclosed tags
+ while (!startTagStack.isEmpty()) {
+ applySpansForTag(id, startTagStack.pop(), spannedText, styles, scratchStyleMatches);
+ }
+ applySpansForTag(id, StartTag.buildWholeCueVirtualTag(), spannedText, styles,
+ scratchStyleMatches);
+ builder.setText(spannedText);
+ }
+
+ private static boolean parseCue(
+ @Nullable String id,
+ Matcher cueHeaderMatcher,
+ ParsableByteArray webvttData,
+ WebvttCue.Builder builder,
+ StringBuilder textBuilder,
+ List<WebvttCssStyle> styles) {
+ try {
+ // Parse the cue start and end times.
+ builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)))
+ .setEndTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(2)));
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Skipping cue with bad header: " + cueHeaderMatcher.group());
+ return false;
+ }
+
+ parseCueSettingsList(cueHeaderMatcher.group(3), builder);
+
+ // Parse the cue text.
+ textBuilder.setLength(0);
+ for (String line = webvttData.readLine();
+ !TextUtils.isEmpty(line);
+ line = webvttData.readLine()) {
+ if (textBuilder.length() > 0) {
+ textBuilder.append("\n");
+ }
+ textBuilder.append(line.trim());
+ }
+ parseCueText(id, textBuilder.toString(), builder, styles);
+ return true;
+ }
+
+ // Internal methods
+
+ 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);
+ }
+ if (s.endsWith("%")) {
+ builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION);
+ } else {
+ int lineNumber = Integer.parseInt(s);
+ if (lineNumber < 0) {
+ // WebVTT defines line -1 as last visible row when lineAnchor is ANCHOR_TYPE_START, where-as
+ // Cue defines it to be the first row that's not visible.
+ lineNumber--;
+ }
+ builder.setLine(lineNumber).setLineType(Cue.LINE_TYPE_NUMBER);
+ }
+ }
+
+ 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);
+ }
+ builder.setPosition(WebvttParserUtil.parsePercentage(s));
+ }
+
+ @Cue.AnchorType
+ private static int parsePositionAnchor(String s) {
+ switch (s) {
+ case "start":
+ return Cue.ANCHOR_TYPE_START;
+ case "center":
+ case "middle":
+ return Cue.ANCHOR_TYPE_MIDDLE;
+ case "end":
+ return Cue.ANCHOR_TYPE_END;
+ default:
+ Log.w(TAG, "Invalid anchor value: " + s);
+ return Cue.TYPE_UNSET;
+ }
+ }
+
+ @WebvttCue.Builder.TextAlignment
+ private static int parseTextAlignment(String s) {
+ switch (s) {
+ case "start":
+ return WebvttCue.Builder.TEXT_ALIGNMENT_START;
+ case "left":
+ return WebvttCue.Builder.TEXT_ALIGNMENT_LEFT;
+ case "center":
+ case "middle":
+ return WebvttCue.Builder.TEXT_ALIGNMENT_CENTER;
+ case "end":
+ return WebvttCue.Builder.TEXT_ALIGNMENT_END;
+ case "right":
+ return WebvttCue.Builder.TEXT_ALIGNMENT_RIGHT;
+ default:
+ Log.w(TAG, "Invalid alignment value: " + s);
+ // Default value: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment
+ return WebvttCue.Builder.TEXT_ALIGNMENT_CENTER;
+ }
+ }
+
+ /**
+ * Find end of tag (&gt;). The position returned is the position of the &gt; plus one (exclusive).
+ *
+ * @param markup The WebVTT cue markup to be parsed.
+ * @param startPos The position from where to start searching for the end of tag.
+ * @return The position of the end of tag plus 1 (one).
+ */
+ private static int findEndOfTag(String markup, int startPos) {
+ int index = markup.indexOf(CHAR_GREATER_THAN, startPos);
+ return index == -1 ? markup.length() : index + 1;
+ }
+
+ private static void applyEntity(String entity, SpannableStringBuilder spannedText) {
+ switch (entity) {
+ case ENTITY_LESS_THAN:
+ spannedText.append('<');
+ break;
+ case ENTITY_GREATER_THAN:
+ spannedText.append('>');
+ break;
+ case ENTITY_NON_BREAK_SPACE:
+ spannedText.append(' ');
+ break;
+ case ENTITY_AMPERSAND:
+ spannedText.append('&');
+ break;
+ default:
+ Log.w(TAG, "ignoring unsupported entity: '&" + entity + ";'");
+ break;
+ }
+ }
+
+ private static boolean isSupportedTag(String tagName) {
+ switch (tagName) {
+ case TAG_BOLD:
+ case TAG_CLASS:
+ case TAG_ITALIC:
+ case TAG_LANG:
+ case TAG_UNDERLINE:
+ case TAG_VOICE:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static void applySpansForTag(
+ @Nullable String cueId,
+ StartTag startTag,
+ SpannableStringBuilder text,
+ List<WebvttCssStyle> styles,
+ List<StyleMatch> scratchStyleMatches) {
+ int start = startTag.position;
+ int end = text.length();
+ switch(startTag.name) {
+ case TAG_BOLD:
+ text.setSpan(new StyleSpan(STYLE_BOLD), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TAG_ITALIC:
+ text.setSpan(new StyleSpan(STYLE_ITALIC), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TAG_UNDERLINE:
+ text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TAG_CLASS:
+ case TAG_LANG:
+ case TAG_VOICE:
+ case "": // Case of the "whole cue" virtual tag.
+ break;
+ default:
+ return;
+ }
+ scratchStyleMatches.clear();
+ getApplicableStyles(styles, cueId, startTag, scratchStyleMatches);
+ int styleMatchesCount = scratchStyleMatches.size();
+ for (int i = 0; i < styleMatchesCount; i++) {
+ applyStyleToText(text, scratchStyleMatches.get(i).style, start, end);
+ }
+ }
+
+ private static void applyStyleToText(SpannableStringBuilder spannedText, WebvttCssStyle style,
+ int start, int end) {
+ if (style == null) {
+ return;
+ }
+ if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) {
+ spannedText.setSpan(new StyleSpan(style.getStyle()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.isLinethrough()) {
+ spannedText.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.isUnderline()) {
+ spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.hasFontColor()) {
+ spannedText.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.hasBackgroundColor()) {
+ spannedText.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.getFontFamily() != null) {
+ spannedText.setSpan(new TypefaceSpan(style.getFontFamily()), 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:
+ spannedText.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case WebvttCssStyle.FONT_SIZE_UNIT_EM:
+ spannedText.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT:
+ spannedText.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case WebvttCssStyle.UNSPECIFIED:
+ // Do nothing.
+ break;
+ }
+ }
+
+ /**
+ * Returns the tag name for the given tag contents.
+ *
+ * @param tagExpression Characters between &amp;lt: and &amp;gt; of a start or end tag.
+ * @return The name of tag.
+ */
+ private static String getTagName(String tagExpression) {
+ tagExpression = tagExpression.trim();
+ Assertions.checkArgument(!tagExpression.isEmpty());
+ return Util.splitAtFirst(tagExpression, "[ \\.]")[0];
+ }
+
+ private static void getApplicableStyles(
+ List<WebvttCssStyle> declaredStyles,
+ @Nullable String id,
+ StartTag tag,
+ List<StyleMatch> output) {
+ int styleCount = declaredStyles.size();
+ for (int i = 0; i < styleCount; i++) {
+ WebvttCssStyle style = declaredStyles.get(i);
+ int score = style.getSpecificityScore(id, tag.name, tag.classes, tag.voice);
+ if (score > 0) {
+ output.add(new StyleMatch(score, style));
+ }
+ }
+ Collections.sort(output);
+ }
+
+ private static final class StyleMatch implements Comparable<StyleMatch> {
+
+ public final int score;
+ public final WebvttCssStyle style;
+
+ public StyleMatch(int score, WebvttCssStyle style) {
+ this.score = score;
+ this.style = style;
+ }
+
+ @Override
+ public int compareTo(@NonNull StyleMatch another) {
+ return this.score - another.score;
+ }
+
+ }
+
+ private static final class StartTag {
+
+ private static final String[] NO_CLASSES = new String[0];
+
+ public final String name;
+ public final int position;
+ public final String voice;
+ public final String[] classes;
+
+ private StartTag(String name, int position, String voice, String[] classes) {
+ this.position = position;
+ this.name = name;
+ this.voice = voice;
+ this.classes = classes;
+ }
+
+ public static StartTag buildStartTag(String fullTagExpression, int position) {
+ fullTagExpression = fullTagExpression.trim();
+ Assertions.checkArgument(!fullTagExpression.isEmpty());
+ int voiceStartIndex = fullTagExpression.indexOf(" ");
+ String voice;
+ if (voiceStartIndex == -1) {
+ voice = "";
+ } else {
+ voice = fullTagExpression.substring(voiceStartIndex).trim();
+ fullTagExpression = fullTagExpression.substring(0, voiceStartIndex);
+ }
+ String[] nameAndClasses = Util.split(fullTagExpression, "\\.");
+ String name = nameAndClasses[0];
+ String[] classes;
+ if (nameAndClasses.length > 1) {
+ classes = Util.nullSafeArrayCopyOfRange(nameAndClasses, 1, nameAndClasses.length);
+ } else {
+ classes = NO_CLASSES;
+ }
+ return new StartTag(name, position, voice, classes);
+ }
+
+ public static StartTag buildWholeCueVirtualTag() {
+ return new StartTag("", 0, "", new String[0]);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java
new file mode 100644
index 0000000000..a70a49e82e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java
@@ -0,0 +1,125 @@
+/*
+ * 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.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;
+import java.util.List;
+
+/**
+ * A {@link SimpleSubtitleDecoder} for WebVTT.
+ * <p>
+ * @see <a href="http://dev.w3.org/html5/webvtt">WebVTT specification</a>
+ */
+public final class WebvttDecoder extends SimpleSubtitleDecoder {
+
+ private static final int EVENT_NONE = -1;
+ private static final int EVENT_END_OF_FILE = 0;
+ private static final int EVENT_COMMENT = 1;
+ private static final int EVENT_STYLE_BLOCK = 2;
+ private static final int EVENT_CUE = 3;
+
+ private static final String COMMENT_START = "NOTE";
+ private static final String STYLE_START = "STYLE";
+
+ private final WebvttCueParser cueParser;
+ private final ParsableByteArray parsableWebvttData;
+ private final WebvttCue.Builder webvttCueBuilder;
+ private final CssParser cssParser;
+ private final List<WebvttCssStyle> definedStyles;
+
+ public WebvttDecoder() {
+ super("WebvttDecoder");
+ cueParser = new WebvttCueParser();
+ parsableWebvttData = new ParsableByteArray();
+ webvttCueBuilder = new WebvttCue.Builder();
+ cssParser = new CssParser();
+ definedStyles = new ArrayList<>();
+ }
+
+ @Override
+ protected Subtitle decode(byte[] bytes, int length, boolean reset)
+ throws SubtitleDecoderException {
+ parsableWebvttData.reset(bytes, length);
+ // Initialization for consistent starting state.
+ webvttCueBuilder.reset();
+ definedStyles.clear();
+
+ // Validate the first line of the header, and skip the remainder.
+ try {
+ WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData);
+ } catch (ParserException e) {
+ throw new SubtitleDecoderException(e);
+ }
+ while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}
+
+ int event;
+ ArrayList<WebvttCue> subtitles = new ArrayList<>();
+ while ((event = getNextEvent(parsableWebvttData)) != EVENT_END_OF_FILE) {
+ if (event == EVENT_COMMENT) {
+ skipComment(parsableWebvttData);
+ } else if (event == EVENT_STYLE_BLOCK) {
+ if (!subtitles.isEmpty()) {
+ throw new SubtitleDecoderException("A style block was found after the first cue.");
+ }
+ parsableWebvttData.readLine(); // Consume the "STYLE" header.
+ definedStyles.addAll(cssParser.parseBlock(parsableWebvttData));
+ } else if (event == EVENT_CUE) {
+ if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) {
+ subtitles.add(webvttCueBuilder.build());
+ webvttCueBuilder.reset();
+ }
+ }
+ }
+ return new WebvttSubtitle(subtitles);
+ }
+
+ /**
+ * Positions the input right before the next event, and returns the kind of event found. Does not
+ * consume any data from such event, if any.
+ *
+ * @return The kind of event found.
+ */
+ private static int getNextEvent(ParsableByteArray parsableWebvttData) {
+ int foundEvent = EVENT_NONE;
+ int currentInputPosition = 0;
+ while (foundEvent == EVENT_NONE) {
+ currentInputPosition = parsableWebvttData.getPosition();
+ String line = parsableWebvttData.readLine();
+ if (line == null) {
+ foundEvent = EVENT_END_OF_FILE;
+ } else if (STYLE_START.equals(line)) {
+ foundEvent = EVENT_STYLE_BLOCK;
+ } else if (line.startsWith(COMMENT_START)) {
+ foundEvent = EVENT_COMMENT;
+ } else {
+ foundEvent = EVENT_CUE;
+ }
+ }
+ parsableWebvttData.setPosition(currentInputPosition);
+ return foundEvent;
+ }
+
+ private static void skipComment(ParsableByteArray parsableWebvttData) {
+ while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java
new file mode 100644
index 0000000000..b87d014de0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java
@@ -0,0 +1,119 @@
+/*
+ * 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.text.webvtt;
+
+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;
+
+/**
+ * Utility methods for parsing WebVTT data.
+ */
+public final class WebvttParserUtil {
+
+ private static final Pattern COMMENT = Pattern.compile("^NOTE([ \t].*)?$");
+ private static final String WEBVTT_HEADER = "WEBVTT";
+
+ private WebvttParserUtil() {}
+
+ /**
+ * Reads and validates the first line of a WebVTT file.
+ *
+ * @param input The input from which the line should be read.
+ * @throws ParserException If the line isn't the start of a valid WebVTT file.
+ */
+ 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.
+ *
+ * @param timestamp The timestamp string.
+ * @return The parsed timestamp in microseconds.
+ * @throws NumberFormatException If the timestamp could not be parsed.
+ */
+ public static long parseTimestampUs(String timestamp) throws NumberFormatException {
+ long value = 0;
+ String[] parts = Util.splitAtFirst(timestamp, "\\.");
+ String[] subparts = Util.split(parts[0], ":");
+ for (String subpart : subparts) {
+ value = (value * 60) + Long.parseLong(subpart);
+ }
+ value *= 1000;
+ if (parts.length == 2) {
+ value += Long.parseLong(parts[1]);
+ }
+ return value * 1000;
+ }
+
+ /**
+ * Parses a percentage string.
+ *
+ * @param s The percentage string.
+ * @return The parsed value, where 1.0 represents 100%.
+ * @throws NumberFormatException If the percentage could not be parsed.
+ */
+ public static float parsePercentage(String s) throws NumberFormatException {
+ if (!s.endsWith("%")) {
+ throw new NumberFormatException("Percentages must end with %");
+ }
+ return Float.parseFloat(s.substring(0, s.length() - 1)) / 100;
+ }
+
+ /**
+ * Reads lines up to and including the next WebVTT cue header.
+ *
+ * @param input The input from which lines should be read.
+ * @return A {@link Matcher} for the WebVTT cue header, or null if the end of the input was
+ * 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) {
+ @Nullable String line;
+ while ((line = input.readLine()) != null) {
+ if (COMMENT.matcher(line).matches()) {
+ // Skip until the end of the comment block.
+ while ((line = input.readLine()) != null && !line.isEmpty()) {}
+ } else {
+ Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(line);
+ if (cueHeaderMatcher.matches()) {
+ return cueHeaderMatcher;
+ }
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java
new file mode 100644
index 0000000000..558c699eba
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java
@@ -0,0 +1,115 @@
+/*
+ * 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.text.webvtt;
+
+import android.text.SpannableStringBuilder;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A representation of a WebVTT subtitle.
+ */
+/* package */ final class WebvttSubtitle implements Subtitle {
+
+ private final List<WebvttCue> cues;
+ private final int numCues;
+ private final long[] cueTimesUs;
+ private final long[] sortedCueTimesUs;
+
+ /**
+ * @param cues A list of the cues in this subtitle.
+ */
+ public WebvttSubtitle(List<WebvttCue> cues) {
+ this.cues = cues;
+ numCues = cues.size();
+ cueTimesUs = new long[2 * numCues];
+ for (int cueIndex = 0; cueIndex < numCues; cueIndex++) {
+ WebvttCue cue = cues.get(cueIndex);
+ int arrayIndex = cueIndex * 2;
+ cueTimesUs[arrayIndex] = cue.startTime;
+ cueTimesUs[arrayIndex + 1] = cue.endTime;
+ }
+ sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length);
+ Arrays.sort(sortedCueTimesUs);
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ int index = Util.binarySearchCeil(sortedCueTimesUs, timeUs, false, false);
+ return index < sortedCueTimesUs.length ? index : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return sortedCueTimesUs.length;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ Assertions.checkArgument(index >= 0);
+ Assertions.checkArgument(index < sortedCueTimesUs.length);
+ return sortedCueTimesUs[index];
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ List<Cue> 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])) {
+ 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
+ // normal cues, otherwise we can just append the single normal cue
+ if (firstNormalCue == null) {
+ firstNormalCue = cue;
+ } else if (normalCueTextBuilder == null) {
+ normalCueTextBuilder = new SpannableStringBuilder();
+ normalCueTextBuilder
+ .append(Assertions.checkNotNull(firstNormalCue.text))
+ .append("\n")
+ .append(Assertions.checkNotNull(cue.text));
+ } else {
+ normalCueTextBuilder.append("\n").append(Assertions.checkNotNull(cue.text));
+ }
+ } else {
+ list.add(cue);
+ }
+ }
+ }
+ if (normalCueTextBuilder != null) {
+ // there were multiple normal cues, so create a new cue with all of the text
+ 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);
+ }
+ return list;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/package-info.java
new file mode 100644
index 0000000000..e2c014d539
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
new file mode 100644
index 0000000000..33f8606e9b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
@@ -0,0 +1,761 @@
+/*
+ * 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.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.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
+ * of highest quality given the current network conditions and the state of the buffer.
+ */
+public class AdaptiveTrackSelection extends BaseTrackSelection {
+
+ /** Factory for {@link AdaptiveTrackSelection} instances. */
+ public static class Factory implements TrackSelection.Factory {
+
+ @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;
+
+ /** 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_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
+ DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
+ Clock.DEFAULT);
+ }
+
+ /**
+ * @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.
+ */
+ 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.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs;
+ this.maxDurationForQualityDecreaseMs = maxDurationForQualityDecreaseMs;
+ this.minDurationToRetainAfterDiscardMs = minDurationToRetainAfterDiscardMs;
+ this.bandwidthFraction = bandwidthFraction;
+ this.bufferedFractionToLiveEdgeForQualityIncrease =
+ bufferedFractionToLiveEdgeForQualityIncrease;
+ this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs;
+ this.clock = clock;
+ }
+
+ @Override
+ 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<AdaptiveTrackSelection> 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_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.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 BandwidthProvider bandwidthProvider;
+ private final long minDurationForQualityIncreaseUs;
+ private final long maxDurationForQualityDecreaseUs;
+ private final long minDurationToRetainAfterDiscardUs;
+ 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}.
+ * @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.
+ */
+ public AdaptiveTrackSelection(TrackGroup group, int[] tracks,
+ BandwidthMeter bandwidthMeter) {
+ 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_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
+ DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
+ Clock.DEFAULT);
+ }
+
+ /**
+ * @param group The {@link TrackGroup}.
+ * @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 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
+ * 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
+ * condition has changed. This is the minimum duration between 2 consecutive buffer
+ * reevaluation calls.
+ */
+ 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.bandwidthProvider = bandwidthProvider;
+ this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
+ this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
+ this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
+ 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 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<? extends MediaChunk> 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;
+ 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 (selectedFormat.bitrate < currentFormat.bitrate
+ && bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
+ // The selected track is a lower quality, but we have sufficient buffer to defer switching
+ // down for now.
+ selectedIndex = currentSelectedIndex;
+ }
+ }
+ // If we adapted, update the trigger.
+ if (selectedIndex != currentSelectedIndex) {
+ reason = C.SELECTION_REASON_ADAPTIVE;
+ }
+ }
+
+ @Override
+ public int getSelectedIndex() {
+ return selectedIndex;
+ }
+
+ @Override
+ public int getSelectionReason() {
+ return reason;
+ }
+
+ @Override
+ @Nullable
+ public Object getSelectionData() {
+ return null;
+ }
+
+ @Override
+ public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
+ long nowMs = clock.elapsedRealtime();
+ if (!shouldEvaluateQueueSize(nowMs)) {
+ return queue.size();
+ }
+
+ lastBufferEvaluationMs = nowMs;
+ if (queue.isEmpty()) {
+ return 0;
+ }
+
+ int queueSize = queue.size();
+ 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(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
+ // track.
+ for (int i = 0; i < queueSize; i++) {
+ MediaChunk chunk = queue.get(i);
+ Format format = chunk.trackFormat;
+ 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
+ && format.height < idealFormat.height) {
+ return i;
+ }
+ }
+ 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 Clock#elapsedRealtime()}, or {@link
+ * Long#MIN_VALUE} to ignore blacklisting.
+ */
+ private int determineIdealSelectedIndex(long nowMs) {
+ 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 (canSelectFormat(format, format.bitrate, playbackSpeed, effectiveBitrate)) {
+ return i;
+ } else {
+ lowestBitrateNonBlacklistedIndex = i;
+ }
+ }
+ }
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java
new file mode 100644
index 0000000000..d7e94cb561
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java
@@ -0,0 +1,217 @@
+/*
+ * 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.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;
+
+/**
+ * An abstract base class suitable for most {@link TrackSelection} implementations.
+ */
+public abstract class BaseTrackSelection implements TrackSelection {
+
+ /**
+ * The selected {@link TrackGroup}.
+ */
+ protected final TrackGroup group;
+ /**
+ * The number of selected tracks within the {@link TrackGroup}. Always greater than zero.
+ */
+ protected final int length;
+ /**
+ * The indices of the selected tracks in {@link #group}, in order of decreasing bandwidth.
+ */
+ protected final int[] tracks;
+
+ /**
+ * The {@link Format}s of the selected tracks, in order of decreasing bandwidth.
+ */
+ private final Format[] formats;
+ /**
+ * Selected track blacklist timestamps, in order of decreasing bandwidth.
+ */
+ private final long[] blacklistUntilTimes;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * @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 BaseTrackSelection(TrackGroup group, int... tracks) {
+ Assertions.checkState(tracks.length > 0);
+ this.group = Assertions.checkNotNull(group);
+ this.length = tracks.length;
+ // Set the formats, sorted in order of decreasing bandwidth.
+ formats = new Format[length];
+ for (int i = 0; i < tracks.length; i++) {
+ formats[i] = group.getFormat(tracks[i]);
+ }
+ Arrays.sort(formats, new DecreasingBandwidthComparator());
+ // Set the format indices in the same order.
+ this.tracks = new int[length];
+ for (int i = 0; i < length; i++) {
+ this.tracks[i] = group.indexOf(formats[i]);
+ }
+ blacklistUntilTimes = new long[length];
+ }
+
+ @Override
+ public void enable() {
+ // Do nothing.
+ }
+
+ @Override
+ public void disable() {
+ // Do nothing.
+ }
+
+ @Override
+ public final TrackGroup getTrackGroup() {
+ return group;
+ }
+
+ @Override
+ public final int length() {
+ return tracks.length;
+ }
+
+ @Override
+ public final Format getFormat(int index) {
+ return formats[index];
+ }
+
+ @Override
+ public final int getIndexInTrackGroup(int index) {
+ return tracks[index];
+ }
+
+ @Override
+ @SuppressWarnings("ReferenceEquality")
+ public final int indexOf(Format format) {
+ for (int i = 0; i < length; i++) {
+ if (formats[i] == format) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public final int indexOf(int indexInTrackGroup) {
+ for (int i = 0; i < length; i++) {
+ if (tracks[i] == indexInTrackGroup) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public final Format getSelectedFormat() {
+ return formats[getSelectedIndex()];
+ }
+
+ @Override
+ public final int getSelectedIndexInTrackGroup() {
+ return tracks[getSelectedIndex()];
+ }
+
+ @Override
+ public void onPlaybackSpeed(float playbackSpeed) {
+ // Do nothing.
+ }
+
+ @Override
+ public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
+ return queue.size();
+ }
+
+ @Override
+ public final boolean blacklist(int index, long blacklistDurationMs) {
+ long nowMs = SystemClock.elapsedRealtime();
+ boolean canBlacklist = isBlacklisted(index, nowMs);
+ for (int i = 0; i < length && !canBlacklist; i++) {
+ canBlacklist = i != index && !isBlacklisted(i, nowMs);
+ }
+ if (!canBlacklist) {
+ return false;
+ }
+ blacklistUntilTimes[index] =
+ Math.max(
+ blacklistUntilTimes[index],
+ Util.addWithOverflowDefault(nowMs, blacklistDurationMs, Long.MAX_VALUE));
+ return true;
+ }
+
+ /**
+ * Returns whether the track at the specified index in the selection is blacklisted.
+ *
+ * @param index The index of the track in the selection.
+ * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}.
+ */
+ protected final boolean isBlacklisted(int index, long nowMs) {
+ return blacklistUntilTimes[index] > nowMs;
+ }
+
+ // Object overrides.
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ hashCode = 31 * System.identityHashCode(group) + Arrays.hashCode(tracks);
+ }
+ return hashCode;
+ }
+
+ // Track groups are compared by identity not value, as distinct groups may have the same value.
+ @Override
+ @SuppressWarnings({"ReferenceEquality", "EqualsGetClass"})
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ BaseTrackSelection other = (BaseTrackSelection) obj;
+ return group == other.group && Arrays.equals(tracks, other.tracks);
+ }
+
+ /**
+ * Sorts {@link Format} objects in order of decreasing bandwidth.
+ */
+ private static final class DecreasingBandwidthComparator implements Comparator<Format> {
+
+ @Override
+ public int compare(Format a, Format b) {
+ return b.bitrate - a.bitrate;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java
new file mode 100644
index 0000000000..735889bfaa
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<TrackSelection.Factory, LoadControl> 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<? extends MediaChunk> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
new file mode 100644
index 0000000000..549e5991b9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
@@ -0,0 +1,2827 @@
+/*
+ * 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.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 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)}.
+ *
+ * <h3>Modifying parameters</h3>
+ *
+ * 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:
+ *
+ * <pre>{@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);
+ * }</pre>
+ *
+ * Convenience methods and chaining allow this to be written more concisely as:
+ *
+ * <pre>{@code
+ * trackSelector.setParameters(
+ * trackSelector
+ * .buildUponParameters()
+ * .setMaxVideoSizeSd()
+ * .setPreferredAudioLanguage("deu"));
+ * }</pre>
+ *
+ * Selection {@link Parameters} support many different options, some of which are described below.
+ *
+ * <h3>Selecting specific tracks</h3>
+ *
+ * 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:
+ *
+ * <pre>{@code
+ * MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
+ * TrackGroupArray rendererTrackGroups = mappedTrackInfo == null ? null
+ * : mappedTrackInfo.getTrackGroups(rendererIndex);
+ * }</pre>
+ *
+ * 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}:
+ *
+ * <pre>{@code
+ * SelectionOverride selectionOverride = new SelectionOverride(groupIndex, trackIndices);
+ * trackSelector.setParameters(
+ * trackSelector
+ * .buildUponParameters()
+ * .setSelectionOverride(rendererIndex, rendererTrackGroups, selectionOverride));
+ * }</pre>
+ *
+ * <h3>Constraint based track selection</h3>
+ *
+ * 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:
+ *
+ * <pre>{@code
+ * trackSelector.setParameters(
+ * trackSelector
+ * .buildUponParameters()
+ * .setMaxVideoSizeSd()
+ * .setPreferredAudioLanguage("deu"));
+ * }</pre>
+ *
+ * There are several benefits to using constraint based track selection instead of specific track
+ * overrides:
+ *
+ * <ul>
+ * <li>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).
+ * <li>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.
+ * </ul>
+ *
+ * <h3>Disabling renderers</h3>
+ *
+ * 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.
+ *
+ * <h3>Tunneling</h3>
+ *
+ * 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 {
+
+ /**
+ * 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 ParametersBuilder extends TrackSelectionParameters.Builder {
+
+ // 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;
+
+ private final SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>>
+ selectionOverrides;
+ private final SparseBooleanArray rendererDisabledFlags;
+
+ /**
+ * @deprecated {@link Context} constraints will not be set using this constructor. Use {@link
+ * #ParametersBuilder(Context)} instead.
+ */
+ @Deprecated
+ @SuppressWarnings({"deprecation"})
+ public ParametersBuilder() {
+ super();
+ setInitialValuesWithoutContext();
+ selectionOverrides = new SparseArray<>();
+ rendererDisabledFlags = new SparseBooleanArray();
+ }
+
+ /**
+ * Creates a builder with default initial values.
+ *
+ * @param context Any context.
+ */
+
+ 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;
+ return this;
+ }
+
+ /**
+ * 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 This builder.
+ */
+ public ParametersBuilder setExceedVideoConstraintsIfNecessary(
+ boolean exceedVideoConstraintsIfNecessary) {
+ this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary;
+ return this;
+ }
+
+ /**
+ * Sets whether to allow adaptive video selections containing mixed MIME types.
+ *
+ * <p>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 ParametersBuilder setAllowVideoMixedMimeTypeAdaptiveness(
+ boolean allowVideoMixedMimeTypeAdaptiveness) {
+ this.allowVideoMixedMimeTypeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness;
+ return this;
+ }
+
+ /**
+ * 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 viewportOrientationMayChange Whether the viewport orientation may change during
+ * playback.
+ * @return This builder.
+ */
+ 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;
+ }
+
+ /**
+ * Sets the maximum allowed audio channel count.
+ *
+ * @param maxAudioChannelCount Maximum allowed audio channel count.
+ * @return This builder.
+ */
+ public ParametersBuilder setMaxAudioChannelCount(int maxAudioChannelCount) {
+ this.maxAudioChannelCount = maxAudioChannelCount;
+ return this;
+ }
+
+ /**
+ * Sets the maximum allowed audio bitrate.
+ *
+ * @param maxAudioBitrate Maximum allowed audio bitrate in bits per second.
+ * @return This builder.
+ */
+ 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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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 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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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}.
+ *
+ * <p>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.
+ *
+ * <p>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<TrackGroupArray, @NullableType SelectionOverride> 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<TrackGroupArray, @NullableType SelectionOverride> 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<TrackGroupArray, @NullableType SelectionOverride> 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<Map<TrackGroupArray, @NullableType SelectionOverride>>
+ cloneSelectionOverrides(
+ SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> selectionOverrides) {
+ SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> 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}.
+ *
+ * <p>If possible, use {@link #getDefaults(Context)} instead.
+ *
+ * <p>This instance will not have the following settings:
+ *
+ * <ul>
+ * <li>{@link ParametersBuilder#setViewportSizeToPhysicalDisplaySize(Context, boolean)
+ * Viewport constraints} configured for the primary display.
+ * <li>{@link
+ * ParametersBuilder#setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(Context)
+ * Preferred text language and role flags} configured to the accessibility settings of
+ * {@link android.view.accessibility.CaptioningManager}.
+ * </ul>
+ */
+ @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).
+ *
+ * <p>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).
+ *
+ * <p>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.
+ *
+ * <p>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<Map<TrackGroupArray, @NullableType SelectionOverride>>
+ 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<Map<TrackGroupArray, @NullableType SelectionOverride>> 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<TrackGroupArray, @NullableType SelectionOverride> 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<TrackGroupArray, @NullableType SelectionOverride> 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;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ Parameters other = (Parameters) obj;
+ return super.equals(obj)
+ // Video
+ && maxVideoWidth == other.maxVideoWidth
+ && maxVideoHeight == other.maxVideoHeight
+ && maxVideoFrameRate == other.maxVideoFrameRate
+ && maxVideoBitrate == other.maxVideoBitrate
+ && 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 = 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 + (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<Parameters> CREATOR =
+ new Parcelable.Creator<Parameters>() {
+
+ @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<Map<TrackGroupArray, @NullableType SelectionOverride>>
+ readSelectionOverrides(Parcel in) {
+ int renderersWithOverridesCount = in.readInt();
+ SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> selectionOverrides =
+ new SparseArray<>(renderersWithOverridesCount);
+ for (int i = 0; i < renderersWithOverridesCount; i++) {
+ int rendererIndex = in.readInt();
+ int overrideCount = in.readInt();
+ Map<TrackGroupArray, @NullableType SelectionOverride> 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<Map<TrackGroupArray, @NullableType SelectionOverride>> selectionOverrides) {
+ int renderersWithOverridesCount = selectionOverrides.size();
+ dest.writeInt(renderersWithOverridesCount);
+ for (int i = 0; i < renderersWithOverridesCount; i++) {
+ int rendererIndex = selectionOverrides.keyAt(i);
+ Map<TrackGroupArray, @NullableType SelectionOverride> overrides =
+ selectionOverrides.valueAt(i);
+ int overrideCount = overrides.size();
+ dest.writeInt(rendererIndex);
+ dest.writeInt(overrideCount);
+ for (Map.Entry<TrackGroupArray, @NullableType SelectionOverride> 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<Map<TrackGroupArray, @NullableType SelectionOverride>> first,
+ SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> 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<TrackGroupArray, @NullableType SelectionOverride> first,
+ Map<TrackGroupArray, @NullableType SelectionOverride> second) {
+ int firstSize = first.size();
+ if (second.size() != firstSize) {
+ return false;
+ }
+ for (Map.Entry<TrackGroupArray, @NullableType SelectionOverride> 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<SelectionOverride> CREATOR =
+ new Parcelable.Creator<SelectionOverride>() {
+
+ @Override
+ public SelectionOverride createFromParcel(Parcel in) {
+ return new SelectionOverride(in);
+ }
+
+ @Override
+ public SelectionOverride[] newArray(int size) {
+ return new SelectionOverride[size];
+ }
+ };
+ }
+
+ /**
+ * If a dimension (i.e. width or height) of a video is greater or equal to this fraction of the
+ * corresponding viewport dimension, then the video is considered as filling the viewport (in that
+ * dimension).
+ */
+ private static final float FRACTION_TO_CONSIDER_FULLSCREEN = 0.98f;
+ private static final int[] NO_TRACKS = new int[0];
+ private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000;
+
+ private final TrackSelection.Factory trackSelectionFactory;
+ private final AtomicReference<Parameters> parametersReference;
+
+ private boolean allowMultipleAdaptiveSelections;
+
+ /** @deprecated Use {@link #DefaultTrackSelector(Context)} instead. */
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public DefaultTrackSelector() {
+ this(new AdaptiveTrackSelection.Factory());
+ }
+
+ /**
+ * @deprecated Use {@link #DefaultTrackSelector(Context)} instead. The bandwidth meter should be
+ * passed directly to the player in {@link
+ * com.google.android.exoplayer2.SimpleExoPlayer.Builder}.
+ */
+ @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 parameters The parameters for track selection.
+ */
+ 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 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.
+ *
+ * <p>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 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 (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++) {
+ 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.
+ *
+ * <p>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<TrackSelection.Definition, AudioTrackScore> 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_TEXT:
+ Pair<TrackSelection.Definition, TextTrackScore> 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:
+ definitions[i] =
+ selectOtherTrack(
+ trackType, mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params);
+ break;
+ }
+ }
+
+ return definitions;
+ }
+
+ // Video track selection implementation.
+
+ /**
+ * 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 (definition == null) {
+ definition = selectFixedVideoTrack(groups, formatSupports, params);
+ }
+ return definition;
+ }
+
+ @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,
+ params.maxVideoWidth,
+ params.maxVideoHeight,
+ params.maxVideoFrameRate,
+ params.maxVideoBitrate,
+ params.viewportWidth,
+ params.viewportHeight,
+ params.viewportOrientationMayChange);
+ if (adaptiveTracks.length > 0) {
+ return new TrackSelection.Definition(group, adaptiveTracks);
+ }
+ }
+ return null;
+ }
+
+ 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<Integer> selectedTrackIndices = getViewportFilteredTrackIndices(group, viewportWidth,
+ viewportHeight, viewportOrientationMayChange);
+ if (selectedTrackIndices.size() < 2) {
+ return NO_TRACKS;
+ }
+
+ String selectedMimeType = null;
+ if (!allowMixedMimeTypes) {
+ // Select the mime type for which we have the most adaptive tracks.
+ 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,
+ maxVideoFrameRate,
+ maxVideoBitrate,
+ selectedTrackIndices);
+ if (countForMimeType > selectedMimeTypeTrackCount) {
+ selectedMimeType = sampleMimeType;
+ selectedMimeTypeTrackCount = countForMimeType;
+ }
+ }
+ }
+ }
+
+ // Filter by the selected mime type.
+ 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,
+ @Capabilities int[] formatSupport,
+ int requiredAdaptiveSupport,
+ @Nullable String mimeType,
+ int maxVideoWidth,
+ int maxVideoHeight,
+ int maxVideoFrameRate,
+ int maxVideoBitrate,
+ List<Integer> 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,
+ maxVideoFrameRate,
+ maxVideoBitrate)) {
+ adaptiveTrackCount++;
+ }
+ }
+ return adaptiveTrackCount;
+ }
+
+ private static void filterAdaptiveVideoTrackCountForMimeType(
+ TrackGroup group,
+ @Capabilities int[] formatSupport,
+ int requiredAdaptiveSupport,
+ @Nullable String mimeType,
+ int maxVideoWidth,
+ int maxVideoHeight,
+ int maxVideoFrameRate,
+ int maxVideoBitrate,
+ List<Integer> 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,
+ maxVideoFrameRate,
+ maxVideoBitrate)) {
+ selectedTrackIndices.remove(i);
+ }
+ }
+ }
+
+ 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)
+ && (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);
+ }
+
+ @Nullable
+ private static TrackSelection.Definition selectFixedVideoTrack(
+ TrackGroupArray groups, @Capabilities int[][] formatSupports, Parameters params) {
+ TrackGroup selectedGroup = null;
+ int selectedTrackIndex = 0;
+ int selectedTrackScore = 0;
+ int selectedBitrate = Format.NO_VALUE;
+ int selectedPixelCount = Format.NO_VALUE;
+ for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
+ TrackGroup trackGroup = groups.get(groupIndex);
+ List<Integer> selectedTrackIndices = getViewportFilteredTrackIndices(trackGroup,
+ params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange);
+ @Capabilities int[] trackFormatSupport = formatSupports[groupIndex];
+ for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+ if (isSupported(trackFormatSupport[trackIndex],
+ params.exceedRendererCapabilitiesIfNecessary)) {
+ Format format = trackGroup.getFormat(trackIndex);
+ 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;
+ boolean isWithinCapabilities = isSupported(trackFormatSupport[trackIndex], false);
+ if (isWithinCapabilities) {
+ trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;
+ }
+ boolean selectTrack = trackScore > selectedTrackScore;
+ if (trackScore == selectedTrackScore) {
+ 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 {
+ // 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;
+ }
+ }
+ if (selectTrack) {
+ selectedGroup = trackGroup;
+ selectedTrackIndex = trackIndex;
+ selectedTrackScore = trackScore;
+ selectedBitrate = format.bitrate;
+ selectedPixelCount = format.getPixelCount();
+ }
+ }
+ }
+ }
+ return selectedGroup == null
+ ? null
+ : new TrackSelection.Definition(selectedGroup, selectedTrackIndex);
+ }
+
+ // Audio track selection implementation.
+
+ /**
+ * 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<TrackSelection.Definition, AudioTrackScore> selectAudioTrack(
+ TrackGroupArray groups,
+ @Capabilities int[][] formatSupports,
+ @AdaptiveSupport int mixedMimeTypeAdaptationSupports,
+ Parameters params,
+ boolean enableAdaptiveTrackSelection)
+ throws ExoPlaybackException {
+ int selectedTrackIndex = C.INDEX_UNSET;
+ int selectedGroupIndex = C.INDEX_UNSET;
+ AudioTrackScore selectedTrackScore = null;
+ for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
+ TrackGroup trackGroup = groups.get(groupIndex);
+ @Capabilities int[] trackFormatSupport = formatSupports[groupIndex];
+ for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+ if (isSupported(trackFormatSupport[trackIndex],
+ params.exceedRendererCapabilitiesIfNecessary)) {
+ Format format = trackGroup.getFormat(trackIndex);
+ 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;
+ }
+ }
+ }
+ }
+
+ if (selectedGroupIndex == C.INDEX_UNSET) {
+ return null;
+ }
+
+ TrackGroup selectedGroup = groups.get(selectedGroupIndex);
+
+ 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,
+ formatSupports[selectedGroupIndex],
+ params.maxAudioBitrate,
+ params.allowAudioMixedMimeTypeAdaptiveness,
+ params.allowAudioMixedSampleRateAdaptiveness,
+ params.allowAudioMixedChannelCountAdaptiveness);
+ if (adaptiveTracks.length > 0) {
+ definition = new TrackSelection.Definition(selectedGroup, adaptiveTracks);
+ }
+ }
+ 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[] getAdaptiveAudioTracks(
+ TrackGroup group,
+ @Capabilities int[] formatSupport,
+ int maxAudioBitrate,
+ boolean allowMixedMimeTypeAdaptiveness,
+ boolean allowMixedSampleRateAdaptiveness,
+ boolean allowAudioMixedChannelCountAdaptiveness) {
+ int selectedConfigurationTrackCount = 0;
+ AudioConfigurationTuple selectedConfiguration = null;
+ HashSet<AudioConfigurationTuple> seenConfigurationTuples = new HashSet<>();
+ for (int i = 0; i < group.length; i++) {
+ Format format = group.getFormat(i);
+ AudioConfigurationTuple configuration =
+ new AudioConfigurationTuple(
+ format.channelCount, format.sampleRate, format.sampleMimeType);
+ if (seenConfigurationTuples.add(configuration)) {
+ int configurationCount =
+ getAdaptiveAudioTrackCount(
+ group,
+ formatSupport,
+ configuration,
+ maxAudioBitrate,
+ allowMixedMimeTypeAdaptiveness,
+ allowMixedSampleRateAdaptiveness,
+ allowAudioMixedChannelCountAdaptiveness);
+ if (configurationCount > selectedConfigurationTrackCount) {
+ selectedConfiguration = configuration;
+ selectedConfigurationTrackCount = configurationCount;
+ }
+ }
+ }
+
+ if (selectedConfigurationTrackCount > 1) {
+ Assertions.checkNotNull(selectedConfiguration);
+ int[] adaptiveIndices = new int[selectedConfigurationTrackCount];
+ int index = 0;
+ for (int i = 0; i < group.length; i++) {
+ Format format = group.getFormat(i);
+ if (isSupportedAdaptiveAudioTrack(
+ format,
+ formatSupport[i],
+ selectedConfiguration,
+ maxAudioBitrate,
+ allowMixedMimeTypeAdaptiveness,
+ allowMixedSampleRateAdaptiveness,
+ allowAudioMixedChannelCountAdaptiveness)) {
+ adaptiveIndices[index++] = i;
+ }
+ }
+ return adaptiveIndices;
+ }
+ return NO_TRACKS;
+ }
+
+ 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,
+ maxAudioBitrate,
+ allowMixedMimeTypeAdaptiveness,
+ allowMixedSampleRateAdaptiveness,
+ allowAudioMixedChannelCountAdaptiveness)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ 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.
+
+ /**
+ * 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<TrackSelection.Definition, TextTrackScore> selectTextTrack(
+ TrackGroupArray groups,
+ @Capabilities int[][] formatSupport,
+ Parameters params,
+ @Nullable String selectedAudioLanguage)
+ throws ExoPlaybackException {
+ TrackGroup selectedGroup = null;
+ int selectedTrackIndex = C.INDEX_UNSET;
+ TextTrackScore selectedTrackScore = null;
+ for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
+ TrackGroup trackGroup = groups.get(groupIndex);
+ @Capabilities int[] trackFormatSupport = formatSupport[groupIndex];
+ for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+ if (isSupported(trackFormatSupport[trackIndex],
+ params.exceedRendererCapabilitiesIfNecessary)) {
+ Format format = trackGroup.getFormat(trackIndex);
+ TextTrackScore trackScore =
+ new TextTrackScore(
+ format, params, trackFormatSupport[trackIndex], selectedAudioLanguage);
+ if (trackScore.isWithinConstraints
+ && (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0)) {
+ selectedGroup = trackGroup;
+ selectedTrackIndex = trackIndex;
+ selectedTrackScore = trackScore;
+ }
+ }
+ }
+ }
+ return selectedGroup == null
+ ? null
+ : Pair.create(
+ new TrackSelection.Definition(selectedGroup, selectedTrackIndex),
+ Assertions.checkNotNull(selectedTrackScore));
+ }
+
+ // General track selection methods.
+
+ /**
+ * 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);
+ @Capabilities int[] trackFormatSupport = formatSupport[groupIndex];
+ for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+ 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;
+ if (isSupported(trackFormatSupport[trackIndex], false)) {
+ trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;
+ }
+ if (trackScore > selectedTrackScore) {
+ selectedGroup = trackGroup;
+ selectedTrackIndex = trackIndex;
+ selectedTrackScore = trackScore;
+ }
+ }
+ }
+ }
+ return selectedGroup == null
+ ? null
+ : new TrackSelection.Definition(selectedGroup, selectedTrackIndex);
+ }
+
+ // 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);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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<Integer> getViewportFilteredTrackIndices(TrackGroup group, int viewportWidth,
+ int viewportHeight, boolean orientationMayChange) {
+ // Initially include all indices.
+ ArrayList<Integer> selectedTrackIndices = new ArrayList<>(group.length);
+ for (int i = 0; i < group.length; i++) {
+ selectedTrackIndices.add(i);
+ }
+
+ if (viewportWidth == Integer.MAX_VALUE || viewportHeight == Integer.MAX_VALUE) {
+ // Viewport dimensions not set. Return the full set of indices.
+ return selectedTrackIndices;
+ }
+
+ int maxVideoPixelsToRetain = Integer.MAX_VALUE;
+ for (int i = 0; i < group.length; i++) {
+ Format format = group.getFormat(i);
+ // Keep track of the number of pixels of the selected format whose resolution is the
+ // smallest to exceed the maximum size at which it can be displayed within the viewport.
+ // We'll discard formats of higher resolution.
+ if (format.width > 0 && format.height > 0) {
+ Point maxVideoSizeInViewport = getMaxVideoSizeInViewport(orientationMayChange,
+ viewportWidth, viewportHeight, format.width, format.height);
+ int videoPixels = format.width * format.height;
+ if (format.width >= (int) (maxVideoSizeInViewport.x * FRACTION_TO_CONSIDER_FULLSCREEN)
+ && format.height >= (int) (maxVideoSizeInViewport.y * FRACTION_TO_CONSIDER_FULLSCREEN)
+ && videoPixels < maxVideoPixelsToRetain) {
+ maxVideoPixelsToRetain = videoPixels;
+ }
+ }
+ }
+
+ // Filter out formats that exceed maxVideoPixelsToRetain. These formats have an unnecessarily
+ // high resolution given the size at which the video will be displayed within the viewport. Also
+ // filter out formats with unknown dimensions, since we have some whose dimensions are known.
+ if (maxVideoPixelsToRetain != Integer.MAX_VALUE) {
+ for (int i = selectedTrackIndices.size() - 1; i >= 0; i--) {
+ Format format = group.getFormat(selectedTrackIndices.get(i));
+ int pixelCount = format.getPixelCount();
+ if (pixelCount == Format.NO_VALUE || pixelCount > maxVideoPixelsToRetain) {
+ selectedTrackIndices.remove(i);
+ }
+ }
+ }
+
+ return selectedTrackIndices;
+ }
+
+ /**
+ * Given viewport dimensions and video dimensions, computes the maximum size of the video as it
+ * will be rendered to fit inside of the viewport.
+ */
+ private static Point getMaxVideoSizeInViewport(boolean orientationMayChange, int viewportWidth,
+ int viewportHeight, int videoWidth, int videoHeight) {
+ if (orientationMayChange && (videoWidth > videoHeight) != (viewportWidth > viewportHeight)) {
+ // Rotation is allowed, and the video will be larger in the rotated viewport.
+ int tempViewportWidth = viewportWidth;
+ viewportWidth = viewportHeight;
+ viewportHeight = tempViewportWidth;
+ }
+
+ if (videoWidth * viewportHeight >= videoHeight * viewportWidth) {
+ // Horizontal letter-boxing along top and bottom.
+ return new Point(viewportWidth, Util.ceilDivide(viewportWidth * videoHeight, videoWidth));
+ } else {
+ // Vertical letter-boxing along edges.
+ return new Point(Util.ceilDivide(viewportHeight * videoWidth, videoHeight), viewportHeight);
+ }
+ }
+
+ /**
+ * 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<AudioTrackScore> {
+
+ /**
+ * 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;
+ @Nullable public final String mimeType;
+
+ public AudioConfigurationTuple(int channelCount, int sampleRate, @Nullable String mimeType) {
+ this.channelCount = channelCount;
+ this.sampleRate = sampleRate;
+ this.mimeType = mimeType;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ AudioConfigurationTuple other = (AudioConfigurationTuple) obj;
+ return channelCount == other.channelCount && sampleRate == other.sampleRate
+ && TextUtils.equals(mimeType, other.mimeType);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = channelCount;
+ result = 31 * result + sampleRate;
+ result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);
+ return result;
+ }
+
+ }
+
+ /** Represents how well a text track matches the selection {@link Parameters}. */
+ protected static final class TextTrackScore implements Comparable<TextTrackScore> {
+
+ /**
+ * 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java
new file mode 100644
index 0000000000..824abaccfa
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java
@@ -0,0 +1,117 @@
+/*
+ * 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.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.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.
+ */
+public final class FixedTrackSelection extends BaseTrackSelection {
+
+ /**
+ * @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;
+ @Nullable private final Object data;
+
+ public Factory() {
+ this.reason = C.SELECTION_REASON_UNKNOWN;
+ this.data = null;
+ }
+
+ /**
+ * @param reason A reason for the track selection.
+ * @param data Optional data associated with the track selection.
+ */
+ public Factory(int reason, @Nullable Object data) {
+ this.reason = reason;
+ this.data = data;
+ }
+
+ @Override
+ 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;
+ @Nullable private final Object data;
+
+ /**
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param track The index of the selected track within the {@link TrackGroup}.
+ */
+ public FixedTrackSelection(TrackGroup group, int track) {
+ this(group, track, C.SELECTION_REASON_UNKNOWN, null);
+ }
+
+ /**
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param track The index of the selected track within the {@link TrackGroup}.
+ * @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, @Nullable Object data) {
+ super(group, track);
+ this.reason = reason;
+ this.data = data;
+ }
+
+ @Override
+ public void updateSelectedTrack(
+ long playbackPositionUs,
+ long bufferedDurationUs,
+ long availableDurationUs,
+ List<? extends MediaChunk> queue,
+ MediaChunkIterator[] mediaChunkIterators) {
+ // Do nothing.
+ }
+
+ @Override
+ public int getSelectedIndex() {
+ return 0;
+ }
+
+ @Override
+ public int getSelectionReason() {
+ return reason;
+ }
+
+ @Override
+ @Nullable
+ public Object getSelectionData() {
+ return data;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
new file mode 100644
index 0000000000..8ba581020b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
@@ -0,0 +1,541 @@
+/*
+ * 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.trackselection;
+
+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 org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * Base class for {@link TrackSelector}s that first establish a mapping between {@link TrackGroup}s
+ * and {@link Renderer}s, and then from that mapping create a {@link TrackSelection} for each
+ * renderer.
+ */
+public abstract class MappingTrackSelector extends TrackSelector {
+
+ /**
+ * Provides mapped track information for each renderer.
+ */
+ public static final class MappedTrackInfo {
+
+ /**
+ * 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}.
+ */
+ @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;
+ }
+
+ /**
+ * Returns the track type that the renderer at a given index handles.
+ *
+ * @see Renderer#getTrackType()
+ * @param rendererIndex The renderer index.
+ * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
+ */
+ public int getRendererType(int rendererIndex) {
+ return rendererTrackTypes[rendererIndex];
+ }
+
+ /**
+ * 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 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 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}.
+ *
+ * <p>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;
+ }
+
+ }
+
+ @Nullable private MappedTrackInfo currentMappedTrackInfo;
+
+ /**
+ * Returns the mapping information for the currently active track selection, or null if no
+ * selection is currently active.
+ */
+ public final @Nullable MappedTrackInfo getCurrentMappedTrackInfo() {
+ return currentMappedTrackInfo;
+ }
+
+ // TrackSelector implementation.
+
+ @Override
+ 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][];
+ @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.
+ @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.
+ 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.
+ @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;
+ rendererFormatSupports[rendererIndex][rendererTrackGroupCount] = rendererFormatSupport;
+ rendererTrackGroupCounts[rendererIndex]++;
+ }
+
+ // Create a track group array for each renderer, and trim each rendererFormatSupports entry.
+ TrackGroupArray[] rendererTrackGroupArrays = new TrackGroupArray[rendererCapabilities.length];
+ int[] rendererTrackTypes = new int[rendererCapabilities.length];
+ for (int i = 0; i < rendererCapabilities.length; i++) {
+ int rendererTrackGroupCount = rendererTrackGroupCounts[i];
+ 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 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,
+ rendererMixedMimeTypeAdaptationSupports,
+ rendererFormatSupports,
+ unmappedTrackGroupArray);
+
+ Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> result =
+ selectTracks(
+ mappedTrackInfo, rendererFormatSupports, rendererMixedMimeTypeAdaptationSupports);
+ return new TrackSelectorResult(result.first, result.second, mappedTrackInfo);
+ }
+
+ /**
+ * Given mapped track information, returns a track selection and configuration for each renderer.
+ *
+ * @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 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 mapped.
+ *
+ * <p>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}.
+ *
+ * <p>In the case that two or more renderers report the same level of support, the assignment
+ * depends on {@code preferUnassociatedRenderer}.
+ *
+ * <ul>
+ * <li>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.
+ * <li>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.
+ * </ul>
+ *
+ * <p>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 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,
+ int[] rendererTrackGroupCounts,
+ boolean preferUnassociatedRenderer)
+ throws ExoPlaybackException {
+ int bestRendererIndex = rendererCapabilities.length;
+ @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++) {
+ @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.
+ *
+ * @param rendererCapabilities The {@link RendererCapabilities} of the renderer.
+ * @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 {
+ @Capabilities int[] formatSupport = new int[group.length];
+ for (int i = 0; i < group.length; i++) {
+ formatSupport[i] = rendererCapabilities.supportsFormat(group.getFormat(i));
+ }
+ return formatSupport;
+ }
+
+ /**
+ * Calls {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer,
+ * returning the results in an array.
+ *
+ * @param rendererCapabilities The {@link RendererCapabilities} of the renderers.
+ * @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.
+ */
+ @AdaptiveSupport
+ private static int[] getMixedMimeTypeAdaptationSupports(
+ RendererCapabilities[] rendererCapabilities) throws ExoPlaybackException {
+ @AdaptiveSupport int[] mixedMimeTypeAdaptationSupport = new int[rendererCapabilities.length];
+ for (int i = 0; i < mixedMimeTypeAdaptationSupport.length; i++) {
+ mixedMimeTypeAdaptationSupport[i] = rendererCapabilities[i].supportsMixedMimeTypeAdaptation();
+ }
+ return mixedMimeTypeAdaptationSupport;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java
new file mode 100644
index 0000000000..75b7fc21f1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java
@@ -0,0 +1,143 @@
+/*
+ * 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.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.
+ */
+public final class RandomTrackSelection extends BaseTrackSelection {
+
+ /**
+ * Factory for {@link RandomTrackSelection} instances.
+ */
+ public static final class Factory implements TrackSelection.Factory {
+
+ private final Random random;
+
+ public Factory() {
+ random = new Random();
+ }
+
+ /**
+ * @param seed A seed for the {@link Random} instance used by the factory.
+ */
+ public Factory(int seed) {
+ random = new Random(seed);
+ }
+
+ @Override
+ 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;
+
+ private int selectedIndex;
+
+ /**
+ * @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 RandomTrackSelection(TrackGroup group, int... tracks) {
+ super(group, tracks);
+ random = new Random();
+ selectedIndex = random.nextInt(length);
+ }
+
+ /**
+ * @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.
+ * @param seed A seed for the {@link Random} instance used to update the selected track.
+ */
+ public RandomTrackSelection(TrackGroup group, int[] tracks, long seed) {
+ this(group, tracks, new Random(seed));
+ }
+
+ /**
+ * @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.
+ * @param random A source of random numbers.
+ */
+ public RandomTrackSelection(TrackGroup group, int[] tracks, Random random) {
+ super(group, tracks);
+ this.random = random;
+ selectedIndex = random.nextInt(length);
+ }
+
+ @Override
+ public void updateSelectedTrack(
+ long playbackPositionUs,
+ long bufferedDurationUs,
+ long availableDurationUs,
+ List<? extends MediaChunk> queue,
+ MediaChunkIterator[] mediaChunkIterators) {
+ // Count the number of non-blacklisted formats.
+ long nowMs = SystemClock.elapsedRealtime();
+ int nonBlacklistedFormatCount = 0;
+ for (int i = 0; i < length; i++) {
+ if (!isBlacklisted(i, nowMs)) {
+ nonBlacklistedFormatCount++;
+ }
+ }
+
+ selectedIndex = random.nextInt(nonBlacklistedFormatCount);
+ if (nonBlacklistedFormatCount != length) {
+ // Adjust the format index to account for blacklisted formats.
+ nonBlacklistedFormatCount = 0;
+ for (int i = 0; i < length; i++) {
+ if (!isBlacklisted(i, nowMs) && selectedIndex == nonBlacklistedFormatCount++) {
+ selectedIndex = i;
+ return;
+ }
+ }
+ }
+ }
+
+ @Override
+ public int getSelectedIndex() {
+ return selectedIndex;
+ }
+
+ @Override
+ public int getSelectionReason() {
+ return C.SELECTION_REASON_ADAPTIVE;
+ }
+
+ @Override
+ @Nullable
+ public Object getSelectionData() {
+ return null;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelection.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelection.java
new file mode 100644
index 0000000000..d2f32222fa
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelection.java
@@ -0,0 +1,269 @@
+/*
+ * 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.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.
+ *
+ * <p>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 track selections for the provided {@link Definition Definitions}.
+ *
+ * <p>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.
+ */
+ @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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ */
+ TrackGroup getTrackGroup();
+
+ // Static subset of selected tracks.
+
+ /**
+ * Returns the number of tracks in the selection.
+ */
+ int length();
+
+ /**
+ * Returns the format of the track at a given index in the selection.
+ *
+ * @param index The index in the selection.
+ * @return The format of the selected track.
+ */
+ Format getFormat(int index);
+
+ /**
+ * Returns the index in the track group of the track at a given index in the selection.
+ *
+ * @param index The index in the selection.
+ * @return The index of the selected track.
+ */
+ int getIndexInTrackGroup(int index);
+
+ /**
+ * 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
+ * format is not part of the selection.
+ */
+ int indexOf(Format format);
+
+ /**
+ * Returns the index in the selection of the track with the specified index in the track group.
+ *
+ * @param indexInTrackGroup The index in the track group.
+ * @return The index in the selection, or {@link C#INDEX_UNSET} if the track with the specified
+ * index is not part of the selection.
+ */
+ int indexOf(int indexInTrackGroup);
+
+ // Individual selected track.
+
+ /**
+ * Returns the {@link Format} of the individual selected track.
+ */
+ Format getSelectedFormat();
+
+ /**
+ * Returns the index in the track group of the individual selected track.
+ */
+ int getSelectedIndexInTrackGroup();
+
+ /**
+ * Returns the index of the selected track.
+ */
+ int getSelectedIndex();
+
+ /**
+ * Returns the reason for the current track selection.
+ */
+ int getSelectionReason();
+
+ /** Returns optional data associated with the current track selection. */
+ @Nullable Object getSelectionData();
+
+ // Adaptation.
+
+ /**
+ * Called to notify the selection of the current playback speed. The playback speed may affect
+ * adaptive track selection.
+ *
+ * @param speed The playback speed.
+ */
+ void onPlaybackSpeed(float speed);
+
+ /**
+ * Called to notify the selection of a position discontinuity.
+ *
+ * <p>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.
+ *
+ * <p>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<? extends MediaChunk> queue,
+ MediaChunkIterator[] mediaChunkIterators);
+
+ /**
+ * May be called periodically by sources that load media in discrete {@link MediaChunk}s and
+ * support discarding of buffered chunks in order to re-buffer using a different selected track.
+ * Returns the number of chunks that should be retained in the queue.
+ * <p>
+ * To avoid excessive re-buffering, implementations should normally return the size of the queue.
+ * 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. 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 queue The queue of buffered {@link MediaChunk}s. Must not be modified.
+ * @return The number of chunks to retain in the queue.
+ */
+ int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue);
+
+ /**
+ * Attempts to blacklist the track at the specified index in the selection, making it ineligible
+ * 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[])}.
+ *
+ * <p>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
+ * milliseconds.
+ * @return Whether blacklisting was successful.
+ */
+ boolean blacklist(int index, long blacklistDurationMs);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java
new file mode 100644
index 0000000000..7953ef354c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java
@@ -0,0 +1,77 @@
+/*
+ * 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.trackselection;
+
+import androidx.annotation.Nullable;
+import java.util.Arrays;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/** An array of {@link TrackSelection}s. */
+public final class TrackSelectionArray {
+
+ /** The length of this array. */
+ public final int length;
+
+ 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(@NullableType TrackSelection... trackSelections) {
+ this.trackSelections = trackSelections;
+ this.length = trackSelections.length;
+ }
+
+ /**
+ * Returns the selection at a given index.
+ *
+ * @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 @NullableType TrackSelection[] getAll() {
+ return trackSelections.clone();
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = 17;
+ result = 31 * result + Arrays.hashCode(trackSelections);
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ TrackSelectionArray other = (TrackSelectionArray) obj;
+ return Arrays.equals(trackSelections, other.trackSelections);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java
new file mode 100644
index 0000000000..b6086fa594
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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}.
+ *
+ * <p>Does nothing for API levels &lt; 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}.
+ *
+ * <p>If possible, use {@link #getDefaults(Context)} instead.
+ *
+ * <p>This instance will not have the following settings:
+ *
+ * <ul>
+ * <li>{@link Builder#setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(Context)
+ * Preferred text language and role flags} configured to the accessibility settings of
+ * {@link CaptioningManager}.
+ * </ul>
+ */
+ @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<TrackSelectionParameters> CREATOR =
+ new Creator<TrackSelectionParameters>() {
+
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java
new file mode 100644
index 0000000000..b2fcf5c13c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelector.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelector.java
new file mode 100644
index 0000000000..878031824d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelector.java
@@ -0,0 +1,157 @@
+/*
+ * 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.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;
+
+/**
+ * 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.
+ *
+ * <h3>Interactions with the player</h3>
+ *
+ * The following interactions occur between the player and its track selector during playback.
+ *
+ * <ul>
+ * <li>When the player is created it will initialize the track selector by calling {@link
+ * #init(InvalidationListener, BandwidthMeter)}.
+ * <li>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.
+ * <li>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)}.
+ * <li>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.
+ * </ul>
+ *
+ * <h3>Renderer configuration</h3>
+ *
+ * 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.
+ *
+ * <h3>Threading model</h3>
+ *
+ * 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 selections previously made by a {@link TrackSelector} are no longer valid.
+ */
+ public interface InvalidationListener {
+
+ /**
+ * 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();
+
+ }
+
+ @Nullable private InvalidationListener listener;
+ @Nullable private BandwidthMeter bandwidthMeter;
+
+ /**
+ * Called by the player to initialize 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, BandwidthMeter bandwidthMeter) {
+ this.listener = listener;
+ this.bandwidthMeter = bandwidthMeter;
+ }
+
+ /**
+ * 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,
+ MediaPeriodId periodId,
+ Timeline timeline)
+ throws ExoPlaybackException;
+
+ /**
+ * 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 selection.
+ */
+ public abstract void onSelectionActivated(Object info);
+
+ /**
+ * Calls {@link InvalidationListener#onTrackSelectionsInvalidated()} to invalidate all previously
+ * generated track selections.
+ */
+ protected final void invalidate() {
+ if (listener != null) {
+ listener.onTrackSelectionsInvalidated();
+ }
+ }
+
+ /**
+ * 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java
new file mode 100644
index 0000000000..9c005497cc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java
@@ -0,0 +1,105 @@
+/*
+ * 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.trackselection;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererConfiguration;
+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;
+ /**
+ * A {@link RendererConfiguration} for each renderer. A null entry indicates the corresponding
+ * renderer should be disabled.
+ */
+ public final @NullableType RendererConfiguration[] rendererConfigurations;
+ /**
+ * A {@link TrackSelectionArray} containing the track selection for each renderer.
+ */
+ public final TrackSelectionArray selections;
+ /**
+ * An opaque object that will be returned to {@link TrackSelector#onSelectionActivated(Object)}
+ * should the selections be activated.
+ */
+ public final Object info;
+
+ /**
+ * @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 selection be activated.
+ */
+ 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.
+ * @return Whether this result is equivalent to {@code other} for all renderers.
+ */
+ public boolean isEquivalent(@Nullable TrackSelectorResult other) {
+ if (other == null || other.selections.length != selections.length) {
+ return false;
+ }
+ for (int i = 0; i < selections.length; i++) {
+ if (!isEquivalent(other, i)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns whether this result is equivalent to {@code other} for the renderer at the given index.
+ * The results are equivalent if they have equal track selections and configurations for the
+ * renderer.
+ *
+ * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false}
+ * will be returned.
+ * @param index The renderer index to check for equivalence.
+ * @return Whether this result is equivalent to {@code other} for the renderer at the specified
+ * index.
+ */
+ public boolean isEquivalent(@Nullable TrackSelectorResult other, int index) {
+ if (other == null) {
+ return false;
+ }
+ return Util.areEqual(rendererConfigurations[index], other.rendererConfigurations[index])
+ && Util.areEqual(selections.get(index), other.selections.get(index));
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/package-info.java
new file mode 100644
index 0000000000..4a04290d0f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocation.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocation.java
new file mode 100644
index 0000000000..87dd142e6a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocation.java
@@ -0,0 +1,46 @@
+/*
+ * 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.upstream;
+
+/**
+ * An allocation within a byte array.
+ * <p>
+ * The allocation's length is obtained by calling {@link Allocator#getIndividualAllocationLength()}
+ * on the {@link Allocator} from which it was obtained.
+ */
+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 #offset} must be used when indexing into it.
+ */
+ public final byte[] data;
+
+ /**
+ * 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 in {@code data}.
+ */
+ public Allocation(byte[] data, int offset) {
+ this.data = data;
+ this.offset = offset;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocator.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocator.java
new file mode 100644
index 0000000000..d554d0fe7f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Allocator.java
@@ -0,0 +1,63 @@
+/*
+ * 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.upstream;
+
+/**
+ * A source of allocations.
+ */
+public interface Allocator {
+
+ /**
+ * Obtain an {@link Allocation}.
+ * <p>
+ * When the caller has finished with the {@link Allocation}, it should be returned by calling
+ * {@link #release(Allocation)}.
+ *
+ * @return The {@link Allocation}.
+ */
+ Allocation allocate();
+
+ /**
+ * Releases an {@link Allocation} back to the allocator.
+ *
+ * @param allocation The {@link Allocation} being released.
+ */
+ void release(Allocation allocation);
+
+ /**
+ * Releases an array of {@link Allocation}s back to the allocator.
+ *
+ * @param allocations The array of {@link Allocation}s being released.
+ */
+ void release(Allocation[] allocations);
+
+ /**
+ * Hints to the allocator that it should make a best effort to release any excess
+ * {@link Allocation}s.
+ */
+ void trim();
+
+ /**
+ * Returns the total number of bytes currently allocated.
+ */
+ int getTotalBytesAllocated();
+
+ /**
+ * Returns the length of each individual {@link Allocation}.
+ */
+ int getIndividualAllocationLength();
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/AssetDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/AssetDataSource.java
new file mode 100644
index 0000000000..70cd1de8fe
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/AssetDataSource.java
@@ -0,0 +1,150 @@
+/*
+ * 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.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 extends BaseDataSource {
+
+ /**
+ * Thrown when an {@link IOException} is encountered reading a local asset.
+ */
+ public static final class AssetDataSourceException extends IOException {
+
+ public AssetDataSourceException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+ private final AssetManager assetManager;
+
+ @Nullable private Uri uri;
+ @Nullable private InputStream inputStream;
+ private long bytesRemaining;
+ private boolean opened;
+
+ /** @param context A context. */
+ public AssetDataSource(Context context) {
+ super(/* isNetwork= */ false);
+ this.assetManager = context.getAssets();
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws AssetDataSourceException {
+ try {
+ uri = dataSpec.uri;
+ 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) {
+ // assetManager.open() returns an AssetInputStream, whose skip() implementation only skips
+ // fewer bytes than requested if the skip is beyond the end of the asset's data.
+ throw new EOFException();
+ }
+ if (dataSpec.length != C.LENGTH_UNSET) {
+ bytesRemaining = dataSpec.length;
+ } else {
+ bytesRemaining = inputStream.available();
+ if (bytesRemaining == Integer.MAX_VALUE) {
+ // assetManager.open() returns an AssetInputStream, whose available() implementation
+ // returns Integer.MAX_VALUE if the remaining length is greater than (or equal to)
+ // Integer.MAX_VALUE. We don't know the true length in this case, so treat as unbounded.
+ bytesRemaining = C.LENGTH_UNSET;
+ }
+ }
+ } catch (IOException e) {
+ throw new AssetDataSourceException(e);
+ }
+
+ opened = true;
+ transferStarted(dataSpec);
+ return bytesRemaining;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws AssetDataSourceException {
+ if (readLength == 0) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ int bytesRead;
+ try {
+ int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
+ : (int) Math.min(bytesRemaining, readLength);
+ bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead);
+ } catch (IOException e) {
+ throw new AssetDataSourceException(e);
+ }
+
+ if (bytesRead == -1) {
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ // End of stream reached having not read sufficient data.
+ throw new AssetDataSourceException(new EOFException());
+ }
+ return C.RESULT_END_OF_INPUT;
+ }
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ bytesTransferred(bytesRead);
+ return bytesRead;
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return uri;
+ }
+
+ @Override
+ public void close() throws AssetDataSourceException {
+ uri = null;
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (IOException e) {
+ throw new AssetDataSourceException(e);
+ } finally {
+ inputStream = null;
+ if (opened) {
+ opened = false;
+ transferEnded();
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BandwidthMeter.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BandwidthMeter.java
new file mode 100644
index 0000000000..5606b45702
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BandwidthMeter.java
@@ -0,0 +1,71 @@
+/*
+ * 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.upstream;
+
+import android.os.Handler;
+import androidx.annotation.Nullable;
+
+/**
+ * Provides estimates of the currently available bandwidth.
+ */
+public interface BandwidthMeter {
+
+ /**
+ * A listener of {@link BandwidthMeter} events.
+ */
+ interface EventListener {
+
+ /**
+ * Called periodically to indicate that bytes have been transferred or the estimated bitrate has
+ * changed.
+ *
+ * <p>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 bytesTransferred, long bitrateEstimate);
+ }
+
+ /** 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BaseDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/BaseDataSource.java
new file mode 100644
index 0000000000..3838094927
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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<TransferListener> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java
new file mode 100644
index 0000000000..4aa66538ff
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java
@@ -0,0 +1,63 @@
+/*
+ * 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.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 @MonotonicNonNull ByteArrayOutputStream stream;
+
+ @Override
+ public void open(DataSpec dataSpec) {
+ if (dataSpec.length == C.LENGTH_UNSET) {
+ stream = new ByteArrayOutputStream();
+ } else {
+ Assertions.checkArgument(dataSpec.length <= Integer.MAX_VALUE);
+ stream = new ByteArrayOutputStream((int) dataSpec.length);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ castNonNull(stream).close();
+ }
+
+ @Override
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java
new file mode 100644
index 0000000000..0be103701d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java
@@ -0,0 +1,91 @@
+/*
+ * 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.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 extends BaseDataSource {
+
+ private final byte[] data;
+
+ @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;
+ }
+
+ @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);
+ if (bytesRemaining <= 0 || readPosition + bytesRemaining > data.length) {
+ 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) {
+ if (readLength == 0) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ readLength = Math.min(readLength, bytesRemaining);
+ 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() {
+ if (opened) {
+ opened = false;
+ transferEnded();
+ }
+ uri = null;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ContentDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ContentDataSource.java
new file mode 100644
index 0000000000..b73d9d6375
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ContentDataSource.java
@@ -0,0 +1,173 @@
+/*
+ * 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.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.nio.channels.FileChannel;
+
+/** 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.
+ */
+ public static class ContentDataSourceException extends IOException {
+
+ public ContentDataSourceException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+ private final ContentResolver resolver;
+
+ @Nullable private Uri uri;
+ @Nullable private AssetFileDescriptor assetFileDescriptor;
+ @Nullable private FileInputStream inputStream;
+ private long bytesRemaining;
+ private boolean opened;
+
+ /**
+ * @param context A context.
+ */
+ public ContentDataSource(Context context) {
+ super(/* isNetwork= */ false);
+ this.resolver = context.getContentResolver();
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws ContentDataSourceException {
+ try {
+ 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();
+ }
+ if (dataSpec.length != C.LENGTH_UNSET) {
+ bytesRemaining = dataSpec.length;
+ } else {
+ 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) {
+ throw new ContentDataSourceException(e);
+ }
+
+ opened = true;
+ transferStarted(dataSpec);
+
+ return bytesRemaining;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws ContentDataSourceException {
+ if (readLength == 0) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ int bytesRead;
+ try {
+ int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
+ : (int) Math.min(bytesRemaining, readLength);
+ bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead);
+ } catch (IOException e) {
+ throw new ContentDataSourceException(e);
+ }
+
+ if (bytesRead == -1) {
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ // End of stream reached having not read sufficient data.
+ throw new ContentDataSourceException(new EOFException());
+ }
+ return C.RESULT_END_OF_INPUT;
+ }
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ bytesTransferred(bytesRead);
+ return bytesRead;
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return uri;
+ }
+
+ @SuppressWarnings("Finally")
+ @Override
+ public void close() throws ContentDataSourceException {
+ uri = null;
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (IOException e) {
+ throw new ContentDataSourceException(e);
+ } finally {
+ inputStream = null;
+ try {
+ if (assetFileDescriptor != null) {
+ assetFileDescriptor.close();
+ }
+ } catch (IOException e) {
+ throw new ContentDataSourceException(e);
+ } finally {
+ assetFileDescriptor = null;
+ if (opened) {
+ opened = false;
+ transferEnded();
+ }
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java
new file mode 100644
index 0000000000..57420250ac
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSink.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSink.java
new file mode 100644
index 0000000000..c85ec8cfca
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSink.java
@@ -0,0 +1,67 @@
+/*
+ * 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.upstream;
+
+import java.io.IOException;
+
+/**
+ * A component to which streams of data can be written.
+ */
+public interface DataSink {
+
+ /**
+ * A factory for {@link DataSink} instances.
+ */
+ interface Factory {
+
+ /**
+ * Creates a {@link DataSink} instance.
+ */
+ DataSink createDataSink();
+
+ }
+
+ /**
+ * Opens the sink to consume the specified data.
+ *
+ * <p>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.
+ */
+ void open(DataSpec dataSpec) throws IOException;
+
+ /**
+ * Consumes the provided data.
+ *
+ * @param buffer The buffer from which data should be consumed.
+ * @param offset The offset of the data to consume in {@code buffer}.
+ * @param length The length of the data to consume, in bytes.
+ * @throws IOException If an error occurs writing to the sink.
+ */
+ void write(byte[] buffer, int offset, int length) throws IOException;
+
+ /**
+ * Closes the sink.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSource.java
new file mode 100644
index 0000000000..26529253f8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSource.java
@@ -0,0 +1,111 @@
+/*
+ * 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.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.
+ */
+public interface DataSource {
+
+ /**
+ * A factory for {@link DataSource} instances.
+ */
+ interface Factory {
+
+ /**
+ * 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.
+ * <p>
+ * 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 read.
+ * @throws IOException If an error occurs opening the source. {@link DataSourceException} can be
+ * thrown or used as a cause of the thrown exception to specify the reason of the error.
+ * @return The number of bytes that can be read from the opened source. For unbounded requests
+ * (i.e. requests where {@link DataSpec#length} equals {@link C#LENGTH_UNSET}) this value
+ * is the resolved length of the request, or {@link C#LENGTH_UNSET} if the length is still
+ * unresolved. For all other requests, the value returned will be equal to the request's
+ * {@link DataSpec#length}.
+ */
+ long open(DataSpec dataSpec) throws IOException;
+
+ /**
+ * Reads up to {@code readLength} bytes of data and stores them into {@code buffer}, starting at
+ * index {@code offset}.
+ *
+ * <p>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.
+ *
+ * @param buffer The buffer into which the read data should be stored.
+ * @param offset The start offset into {@code buffer} at which data should be written.
+ * @param readLength The maximum number of bytes to read.
+ * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is available
+ * because the end of the opened range has been reached.
+ * @throws IOException If an error occurs reading from the source.
+ */
+ int read(byte[] buffer, int offset, int readLength) throws IOException;
+
+ /**
+ * When the source is open, returns the {@link Uri} from which data is being read. The returned
+ * {@link Uri} will be identical to the one passed {@link #open(DataSpec)} in the {@link DataSpec}
+ * unless redirection has occurred. If redirection has occurred, the {@link Uri} after redirection
+ * is returned.
+ *
+ * @return The {@link Uri} from which data is being read, or null if the source is not open.
+ */
+ @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<String, List<String>> getResponseHeaders() {
+ return Collections.emptyMap();
+ }
+
+ /**
+ * Closes the source.
+ * <p>
+ * 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 source.
+ */
+ void close() throws IOException;
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceException.java
new file mode 100644
index 0000000000..13c34d1dfb
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceException.java
@@ -0,0 +1,41 @@
+/*
+ * 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.upstream;
+
+import java.io.IOException;
+
+/**
+ * Used to specify reason of a DataSource error.
+ */
+public final class DataSourceException extends IOException {
+
+ public static final int POSITION_OUT_OF_RANGE = 0;
+
+ /**
+ * The reason of this {@link DataSourceException}. It can only be {@link #POSITION_OUT_OF_RANGE}.
+ */
+ public final int reason;
+
+ /**
+ * Constructs a DataSourceException.
+ *
+ * @param reason Reason of the error. It can only be {@link #POSITION_OUT_OF_RANGE}.
+ */
+ public DataSourceException(int reason) {
+ this.reason = reason;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceInputStream.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceInputStream.java
new file mode 100644
index 0000000000..c25ba4c10a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSourceInputStream.java
@@ -0,0 +1,107 @@
+/*
+ * 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.upstream;
+
+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;
+import java.io.InputStream;
+
+/**
+ * Allows data corresponding to a given {@link DataSpec} to be read from a {@link DataSource} and
+ * consumed through an {@link InputStream}.
+ */
+public final class DataSourceInputStream extends InputStream {
+
+ private final DataSource dataSource;
+ private final DataSpec dataSpec;
+ private final byte[] singleByteArray;
+
+ private boolean opened = false;
+ private boolean closed = false;
+ private long totalBytesRead;
+
+ /**
+ * @param dataSource The {@link DataSource} from which the data should be read.
+ * @param dataSpec The {@link DataSpec} defining the data to be read from {@code dataSource}.
+ */
+ public DataSourceInputStream(DataSource dataSource, DataSpec dataSpec) {
+ this.dataSource = dataSource;
+ this.dataSpec = dataSpec;
+ singleByteArray = new byte[1];
+ }
+
+ /**
+ * Returns the total number of bytes that have been read or skipped.
+ */
+ public long bytesRead() {
+ return totalBytesRead;
+ }
+
+ /**
+ * Optional call to open the underlying {@link DataSource}.
+ * <p>
+ * Calling this method does nothing if the {@link DataSource} is already open. Calling this
+ * method is optional, since the read and skip methods will automatically open the underlying
+ * {@link DataSource} if it's not open already.
+ *
+ * @throws IOException If an error occurs opening the {@link DataSource}.
+ */
+ public void open() throws IOException {
+ checkOpened();
+ }
+
+ @Override
+ public int read() throws IOException {
+ int length = read(singleByteArray);
+ return length == -1 ? -1 : (singleByteArray[0] & 0xFF);
+ }
+
+ @Override
+ public int read(@NonNull byte[] buffer) throws IOException {
+ return read(buffer, 0, buffer.length);
+ }
+
+ @Override
+ public int read(@NonNull byte[] buffer, int offset, int length) throws IOException {
+ Assertions.checkState(!closed);
+ checkOpened();
+ int bytesRead = dataSource.read(buffer, offset, length);
+ if (bytesRead == C.RESULT_END_OF_INPUT) {
+ return -1;
+ } else {
+ totalBytesRead += bytesRead;
+ return bytesRead;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (!closed) {
+ dataSource.close();
+ closed = true;
+ }
+ }
+
+ private void checkOpened() throws IOException {
+ if (!opened) {
+ dataSource.open(dataSpec);
+ opened = true;
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSpec.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSpec.java
new file mode 100644
index 0000000000..6a419c6632
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DataSpec.java
@@ -0,0 +1,478 @@
+/*
+ * 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.upstream;
+
+import android.net.Uri;
+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.
+ */
+public final class DataSpec {
+
+ /**
+ * 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_DONT_CACHE_IF_LENGTH_UNKNOWN, FLAG_ALLOW_CACHE_FRAGMENTATION})
+ public @interface Flags {}
+ /**
+ * Allows an underlying network stack to request that the server use gzip compression.
+ *
+ * <p>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.
+ *
+ * <p>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;
+ /** 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
+
+ /**
+ * 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}.
+ */
+ @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;
+
+ /**
+ * 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 @HttpMethod int httpMethod;
+
+ /**
+ * 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<String, String> 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}.
+ * <p>
+ * Always equal to {@link #absoluteStreamPosition} unless the {@link #uri} defines the location
+ * of a subset of the underlying data.
+ */
+ public final long position;
+ /**
+ * The length of the data, or {@link C#LENGTH_UNSET}.
+ */
+ public final long length;
+ /**
+ * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the
+ * data spec is not intended to be used in conjunction with a cache.
+ */
+ @Nullable public final String key;
+ /** Request {@link Flags flags}. */
+ public final @Flags int flags;
+
+ /**
+ * Construct a data spec for the given uri and with {@link #key} set to null.
+ *
+ * @param uri {@link #uri}.
+ */
+ public DataSpec(Uri uri) {
+ this(uri, 0);
+ }
+
+ /**
+ * Construct a data spec for the given uri and with {@link #key} set to null.
+ *
+ * @param uri {@link #uri}.
+ * @param flags {@link #flags}.
+ */
+ public DataSpec(Uri uri, @Flags int flags) {
+ this(uri, 0, C.LENGTH_UNSET, null, flags);
+ }
+
+ /**
+ * 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, @Nullable String key) {
+ this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, 0);
+ }
+
+ /**
+ * 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}.
+ * @param flags {@link #flags}.
+ */
+ public DataSpec(
+ Uri uri, long absoluteStreamPosition, long length, @Nullable String key, @Flags int flags) {
+ this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags);
+ }
+
+ /**
+ * 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<String, String> 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}.
+ * @param position {@link #position}.
+ * @param length {@link #length}.
+ * @param key {@link #key}.
+ * @param flags {@link #flags}.
+ */
+ 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 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 #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,
+ @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<String, String> httpRequestHeaders) {
+ Assertions.checkArgument(absoluteStreamPosition >= 0);
+ Assertions.checkArgument(position >= 0);
+ Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET);
+ this.uri = uri;
+ 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));
+ }
+
+ /**
+ * Returns whether the given flag is set.
+ *
+ * @param flag Flag to be checked if it is set.
+ */
+ public boolean isFlagSet(@Flags int flag) {
+ return (this.flags & flag) == flag;
+ }
+
+ @Override
+ public String toString() {
+ 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<String, String> requestHeaders) {
+ return new DataSpec(
+ uri,
+ httpMethod,
+ httpBody,
+ absoluteStreamPosition,
+ position,
+ length,
+ key,
+ flags,
+ requestHeaders);
+ }
+
+ /**
+ * Returns a copy this data spec with additional request headers.
+ *
+ * <p>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<String, String> requestHeaders) {
+ Map<String, String> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultAllocator.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultAllocator.java
new file mode 100644
index 0000000000..b12efcbe4e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultAllocator.java
@@ -0,0 +1,179 @@
+/*
+ * 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.upstream;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+
+/**
+ * Default implementation of {@link Allocator}.
+ */
+public final class DefaultAllocator implements Allocator {
+
+ private static final int AVAILABLE_EXTRA_CAPACITY = 100;
+
+ private final boolean trimOnReset;
+ private final int individualAllocationSize;
+ private final byte[] initialAllocationBlock;
+ private final Allocation[] singleAllocationReleaseHolder;
+
+ private int targetBufferSize;
+ private int allocatedCount;
+ private int availableCount;
+ private Allocation[] availableAllocations;
+
+ /**
+ * Constructs an instance without creating any {@link Allocation}s up front.
+ *
+ * @param trimOnReset Whether memory is freed when the allocator is reset. Should be true unless
+ * the allocator will be re-used by multiple player instances.
+ * @param individualAllocationSize The length of each individual {@link Allocation}.
+ */
+ public DefaultAllocator(boolean trimOnReset, int individualAllocationSize) {
+ this(trimOnReset, individualAllocationSize, 0);
+ }
+
+ /**
+ * Constructs an instance with some {@link Allocation}s created up front.
+ * <p>
+ * Note: {@link Allocation}s created up front will never be discarded by {@link #trim()}.
+ *
+ * @param trimOnReset Whether memory is freed when the allocator is reset. Should be true unless
+ * the allocator will be re-used by multiple player instances.
+ * @param individualAllocationSize The length of each individual {@link Allocation}.
+ * @param initialAllocationCount The number of allocations to create up front.
+ */
+ public DefaultAllocator(boolean trimOnReset, int individualAllocationSize,
+ int initialAllocationCount) {
+ Assertions.checkArgument(individualAllocationSize > 0);
+ Assertions.checkArgument(initialAllocationCount >= 0);
+ this.trimOnReset = trimOnReset;
+ this.individualAllocationSize = individualAllocationSize;
+ this.availableCount = initialAllocationCount;
+ this.availableAllocations = new Allocation[initialAllocationCount + AVAILABLE_EXTRA_CAPACITY];
+ if (initialAllocationCount > 0) {
+ initialAllocationBlock = new byte[initialAllocationCount * individualAllocationSize];
+ for (int i = 0; i < initialAllocationCount; i++) {
+ int allocationOffset = i * individualAllocationSize;
+ availableAllocations[i] = new Allocation(initialAllocationBlock, allocationOffset);
+ }
+ } else {
+ initialAllocationBlock = null;
+ }
+ singleAllocationReleaseHolder = new Allocation[1];
+ }
+
+ public synchronized void reset() {
+ if (trimOnReset) {
+ setTargetBufferSize(0);
+ }
+ }
+
+ public synchronized void setTargetBufferSize(int targetBufferSize) {
+ boolean targetBufferSizeReduced = targetBufferSize < this.targetBufferSize;
+ this.targetBufferSize = targetBufferSize;
+ if (targetBufferSizeReduced) {
+ trim();
+ }
+ }
+
+ @Override
+ public synchronized Allocation allocate() {
+ allocatedCount++;
+ Allocation allocation;
+ if (availableCount > 0) {
+ allocation = availableAllocations[--availableCount];
+ availableAllocations[availableCount] = null;
+ } else {
+ allocation = new Allocation(new byte[individualAllocationSize], 0);
+ }
+ return allocation;
+ }
+
+ @Override
+ public synchronized void release(Allocation allocation) {
+ singleAllocationReleaseHolder[0] = allocation;
+ release(singleAllocationReleaseHolder);
+ }
+
+ @Override
+ public synchronized void release(Allocation[] allocations) {
+ if (availableCount + allocations.length >= availableAllocations.length) {
+ availableAllocations = Arrays.copyOf(availableAllocations,
+ Math.max(availableAllocations.length * 2, availableCount + allocations.length));
+ }
+ for (Allocation allocation : allocations) {
+ availableAllocations[availableCount++] = allocation;
+ }
+ allocatedCount -= allocations.length;
+ // Wake up threads waiting for the allocated size to drop.
+ notifyAll();
+ }
+
+ @Override
+ public synchronized void trim() {
+ int targetAllocationCount = Util.ceilDivide(targetBufferSize, individualAllocationSize);
+ int targetAvailableCount = Math.max(0, targetAllocationCount - allocatedCount);
+ if (targetAvailableCount >= availableCount) {
+ // We're already at or below the target.
+ return;
+ }
+
+ if (initialAllocationBlock != null) {
+ // Some allocations are backed by an initial block. We need to make sure that we hold onto all
+ // such allocations. Re-order the available allocations so that the ones backed by the initial
+ // block come first.
+ int lowIndex = 0;
+ int highIndex = availableCount - 1;
+ while (lowIndex <= highIndex) {
+ Allocation lowAllocation = availableAllocations[lowIndex];
+ if (lowAllocation.data == initialAllocationBlock) {
+ lowIndex++;
+ } else {
+ Allocation highAllocation = availableAllocations[highIndex];
+ if (highAllocation.data != initialAllocationBlock) {
+ highIndex--;
+ } else {
+ availableAllocations[lowIndex++] = highAllocation;
+ availableAllocations[highIndex--] = lowAllocation;
+ }
+ }
+ }
+ // lowIndex is the index of the first allocation not backed by an initial block.
+ targetAvailableCount = Math.max(targetAvailableCount, lowIndex);
+ if (targetAvailableCount >= availableCount) {
+ // We're already at or below the target.
+ return;
+ }
+ }
+
+ // Discard allocations beyond the target.
+ Arrays.fill(availableAllocations, targetAvailableCount, availableCount, null);
+ availableCount = targetAvailableCount;
+ }
+
+ @Override
+ public synchronized int getTotalBytesAllocated() {
+ return allocatedCount * individualAllocationSize;
+ }
+
+ @Override
+ public int getIndividualAllocationLength() {
+ return individualAllocationSize;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java
new file mode 100644
index 0000000000..63ca7c7eac
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java
@@ -0,0 +1,731 @@
+/*
+ * 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.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.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.
+ *
+ * <p>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 {
+
+ /**
+ * 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 Map<String, int[]> 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<Long> 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<Long> getInitialBitrateEstimatesForCountry(String countryCode) {
+ int[] groupIndices = getCountryGroupIndices(countryCode);
+ SparseArray<Long> 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;
+
+ @Nullable private final Context context;
+ private final SparseArray<Long> initialBitrateEstimates;
+ private final EventDispatcher<EventListener> 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(
+ /* context= */ null,
+ /* initialBitrateEstimates= */ new SparseArray<>(),
+ DEFAULT_SLIDING_WINDOW_MAX_WEIGHT,
+ Clock.DEFAULT,
+ /* resetOnNetworkTypeChange= */ false);
+ }
+
+ private DefaultBandwidthMeter(
+ @Nullable Context context,
+ SparseArray<Long> 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);
+ 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.
+ *
+ * <p>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
+ public synchronized long getBitrateEstimate() {
+ return bitrateEstimate;
+ }
+
+ @Override
+ @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 = clock.elapsedRealtime();
+ }
+ streamCount++;
+ }
+
+ @Override
+ public synchronized void onBytesTransferred(
+ DataSource source, DataSpec dataSpec, boolean isNetwork, int bytes) {
+ if (!isNetwork) {
+ return;
+ }
+ sampleBytesTransferred += bytes;
+ }
+
+ @Override
+ public synchronized void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork) {
+ if (!isNetwork) {
+ return;
+ }
+ Assertions.checkState(streamCount > 0);
+ long nowMs = clock.elapsedRealtime();
+ int sampleElapsedTimeMs = (int) (nowMs - sampleStartTimeMs);
+ totalElapsedTimeMs += sampleElapsedTimeMs;
+ totalBytesTransferred += sampleBytesTransferred;
+ if (sampleElapsedTimeMs > 0) {
+ float bitsPerSecond = (sampleBytesTransferred * 8000f) / sampleElapsedTimeMs;
+ slidingPercentile.addSample((int) Math.sqrt(sampleBytesTransferred), bitsPerSecond);
+ if (totalElapsedTimeMs >= ELAPSED_MILLIS_FOR_ESTIMATE
+ || totalBytesTransferred >= BYTES_TRANSFERRED_FOR_ESTIMATE) {
+ 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<WeakReference<DefaultBandwidthMeter>> 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<DefaultBandwidthMeter> bandwidthMeterReference = bandwidthMeters.get(i);
+ DefaultBandwidthMeter bandwidthMeter = bandwidthMeterReference.get();
+ if (bandwidthMeter != null) {
+ updateBandwidthMeter(bandwidthMeter);
+ }
+ }
+ }
+
+ private void updateBandwidthMeter(DefaultBandwidthMeter bandwidthMeter) {
+ bandwidthMeter.onConnectivityAction();
+ }
+
+ private void removeClearedReferences() {
+ for (int i = bandwidthMeters.size() - 1; i >= 0; i--) {
+ WeakReference<DefaultBandwidthMeter> bandwidthMeterReference = bandwidthMeters.get(i);
+ DefaultBandwidthMeter bandwidthMeter = bandwidthMeterReference.get();
+ if (bandwidthMeter == null) {
+ bandwidthMeters.remove(i);
+ }
+ }
+ }
+ }
+
+ private static Map<String, int[]> createInitialBitrateCountryGroupAssignment() {
+ HashMap<String, int[]> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSource.java
new file mode 100644
index 0000000000..87e1c728a0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSource.java
@@ -0,0 +1,289 @@
+/*
+ * 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.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:
+ *
+ * <ul>
+ * <li>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).
+ * <li>asset: For fetching data from an asset in the application's apk (e.g. asset:///media.mp4).
+ * <li>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).
+ * <li>content: For fetching data from a content URI (e.g. content://authority/path/123).
+ * <li>rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an
+ * explicit dependency on ExoPlayer's RTMP extension.
+ * <li>data: For parsing data inlined in the URI as defined in RFC 2397.
+ * <li>udp: For fetching data over UDP (e.g. udp://something.com/media).
+ * <li>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)}.
+ * </ul>
+ */
+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<TransferListener> transferListeners;
+ private final DataSource baseDataSource;
+
+ // 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 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, 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 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 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,
+ String userAgent,
+ int connectTimeoutMillis,
+ int readTimeoutMillis,
+ boolean allowCrossProtocolRedirects) {
+ this(
+ context,
+ new DefaultHttpDataSource(
+ userAgent,
+ connectTimeoutMillis,
+ readTimeoutMillis,
+ allowCrossProtocolRedirects,
+ /* defaultRequestProperties= */ null));
+ }
+
+ /**
+ * Constructs a new instance that delegates to a provided {@link DataSource} for URI schemes other
+ * than file, asset and content.
+ *
+ * @param context A context.
+ * @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, DataSource baseDataSource) {
+ this.context = context.getApplicationContext();
+ this.baseDataSource = Assertions.checkNotNull(baseDataSource);
+ 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
+ public long open(DataSpec dataSpec) throws IOException {
+ Assertions.checkState(dataSource == null);
+ // Choose the correct source for the scheme.
+ String scheme = dataSpec.uri.getScheme();
+ if (Util.isLocalFileUri(dataSpec.uri)) {
+ String uriPath = dataSpec.uri.getPath();
+ if (uriPath != null && uriPath.startsWith("/android_asset/")) {
+ dataSource = getAssetDataSource();
+ } else {
+ dataSource = getFileDataSource();
+ }
+ } else if (SCHEME_ASSET.equals(scheme)) {
+ dataSource = getAssetDataSource();
+ } else if (SCHEME_CONTENT.equals(scheme)) {
+ 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;
+ }
+ // Open the source and return.
+ return dataSource.open(dataSpec);
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ return Assertions.checkNotNull(dataSource).read(buffer, offset, readLength);
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return dataSource == null ? null : dataSource.getUri();
+ }
+
+ @Override
+ public Map<String, List<String>> getResponseHeaders() {
+ return dataSource == null ? Collections.emptyMap() : dataSource.getResponseHeaders();
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (dataSource != null) {
+ try {
+ dataSource.close();
+ } finally {
+ dataSource = null;
+ }
+ }
+ }
+
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java
new file mode 100644
index 0000000000..81add13c10
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java
@@ -0,0 +1,85 @@
+/*
+ * 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.upstream;
+
+import android.content.Context;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource.Factory;
+
+/**
+ * A {@link Factory} that produces {@link DefaultDataSource} instances that delegate to
+ * {@link DefaultHttpDataSource}s for non-file/asset/content URIs.
+ */
+public final class DefaultDataSourceFactory implements Factory {
+
+ private final Context context;
+ @Nullable private final TransferListener listener;
+ private final DataSource.Factory baseDataSourceFactory;
+
+ /**
+ * @param context A context.
+ * @param userAgent The User-Agent string that should be used.
+ */
+ public DefaultDataSourceFactory(Context context, String userAgent) {
+ this(context, userAgent, /* listener= */ null);
+ }
+
+ /**
+ * @param context A context.
+ * @param userAgent The User-Agent string that should be used.
+ * @param listener An optional 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, DataSource)
+ */
+ public DefaultDataSourceFactory(
+ Context context,
+ @Nullable TransferListener listener,
+ DataSource.Factory baseDataSourceFactory) {
+ this.context = context.getApplicationContext();
+ this.listener = listener;
+ this.baseDataSourceFactory = baseDataSourceFactory;
+ }
+
+ @Override
+ public DefaultDataSource createDataSource() {
+ DefaultDataSource dataSource =
+ new DefaultDataSource(context, baseDataSourceFactory.createDataSource());
+ if (listener != null) {
+ dataSource.addTransferListener(listener);
+ }
+ return dataSource;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
new file mode 100644
index 0000000000..c0e8e23bfe
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
@@ -0,0 +1,798 @@
+/*
+ * 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.upstream;
+
+import android.net.Uri;
+import android.text.TextUtils;
+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;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.lang.reflect.Method;
+import java.net.HttpURLConnection;
+import java.net.NoRouteToHostException;
+import java.net.ProtocolException;
+import java.net.Proxy;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+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;
+
+/**
+ * An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}.
+ *
+ * <p>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.
+ *
+ * <p>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 extends BaseDataSource implements HttpDataSource {
+
+ /** The default connection timeout, in milliseconds. */
+ public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;
+ /**
+ * The default read timeout, in milliseconds.
+ */
+ public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
+
+ 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+)$");
+ private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>();
+
+ private final boolean allowCrossProtocolRedirects;
+ private final int connectTimeoutMillis;
+ private final int readTimeoutMillis;
+ private final String userAgent;
+ @Nullable private final RequestProperties defaultRequestProperties;
+ private final RequestProperties requestProperties;
+
+ @Nullable private Predicate<String> 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;
+
+ private long bytesSkipped;
+ private long bytesRead;
+
+ /** @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 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.
+ */
+ 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 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.
+ */
+ public DefaultHttpDataSource(
+ String userAgent,
+ int connectTimeoutMillis,
+ int readTimeoutMillis,
+ boolean allowCrossProtocolRedirects,
+ @Nullable RequestProperties defaultRequestProperties) {
+ super(/* isNetwork= */ true);
+ this.userAgent = Assertions.checkNotEmpty(userAgent);
+ this.requestProperties = new RequestProperties();
+ this.connectTimeoutMillis = connectTimeoutMillis;
+ this.readTimeoutMillis = readTimeoutMillis;
+ this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
+ 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<String> 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<String> 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<String> 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<String> 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<String, List<String>> getResponseHeaders() {
+ return connection == null ? Collections.emptyMap() : connection.getHeaderFields();
+ }
+
+ @Override
+ public void setRequestProperty(String name, String value) {
+ Assertions.checkNotNull(name);
+ Assertions.checkNotNull(value);
+ requestProperties.set(name, value);
+ }
+
+ @Override
+ public void clearRequestProperty(String name) {
+ Assertions.checkNotNull(name);
+ requestProperties.remove(name);
+ }
+
+ @Override
+ public void clearAllRequestProperties() {
+ 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", e, dataSpec, HttpDataSourceException.TYPE_OPEN);
+ } catch (URISyntaxException e) {
+ throw new HttpDataSourceException("URI invalid: " + dataSpec.uri.toString(), dataSpec, HttpDataSourceException.TYPE_OPEN);
+ }
+
+ String responseMessage;
+ try {
+ responseCode = connection.getResponseCode();
+ responseMessage = connection.getResponseMessage();
+ } catch (IOException e) {
+ closeConnectionQuietly();
+ throw new HttpDataSourceException(
+ "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN);
+ }
+
+ // Check for a valid response code.
+ if (responseCode < 200 || responseCode > 299) {
+ Map<String, List<String>> headers = connection.getHeaderFields();
+ closeConnectionQuietly();
+ InvalidResponseCodeException exception =
+ new InvalidResponseCodeException(responseCode, responseMessage, headers, dataSpec);
+ if (responseCode == 416) {
+ exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
+ }
+ throw exception;
+ }
+
+ // Check for a valid content type.
+ String contentType = connection.getContentType();
+ if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) {
+ closeConnectionQuietly();
+ throw new InvalidContentTypeException(contentType, dataSpec);
+ }
+
+ // If we requested a range starting from a non-zero position and received a 200 rather than a
+ // 206, then the server does not support partial requests. We'll need to manually skip to the
+ // requested position.
+ bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
+
+ // Determine the length of the data to be read, after skipping.
+ boolean isCompressed = isCompressed(connection);
+ if (!isCompressed) {
+ if (dataSpec.length != C.LENGTH_UNSET) {
+ bytesToRead = dataSpec.length;
+ } else {
+ long contentLength = getContentLength(connection);
+ bytesToRead = contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip)
+ : C.LENGTH_UNSET;
+ }
+ } 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. 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;
+ transferStarted(dataSpec);
+
+ return bytesToRead;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
+ try {
+ skipInternal();
+ return readInternal(buffer, offset, readLength);
+ } catch (IOException e) {
+ throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_READ);
+ }
+ }
+
+ @Override
+ public void close() throws HttpDataSourceException {
+ try {
+ if (inputStream != null) {
+ maybeTerminateInputStream(connection, bytesRemaining());
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_CLOSE);
+ }
+ }
+ } finally {
+ inputStream = null;
+ closeConnectionQuietly();
+ if (opened) {
+ opened = false;
+ transferEnded();
+ }
+ }
+ }
+
+ /**
+ * Returns the current connection, or null if the source is not currently opened.
+ *
+ * @return The current open connection, or null.
+ */
+ protected final @Nullable HttpURLConnection getConnection() {
+ return connection;
+ }
+
+ /**
+ * Returns the number of bytes that have been skipped since the most recent call to
+ * {@link #open(DataSpec)}.
+ *
+ * @return The number of bytes skipped.
+ */
+ protected final long bytesSkipped() {
+ return bytesSkipped;
+ }
+
+ /**
+ * Returns the number of bytes that have been read since the most recent call to
+ * {@link #open(DataSpec)}.
+ *
+ * @return The number of bytes read.
+ */
+ protected final long bytesRead() {
+ return bytesRead;
+ }
+
+ /**
+ * Returns the number of bytes that are still to be read for the current {@link DataSpec}.
+ * <p>
+ * If the total length of the data being read is known, then this length minus {@code bytesRead()}
+ * is returned. If the total length is unknown, {@link C#LENGTH_UNSET} is returned.
+ *
+ * @return The remaining length, or {@link C#LENGTH_UNSET}.
+ */
+ protected final long bytesRemaining() {
+ return bytesToRead == C.LENGTH_UNSET ? bytesToRead : bytesToRead - bytesRead;
+ }
+
+ /**
+ * Establishes a connection, following redirects to do so where permitted.
+ */
+ private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException, URISyntaxException {
+ URL url = new URL(dataSpec.uri.toString());
+ @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);
+
+ if (!allowCrossProtocolRedirects) {
+ // HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection
+ // automatically. This is the behavior we want, so use it.
+ 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,
+ httpMethod,
+ httpBody,
+ position,
+ length,
+ allowGzip,
+ /* followRedirects= */ false,
+ dataSpec.httpRequestHeaders);
+ int responseCode = connection.getResponseCode();
+ 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;
+ }
+ }
+
+ // If we get here we've been redirected more times than are permitted.
+ throw new NoRouteToHostException("Too many redirects: " + redirectCount);
+ }
+
+ private static URLConnection openConnectionWithProxy(final URI uri) throws IOException {
+ final java.net.ProxySelector ps = java.net.ProxySelector.getDefault();
+ Proxy proxy = Proxy.NO_PROXY;
+ if (ps != null) {
+ final List<Proxy> proxies = ps.select(uri);
+ if (proxies != null && !proxies.isEmpty()) {
+ proxy = proxies.get(0);
+ }
+ }
+
+ return uri.toURL().openConnection(proxy);
+ }
+
+ /**
+ * Configures a connection and opens it.
+ *
+ * @param url The url to connect to.
+ * @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,
+ @HttpMethod int httpMethod,
+ byte[] httpBody,
+ long position,
+ long length,
+ boolean allowGzip,
+ boolean followRedirects,
+ Map<String, String> 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
+ * to use the ProxySelector.
+ */
+ HttpURLConnection connection = (HttpURLConnection) openConnectionWithProxy(url.toURI());
+
+ connection.setConnectTimeout(connectTimeoutMillis);
+ connection.setReadTimeout(readTimeoutMillis);
+
+ Map<String, String> requestHeaders = new HashMap<>();
+ if (defaultRequestProperties != null) {
+ requestHeaders.putAll(defaultRequestProperties.getSnapshot());
+ }
+ requestHeaders.putAll(requestProperties.getSnapshot());
+ requestHeaders.putAll(requestParameters);
+
+ for (Map.Entry<String, String> 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) {
+ rangeRequest += (position + length - 1);
+ }
+ connection.setRequestProperty("Range", rangeRequest);
+ }
+ connection.setRequestProperty("User-Agent", userAgent);
+ connection.setRequestProperty("Accept-Encoding", allowGzip ? "gzip" : "identity");
+ connection.setInstanceFollowRedirects(followRedirects);
+ 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.
+ *
+ * @param originalUrl The original URL.
+ * @param location The Location header in the response.
+ * @return The next URL.
+ * @throws IOException If redirection isn't possible.
+ */
+ private static URL handleRedirect(URL originalUrl, String location) throws IOException {
+ if (location == null) {
+ throw new ProtocolException("Null location redirect");
+ }
+ // Form the new url.
+ URL url = new URL(originalUrl, location);
+ // Check that the protocol of the new url is supported.
+ String protocol = url.getProtocol();
+ if (!"https".equals(protocol) && !"http".equals(protocol)) {
+ throw new ProtocolException("Unsupported protocol redirect: " + protocol);
+ }
+ // Currently this method is only called if allowCrossProtocolRedirects is true, and so the code
+ // below isn't required. If we ever decide to handle redirects ourselves when cross-protocol
+ // redirects are disabled, we'll need to uncomment this block of code.
+ // if (!allowCrossProtocolRedirects && !protocol.equals(originalUrl.getProtocol())) {
+ // throw new ProtocolException("Disallowed cross-protocol redirect ("
+ // + originalUrl.getProtocol() + " to " + protocol + ")");
+ // }
+ return url;
+ }
+
+ /**
+ * Attempts to extract the length of the content from the response headers of an open connection.
+ *
+ * @param connection The open connection.
+ * @return The extracted length, or {@link C#LENGTH_UNSET}.
+ */
+ private static long getContentLength(HttpURLConnection connection) {
+ long contentLength = C.LENGTH_UNSET;
+ String contentLengthHeader = connection.getHeaderField("Content-Length");
+ if (!TextUtils.isEmpty(contentLengthHeader)) {
+ try {
+ contentLength = Long.parseLong(contentLengthHeader);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]");
+ }
+ }
+ String contentRangeHeader = connection.getHeaderField("Content-Range");
+ if (!TextUtils.isEmpty(contentRangeHeader)) {
+ Matcher matcher = CONTENT_RANGE_HEADER.matcher(contentRangeHeader);
+ if (matcher.find()) {
+ try {
+ long contentLengthFromRange =
+ Long.parseLong(matcher.group(2)) - Long.parseLong(matcher.group(1)) + 1;
+ if (contentLength < 0) {
+ // Some proxy servers strip the Content-Length header. Fall back to the length
+ // calculated here in this case.
+ contentLength = contentLengthFromRange;
+ } else if (contentLength != contentLengthFromRange) {
+ // If there is a discrepancy between the Content-Length and Content-Range headers,
+ // assume the one with the larger value is correct. We have seen cases where carrier
+ // change one of them to reduce the size of a request, but it is unlikely anybody would
+ // increase it.
+ Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader
+ + "]");
+ contentLength = Math.max(contentLength, contentLengthFromRange);
+ }
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
+ }
+ }
+ }
+ return contentLength;
+ }
+
+ /**
+ * Skips any bytes that need skipping. Else does nothing.
+ * <p>
+ * This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.
+ *
+ * @throws InterruptedIOException If the thread is interrupted during the operation.
+ * @throws EOFException If the end of the input stream is reached before the bytes are skipped.
+ */
+ private void skipInternal() throws IOException {
+ if (bytesSkipped == bytesToSkip) {
+ return;
+ }
+
+ // Acquire the shared skip buffer.
+ byte[] skipBuffer = skipBufferReference.getAndSet(null);
+ if (skipBuffer == null) {
+ skipBuffer = new byte[4096];
+ }
+
+ while (bytesSkipped != bytesToSkip) {
+ int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length);
+ int read = inputStream.read(skipBuffer, 0, readLength);
+ if (Thread.currentThread().isInterrupted()) {
+ throw new InterruptedIOException();
+ }
+ if (read == -1) {
+ throw new EOFException();
+ }
+ bytesSkipped += read;
+ bytesTransferred(read);
+ }
+
+ // Release the shared skip buffer.
+ skipBufferReference.set(skipBuffer);
+ }
+
+ /**
+ * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at
+ * index {@code offset}.
+ * <p>
+ * This method blocks until at least one byte of data can be read, the end of the opened range is
+ * detected, or an exception is thrown.
+ *
+ * @param buffer The buffer into which the read data should be stored.
+ * @param offset The start offset into {@code buffer} at which data should be written.
+ * @param readLength The maximum number of bytes to read.
+ * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened
+ * range is reached.
+ * @throws IOException If an error occurs reading from the source.
+ */
+ private int readInternal(byte[] buffer, int offset, int readLength) throws IOException {
+ if (readLength == 0) {
+ return 0;
+ }
+ if (bytesToRead != C.LENGTH_UNSET) {
+ long bytesRemaining = bytesToRead - bytesRead;
+ if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ readLength = (int) Math.min(readLength, bytesRemaining);
+ }
+
+ int read = inputStream.read(buffer, offset, readLength);
+ if (read == -1) {
+ if (bytesToRead != C.LENGTH_UNSET) {
+ // End of stream reached having not read sufficient data.
+ throw new EOFException();
+ }
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ bytesRead += read;
+ bytesTransferred(read);
+ return read;
+ }
+
+ /**
+ * On platform API levels 19 and 20, okhttp's implementation of {@link InputStream#close} can
+ * block for a long time if the stream has a lot of data remaining. Call this method before
+ * closing the input stream to make a best effort to cause the input stream to encounter an
+ * unexpected end of input, working around this issue. On other platform API levels, the method
+ * does nothing.
+ *
+ * @param connection The connection whose {@link InputStream} should be terminated.
+ * @param bytesRemaining The number of bytes remaining to be read from the input stream if its
+ * length is known. {@link C#LENGTH_UNSET} otherwise.
+ */
+ private static void maybeTerminateInputStream(HttpURLConnection connection, long bytesRemaining) {
+ if (Util.SDK_INT != 19 && Util.SDK_INT != 20) {
+ return;
+ }
+
+ try {
+ InputStream inputStream = connection.getInputStream();
+ if (bytesRemaining == C.LENGTH_UNSET) {
+ // If the input stream has already ended, do nothing. The socket may be re-used.
+ if (inputStream.read() == -1) {
+ return;
+ }
+ } else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) {
+ // There isn't much data left. Prefer to allow it to drain, which may allow the socket to be
+ // re-used.
+ return;
+ }
+ String className = inputStream.getClass().getName();
+ 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);
+ unexpectedEndOfInput.invoke(inputStream);
+ }
+ } catch (Exception e) {
+ // If an IOException then the connection didn't ever have an input stream, or it was closed
+ // already. If another type of exception then something went wrong, most likely the device
+ // isn't using okhttp.
+ }
+ }
+
+
+ /**
+ * Closes the current connection quietly, if there is one.
+ */
+ private void closeConnectionQuietly() {
+ if (connection != null) {
+ try {
+ connection.disconnect();
+ } catch (Exception e) {
+ Log.e(TAG, "Unexpected error while disconnecting", e);
+ }
+ connection = null;
+ }
+ }
+
+ private static boolean isCompressed(HttpURLConnection connection) {
+ String contentEncoding = connection.getHeaderField("Content-Encoding");
+ return "gzip".equalsIgnoreCase(contentEncoding);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java
new file mode 100644
index 0000000000..cf7448fbd0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java
@@ -0,0 +1,119 @@
+/*
+ * 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.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;
+ @Nullable private final TransferListener listener;
+ private final int connectTimeoutMillis;
+ private final int readTimeoutMillis;
+ private final boolean allowCrossProtocolRedirects;
+
+ /**
+ * Constructs a DefaultHttpDataSourceFactory. Sets {@link
+ * DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
+ * DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
+ * cross-protocol redirects.
+ *
+ * @param userAgent The User-Agent string that should be used.
+ */
+ public DefaultHttpDataSourceFactory(String userAgent) {
+ this(userAgent, null);
+ }
+
+ /**
+ * Constructs a DefaultHttpDataSourceFactory. Sets {@link
+ * DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
+ * DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
+ * cross-protocol redirects.
+ *
+ * @param userAgent The User-Agent string that should be used.
+ * @param listener An optional listener.
+ * @see #DefaultHttpDataSourceFactory(String, TransferListener, int, int, boolean)
+ */
+ 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.
+ * @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,
+ @Nullable TransferListener listener,
+ int connectTimeoutMillis,
+ int readTimeoutMillis,
+ boolean allowCrossProtocolRedirects) {
+ this.userAgent = Assertions.checkNotEmpty(userAgent);
+ this.listener = listener;
+ this.connectTimeoutMillis = connectTimeoutMillis;
+ this.readTimeoutMillis = readTimeoutMillis;
+ this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
+ }
+
+ @Override
+ protected DefaultHttpDataSource createDataSourceInternal(
+ HttpDataSource.RequestProperties defaultRequestProperties) {
+ DefaultHttpDataSource dataSource =
+ new DefaultHttpDataSource(
+ userAgent,
+ connectTimeoutMillis,
+ readTimeoutMillis,
+ allowCrossProtocolRedirects,
+ defaultRequestProperties);
+ if (listener != null) {
+ dataSource.addTransferListener(listener);
+ }
+ return dataSource;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java
new file mode 100644
index 0000000000..082014b7ef
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>{@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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DummyDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DummyDataSource.java
new file mode 100644
index 0000000000..585c37cc78
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/DummyDataSource.java
@@ -0,0 +1,59 @@
+/*
+ * 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 android.net.Uri;
+import androidx.annotation.Nullable;
+import java.io.IOException;
+
+/**
+ * A dummy DataSource which provides no data. {@link #open(DataSpec)} throws {@link IOException}.
+ */
+public final class DummyDataSource implements DataSource {
+
+ public static final DummyDataSource INSTANCE = 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) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return null;
+ }
+
+ @Override
+ public void close() {
+ // do nothing.
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSource.java
new file mode 100644
index 0000000000..eee30e668f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSource.java
@@ -0,0 +1,171 @@
+/*
+ * 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.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 extends BaseDataSource {
+
+ /** 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);
+ }
+ }
+
+ /** {@link DataSource.Factory} for {@link FileDataSource} instances. */
+ public static final class Factory implements DataSource.Factory {
+
+ @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() {
+ super(/* isNetwork= */ false);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws FileDataSourceException {
+ try {
+ 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;
+ if (bytesRemaining < 0) {
+ throw new EOFException();
+ }
+ } catch (IOException e) {
+ throw new FileDataSourceException(e);
+ }
+
+ opened = true;
+ 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) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ } else {
+ int bytesRead;
+ try {
+ bytesRead =
+ castNonNull(file).read(buffer, offset, (int) Math.min(bytesRemaining, readLength));
+ } catch (IOException e) {
+ throw new FileDataSourceException(e);
+ }
+
+ if (bytesRead > 0) {
+ bytesRemaining -= bytesRead;
+ bytesTransferred(bytesRead);
+ }
+
+ return bytesRead;
+ }
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return uri;
+ }
+
+ @Override
+ public void close() throws FileDataSourceException {
+ uri = null;
+ try {
+ if (file != null) {
+ file.close();
+ }
+ } catch (IOException e) {
+ throw new FileDataSourceException(e);
+ } finally {
+ file = null;
+ if (opened) {
+ opened = false;
+ transferEnded();
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java
new file mode 100644
index 0000000000..660a38161c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java
@@ -0,0 +1,38 @@
+/*
+ * 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.upstream;
+
+import androidx.annotation.Nullable;
+
+/** @deprecated Use {@link FileDataSource.Factory}. */
+@Deprecated
+public final class FileDataSourceFactory implements DataSource.Factory {
+
+ private final FileDataSource.Factory wrappedFactory;
+
+ public FileDataSourceFactory() {
+ this(/* listener= */ null);
+ }
+
+ public FileDataSourceFactory(@Nullable TransferListener listener) {
+ wrappedFactory = new FileDataSource.Factory().setListener(listener);
+ }
+
+ @Override
+ public FileDataSource createDataSource() {
+ return wrappedFactory.createDataSource();
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/HttpDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/HttpDataSource.java
new file mode 100644
index 0000000000..ffac1ca893
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/HttpDataSource.java
@@ -0,0 +1,379 @@
+/*
+ * 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.upstream;
+
+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;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An HTTP {@link DataSource}.
+ */
+public interface HttpDataSource extends DataSource {
+
+ /**
+ * A factory for {@link HttpDataSource} instances.
+ */
+ interface Factory extends DataSource.Factory {
+
+ @Override
+ HttpDataSource createDataSource();
+
+ /**
+ * Gets the default request properties used by all {@link HttpDataSource}s created by the
+ * factory. Changes to the properties will be reflected in any future requests made by
+ * {@link HttpDataSource}s created by the factory.
+ *
+ * @return The default request properties of the factory.
+ */
+ RequestProperties getDefaultRequestProperties();
+
+ /**
+ * Sets a default request header for {@link HttpDataSource} instances created by the factory.
+ *
+ * @deprecated Use {@link #getDefaultRequestProperties} instead.
+ * @param name The name of the header field.
+ * @param value The value of the field.
+ */
+ @Deprecated
+ void setDefaultRequestProperty(String name, String value);
+
+ /**
+ * Clears a default request header for {@link HttpDataSource} instances created by the factory.
+ *
+ * @deprecated Use {@link #getDefaultRequestProperties} instead.
+ * @param name The name of the header field.
+ */
+ @Deprecated
+ void clearDefaultRequestProperty(String name);
+
+ /**
+ * Clears all default request headers for all {@link HttpDataSource} instances created by the
+ * factory.
+ *
+ * @deprecated Use {@link #getDefaultRequestProperties} instead.
+ */
+ @Deprecated
+ void clearAllDefaultRequestProperties();
+
+ }
+
+ /**
+ * Stores HTTP request properties (aka HTTP headers) and provides methods to modify the headers
+ * in a thread safe way to avoid the potential of creating snapshots of an inconsistent or
+ * unintended state.
+ */
+ final class RequestProperties {
+
+ private final Map<String, String> requestProperties;
+ private Map<String, String> requestPropertiesSnapshot;
+
+ public RequestProperties() {
+ requestProperties = new HashMap<>();
+ }
+
+ /**
+ * Sets the specified property {@code value} for the specified {@code name}. If a property for
+ * this name previously existed, the old value is replaced by the specified value.
+ *
+ * @param name The name of the request property.
+ * @param value The value of the request property.
+ */
+ public synchronized void set(String name, String value) {
+ requestPropertiesSnapshot = null;
+ requestProperties.put(name, value);
+ }
+
+ /**
+ * Sets the keys and values contained in the map. If a property previously existed, the old
+ * value is replaced by the specified value. If a property previously existed and is not in the
+ * map, the property is left unchanged.
+ *
+ * @param properties The request properties.
+ */
+ public synchronized void set(Map<String, String> properties) {
+ requestPropertiesSnapshot = null;
+ requestProperties.putAll(properties);
+ }
+
+ /**
+ * Removes all properties previously existing and sets the keys and values of the map.
+ *
+ * @param properties The request properties.
+ */
+ public synchronized void clearAndSet(Map<String, String> properties) {
+ requestPropertiesSnapshot = null;
+ requestProperties.clear();
+ requestProperties.putAll(properties);
+ }
+
+ /**
+ * Removes a request property by name.
+ *
+ * @param name The name of the request property to remove.
+ */
+ public synchronized void remove(String name) {
+ requestPropertiesSnapshot = null;
+ requestProperties.remove(name);
+ }
+
+ /**
+ * Clears all request properties.
+ */
+ public synchronized void clear() {
+ requestPropertiesSnapshot = null;
+ requestProperties.clear();
+ }
+
+ /**
+ * Gets a snapshot of the request properties.
+ *
+ * @return A snapshot of the request properties.
+ */
+ public synchronized Map<String, String> getSnapshot() {
+ if (requestPropertiesSnapshot == null) {
+ requestPropertiesSnapshot = Collections.unmodifiableMap(new HashMap<>(requestProperties));
+ }
+ return requestPropertiesSnapshot;
+ }
+
+ }
+
+ /**
+ * Base implementation of {@link Factory} that sets default request properties.
+ */
+ abstract class BaseFactory implements Factory {
+
+ private final RequestProperties defaultRequestProperties;
+
+ public BaseFactory() {
+ defaultRequestProperties = new RequestProperties();
+ }
+
+ @Override
+ public final HttpDataSource createDataSource() {
+ return createDataSourceInternal(defaultRequestProperties);
+ }
+
+ @Override
+ public final RequestProperties getDefaultRequestProperties() {
+ 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() {
+ defaultRequestProperties.clear();
+ }
+
+ /**
+ * Called by {@link #createDataSource()} to create a {@link HttpDataSource} instance.
+ *
+ * @param defaultRequestProperties The default {@code RequestProperties} to be used by the
+ * {@link HttpDataSource} instance.
+ * @return A {@link HttpDataSource} instance.
+ */
+ protected abstract HttpDataSource createDataSourceInternal(RequestProperties
+ defaultRequestProperties);
+
+ }
+
+ /** A {@link Predicate} that rejects content types often used for pay-walls. */
+ Predicate<String> 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;
+
+ @Type public final int type;
+
+ /**
+ * The {@link DataSpec} associated with the current connection.
+ */
+ public final DataSpec dataSpec;
+
+ public HttpDataSourceException(DataSpec dataSpec, @Type int type) {
+ super();
+ this.dataSpec = dataSpec;
+ this.type = type;
+ }
+
+ public HttpDataSourceException(String message, DataSpec dataSpec, @Type int type) {
+ super(message);
+ this.dataSpec = dataSpec;
+ this.type = type;
+ }
+
+ public HttpDataSourceException(IOException cause, DataSpec dataSpec, @Type int type) {
+ super(cause);
+ this.dataSpec = dataSpec;
+ this.type = type;
+ }
+
+ public HttpDataSourceException(String message, IOException cause, DataSpec dataSpec,
+ @Type int type) {
+ super(message, cause);
+ this.dataSpec = dataSpec;
+ this.type = type;
+ }
+
+ }
+
+ /**
+ * Thrown when the content type is invalid.
+ */
+ final class InvalidContentTypeException extends HttpDataSourceException {
+
+ public final String contentType;
+
+ public InvalidContentTypeException(String contentType, DataSpec dataSpec) {
+ super("Invalid content type: " + contentType, dataSpec, TYPE_OPEN);
+ this.contentType = contentType;
+ }
+
+ }
+
+ /**
+ * Thrown when an attempt to open a connection results in a response code not in the 2xx range.
+ */
+ final class InvalidResponseCodeException extends HttpDataSourceException {
+
+ /**
+ * The response code that was outside of the 2xx range.
+ */
+ 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<String, List<String>> headerFields;
+
+ /** @deprecated Use {@link #InvalidResponseCodeException(int, String, Map, DataSpec)}. */
+ @Deprecated
+ public InvalidResponseCodeException(
+ int responseCode, Map<String, List<String>> headerFields, DataSpec dataSpec) {
+ this(responseCode, /* responseMessage= */ null, headerFields, dataSpec);
+ }
+
+ public InvalidResponseCodeException(
+ int responseCode,
+ @Nullable String responseMessage,
+ Map<String, List<String>> 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.
+ *
+ * <p>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;
+
+ @Override
+ void close() throws HttpDataSourceException;
+
+ @Override
+ int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException;
+
+ /**
+ * Sets the value of a request header. The value will be used for subsequent connections
+ * established by the source.
+ *
+ * <p>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.
+ */
+ void setRequestProperty(String name, String value);
+
+ /**
+ * Clears the value of a request header. The change will apply to subsequent connections
+ * established by the source.
+ *
+ * @param name The name of the header field.
+ */
+ void clearRequestProperty(String name);
+
+ /**
+ * Clears all request headers that were set by {@link #setRequestProperty(String, String)}.
+ */
+ void clearAllRequestProperties();
+
+ /**
+ * When the source is open, returns the HTTP response status code associated with the last {@link
+ * #open} call. Otherwise, returns a negative value.
+ */
+ int getResponseCode();
+
+ @Override
+ Map<String, List<String>> getResponseHeaders();
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoadErrorHandlingPolicy.java
new file mode 100644
index 0000000000..03c861c5f1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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.
+ *
+ * <p>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)}.
+ *
+ * <p>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.
+ *
+ * <p>{@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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Loader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Loader.java
new file mode 100644
index 0000000000..0e79759b36
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/Loader.java
@@ -0,0 +1,521 @@
+/*
+ * 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.upstream;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+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.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;
+
+/**
+ * Manages the background loading of {@link Loadable}s.
+ */
+public final class Loader implements LoaderErrorThrower {
+
+ /**
+ * Thrown when an unexpected exception or error is encountered during loading.
+ */
+ public static final class UnexpectedLoaderException extends IOException {
+
+ public UnexpectedLoaderException(Throwable cause) {
+ super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause);
+ }
+
+ }
+
+ /**
+ * An object that can be loaded using a {@link Loader}.
+ */
+ public interface Loadable {
+
+ /**
+ * Cancels the load.
+ */
+ void cancelLoad();
+
+ /**
+ * Performs the load, returning on completion or cancellation.
+ *
+ * @throws IOException If the input could not be loaded.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ void load() throws IOException, InterruptedException;
+
+ }
+
+ /**
+ * A callback to be notified of {@link Loader} events.
+ */
+ public interface Callback<T extends Loadable> {
+
+ /**
+ * Called when a load has completed.
+ *
+ * <p>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 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.
+ *
+ * <p>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 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.
+ */
+ void onLoadCanceled(T loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released);
+
+ /**
+ * Called when a load encounters an error.
+ *
+ * <p>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 in milliseconds of the load since {@link #startLoading}
+ * was called up to the point at which the error occurred.
+ * @param error The load error.
+ * @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}.
+ */
+ 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();
+
+ }
+
+ /** 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 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;
+
+ @Nullable private LoadTask<? extends Loadable> currentTask;
+ @Nullable private IOException fatalError;
+
+ /**
+ * @param threadName A name for the loader's thread.
+ */
+ public Loader(String threadName) {
+ 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}.
+ *
+ * <p>The calling thread must be a {@link Looper} thread, which is the thread on which the {@link
+ * Callback} will be called.
+ *
+ * @param <T> The type of the loadable.
+ * @param loadable The {@link Loadable} to load.
+ * @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 <T extends Loadable> long startLoading(
+ T loadable, Callback<T> 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 loader is currently loading. */
+ public boolean isLoading() {
+ return currentTask != null;
+ }
+
+ /**
+ * Cancels the current load.
+ *
+ * @throws IllegalStateException If the loader is not currently loading.
+ */
+ public void cancelLoading() {
+ Assertions.checkStateNotNull(currentTask).cancel(false);
+ }
+
+ /** Releases the loader. This method should be called when the loader is no longer required. */
+ public void release() {
+ release(null);
+ }
+
+ /**
+ * Releases the loader. This method should be called when the loader is no longer required.
+ *
+ * @param callback An optional callback to be called on the loading thread once the loader has
+ * been released.
+ */
+ public void release(@Nullable ReleaseCallback callback) {
+ if (currentTask != null) {
+ currentTask.cancel(true);
+ }
+ if (callback != null) {
+ downloadExecutorService.execute(new ReleaseTask(callback));
+ }
+ downloadExecutorService.shutdown();
+ }
+
+ // LoaderErrorThrower implementation.
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ maybeThrowError(Integer.MIN_VALUE);
+ }
+
+ @Override
+ public void maybeThrowError(int minRetryCount) throws IOException {
+ if (fatalError != null) {
+ throw fatalError;
+ } else if (currentTask != null) {
+ currentTask.maybeThrowError(minRetryCount == Integer.MIN_VALUE
+ ? currentTask.defaultMinRetryCount : minRetryCount);
+ }
+ }
+
+ // Internal classes.
+
+ @SuppressLint("HandlerLeak")
+ private final class LoadTask<T extends Loadable> extends Handler implements Runnable {
+
+ private static final String TAG = "LoadTask";
+
+ 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;
+
+ @Nullable private Loader.Callback<T> callback;
+ @Nullable private IOException currentError;
+ private int errorCount;
+
+ @Nullable private volatile Thread executorThread;
+ private volatile boolean canceled;
+ private volatile boolean released;
+
+ public LoadTask(Looper looper, T loadable, Loader.Callback<T> callback,
+ int defaultMinRetryCount, long startTimeMs) {
+ super(looper);
+ this.loadable = loadable;
+ this.callback = callback;
+ this.defaultMinRetryCount = defaultMinRetryCount;
+ this.startTimeMs = startTimeMs;
+ }
+
+ public void maybeThrowError(int minRetryCount) throws IOException {
+ if (currentError != null && errorCount > minRetryCount) {
+ throw currentError;
+ }
+ }
+
+ public void start(long delayMillis) {
+ Assertions.checkState(currentTask == null);
+ currentTask = this;
+ if (delayMillis > 0) {
+ sendEmptyMessageDelayed(MSG_START, delayMillis);
+ } else {
+ execute();
+ }
+ }
+
+ public void cancel(boolean released) {
+ this.released = released;
+ currentError = null;
+ if (hasMessages(MSG_START)) {
+ removeMessages(MSG_START);
+ if (!released) {
+ sendEmptyMessage(MSG_CANCEL);
+ }
+ } else {
+ canceled = true;
+ loadable.cancelLoad();
+ Thread executorThread = this.executorThread;
+ if (executorThread != null) {
+ executorThread.interrupt();
+ }
+ }
+ if (released) {
+ finish();
+ long nowMs = SystemClock.elapsedRealtime();
+ 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;
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ executorThread = Thread.currentThread();
+ if (!canceled) {
+ TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName());
+ try {
+ loadable.load();
+ } finally {
+ TraceUtil.endSection();
+ }
+ }
+ if (!released) {
+ sendEmptyMessage(MSG_END_OF_SOURCE);
+ }
+ } catch (IOException e) {
+ if (!released) {
+ obtainMessage(MSG_IO_EXCEPTION, e).sendToTarget();
+ }
+ } catch (InterruptedException e) {
+ // The load was canceled.
+ Assertions.checkState(canceled);
+ if (!released) {
+ sendEmptyMessage(MSG_END_OF_SOURCE);
+ }
+ } catch (Exception e) {
+ // This should never happen, but handle it anyway.
+ Log.e(TAG, "Unexpected exception loading stream", e);
+ 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
+ // the handler thread so that the process dies even if the executor behaves in this way.
+ Log.e(TAG, "Unexpected error loading stream", e);
+ if (!released) {
+ obtainMessage(MSG_FATAL_ERROR, e).sendToTarget();
+ }
+ throw e;
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (released) {
+ return;
+ }
+ if (msg.what == MSG_START) {
+ execute();
+ return;
+ }
+ if (msg.what == MSG_FATAL_ERROR) {
+ throw (Error) msg.obj;
+ }
+ finish();
+ long nowMs = SystemClock.elapsedRealtime();
+ long durationMs = nowMs - startTimeMs;
+ Loader.Callback<T> callback = Assertions.checkNotNull(this.callback);
+ if (canceled) {
+ callback.onLoadCanceled(loadable, nowMs, durationMs, false);
+ return;
+ }
+ switch (msg.what) {
+ case MSG_CANCEL:
+ callback.onLoadCanceled(loadable, nowMs, durationMs, false);
+ break;
+ case MSG_END_OF_SOURCE:
+ 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;
+ errorCount++;
+ LoadErrorAction action =
+ callback.onLoadError(loadable, nowMs, durationMs, currentError, errorCount);
+ if (action.type == ACTION_TYPE_DONT_RETRY_FATAL) {
+ fatalError = currentError;
+ } 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(Assertions.checkNotNull(currentTask));
+ }
+
+ private void finish() {
+ currentTask = null;
+ }
+
+ private long getRetryDelayMillis() {
+ return Math.min((errorCount - 1) * 1000, 5000);
+ }
+
+ }
+
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java
new file mode 100644
index 0000000000..9a67f20b84
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java
@@ -0,0 +1,63 @@
+/*
+ * 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.upstream;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Loader.Loadable;
+import java.io.IOException;
+
+/**
+ * Conditionally throws errors affecting a {@link Loader}.
+ */
+public interface LoaderErrorThrower {
+
+ /**
+ * Throws a fatal error, or a non-fatal error if loading is currently backed off and the current
+ * {@link Loadable} has incurred a number of errors greater than the {@link Loader}s default
+ * minimum number of retries. Else does nothing.
+ *
+ * @throws IOException The error.
+ */
+ void maybeThrowError() throws IOException;
+
+ /**
+ * Throws a fatal error, or a non-fatal error if loading is currently backed off and the current
+ * {@link Loadable} has incurred a number of errors greater than the specified minimum number
+ * of retries. Else does nothing.
+ *
+ * @param minRetryCount A minimum retry count that must be exceeded for a non-fatal error to be
+ * thrown. Should be non-negative.
+ * @throws IOException The error.
+ */
+ void maybeThrowError(int minRetryCount) throws IOException;
+
+ /**
+ * A {@link LoaderErrorThrower} that never throws.
+ */
+ final class Dummy implements LoaderErrorThrower {
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public void maybeThrowError(int minRetryCount) throws IOException {
+ // Do nothing.
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ParsingLoadable.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ParsingLoadable.java
new file mode 100644
index 0000000000..3e4192b651
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ParsingLoadable.java
@@ -0,0 +1,177 @@
+/*
+ * 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.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}.
+ *
+ * @param <T> The type of the object being loaded.
+ */
+public final class ParsingLoadable<T> implements Loadable {
+
+ /**
+ * Parses an object from loaded data.
+ */
+ public interface Parser<T> {
+
+ /**
+ * Parses an object from a response.
+ *
+ * @param uri The source {@link Uri} of the response, after any redirection.
+ * @param inputStream An {@link InputStream} from which the response data can be read.
+ * @return The parsed object.
+ * @throws ParserException If an error occurs parsing the data.
+ * @throws IOException If an error occurs reading data from the stream.
+ */
+ T parse(Uri uri, InputStream inputStream) throws IOException;
+
+ }
+
+ /**
+ * 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> T load(DataSource dataSource, Parser<? extends T> parser, Uri uri, int type)
+ throws IOException {
+ ParsingLoadable<T> 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> T load(
+ DataSource dataSource, Parser<? extends T> parser, DataSpec dataSpec, int type)
+ throws IOException {
+ ParsingLoadable<T> loadable = new ParsingLoadable<>(dataSource, dataSpec, type, parser);
+ loadable.load();
+ return Assertions.checkNotNull(loadable.getResult());
+ }
+
+ /**
+ * The {@link DataSpec} that defines the data to be loaded.
+ */
+ public final DataSpec dataSpec;
+ /**
+ * The type of the data. One of the {@code DATA_TYPE_*} constants defined in {@link C}. For
+ * reporting only.
+ */
+ public final int type;
+
+ private final StatsDataSource dataSource;
+ private final Parser<? extends T> parser;
+
+ private volatile @Nullable T result;
+
+ /**
+ * @param dataSource A {@link DataSource} to use when loading the data.
+ * @param uri The {@link Uri} from which the object should be loaded.
+ * @param type See {@link #type}.
+ * @param parser Parses the object from the response.
+ */
+ public ParsingLoadable(DataSource dataSource, Uri uri, int type, Parser<? extends T> parser) {
+ 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<? extends T> 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 @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 <em>after</em> decompression. Must only be called after
+ * the load completed, failed, or was canceled.
+ */
+ public long 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<String, List<String>> getResponseHeaders() {
+ return dataSource.getLastResponseHeaders();
+ }
+
+ @Override
+ public final void cancelLoad() {
+ // Do nothing.
+ }
+
+ @Override
+ 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();
+ Uri dataSourceUri = Assertions.checkNotNull(dataSource.getUri());
+ result = parser.parse(dataSourceUri, inputStream);
+ } finally {
+ Util.closeQuietly(inputStream);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSource.java
new file mode 100644
index 0000000000..18a7fb6238
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSource.java
@@ -0,0 +1,89 @@
+/*
+ * 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.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
+ * {@link PriorityTaskManager}.
+ * <p>
+ * Calls to {@link #open(DataSpec)} and {@link #read(byte[], int, int)} are allowed to proceed only
+ * if there are no higher priority tasks registered to the {@link PriorityTaskManager}. If there
+ * exists a higher priority task then {@link PriorityTaskManager.PriorityTooLowException} is thrown.
+ * <p>
+ * Instances of this class are intended to be used as parts of (possibly larger) tasks that are
+ * registered with the {@link PriorityTaskManager}, and hence do <em>not</em> register as tasks
+ * themselves.
+ */
+public final class PriorityDataSource implements DataSource {
+
+ private final DataSource upstream;
+ private final PriorityTaskManager priorityTaskManager;
+ private final int priority;
+
+ /**
+ * @param upstream The upstream {@link DataSource}.
+ * @param priorityTaskManager The priority manager to which the task is registered.
+ * @param priority The priority of the task.
+ */
+ public PriorityDataSource(DataSource upstream, PriorityTaskManager priorityTaskManager,
+ int priority) {
+ this.upstream = Assertions.checkNotNull(upstream);
+ this.priorityTaskManager = Assertions.checkNotNull(priorityTaskManager);
+ this.priority = priority;
+ }
+
+ @Override
+ public void addTransferListener(TransferListener transferListener) {
+ upstream.addTransferListener(transferListener);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ priorityTaskManager.proceedOrThrow(priority);
+ return upstream.open(dataSpec);
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int max) throws IOException {
+ priorityTaskManager.proceedOrThrow(priority);
+ return upstream.read(buffer, offset, max);
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return upstream.getUri();
+ }
+
+ @Override
+ public Map<String, List<String>> getResponseHeaders() {
+ return upstream.getResponseHeaders();
+ }
+
+ @Override
+ public void close() throws IOException {
+ upstream.close();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java
new file mode 100644
index 0000000000..cf9a89f51d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java
@@ -0,0 +1,49 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource.Factory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.PriorityTaskManager;
+
+/**
+ * A {@link DataSource.Factory} that produces {@link PriorityDataSource} instances.
+ */
+public final class PriorityDataSourceFactory implements Factory {
+
+ private final Factory upstreamFactory;
+ private final PriorityTaskManager priorityTaskManager;
+ private final int priority;
+
+ /**
+ * @param upstreamFactory A {@link DataSource.Factory} to be used to create an upstream {@link
+ * DataSource} for {@link PriorityDataSource}.
+ * @param priorityTaskManager The priority manager to which PriorityDataSource task is registered.
+ * @param priority The priority of PriorityDataSource task.
+ */
+ public PriorityDataSourceFactory(Factory upstreamFactory, PriorityTaskManager priorityTaskManager,
+ int priority) {
+ this.upstreamFactory = upstreamFactory;
+ this.priorityTaskManager = priorityTaskManager;
+ this.priority = priority;
+ }
+
+ @Override
+ public PriorityDataSource createDataSource() {
+ return new PriorityDataSource(upstreamFactory.createDataSource(), priorityTaskManager,
+ priority);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/RawResourceDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/RawResourceDataSource.java
new file mode 100644
index 0000000000..ec5263d8ac
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/RawResourceDataSource.java
@@ -0,0 +1,199 @@
+/*
+ * 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.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;
+import java.io.InputStream;
+
+/**
+ * A {@link DataSource} for reading a raw resource inside the APK.
+ *
+ * <p>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 extends BaseDataSource {
+
+ /**
+ * Thrown when an {@link IOException} is encountered reading from a raw resource.
+ */
+ public static class RawResourceDataSourceException extends IOException {
+ public RawResourceDataSourceException(String message) {
+ super(message);
+ }
+
+ public RawResourceDataSourceException(IOException e) {
+ super(e);
+ }
+ }
+
+ /**
+ * Builds a {@link Uri} for the specified raw resource identifier.
+ *
+ * @param rawResourceId A raw resource identifier (i.e. a constant defined in {@code R.raw}).
+ * @return The corresponding {@link Uri}.
+ */
+ public static Uri buildRawResourceUri(int rawResourceId) {
+ return Uri.parse(RAW_RESOURCE_SCHEME + ":///" + rawResourceId);
+ }
+
+ /** The scheme part of a raw resource URI. */
+ public static final String RAW_RESOURCE_SCHEME = "rawresource";
+
+ private final Resources resources;
+
+ @Nullable private Uri uri;
+ @Nullable private AssetFileDescriptor assetFileDescriptor;
+ @Nullable private InputStream inputStream;
+ private long bytesRemaining;
+ private boolean opened;
+
+ /**
+ * @param context A context.
+ */
+ public RawResourceDataSource(Context context) {
+ super(/* isNetwork= */ false);
+ this.resources = context.getResources();
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws RawResourceDataSourceException {
+ try {
+ 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(Assertions.checkNotNull(uri.getLastPathSegment()));
+ } catch (NumberFormatException e) {
+ throw new RawResourceDataSourceException("Resource identifier must be an integer.");
+ }
+
+ 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) {
+ // 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();
+ }
+ if (dataSpec.length != C.LENGTH_UNSET) {
+ bytesRemaining = dataSpec.length;
+ } else {
+ long assetFileDescriptorLength = assetFileDescriptor.getLength();
+ // If the length is UNKNOWN_LENGTH then the asset extends to the end of the file.
+ bytesRemaining = assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH
+ ? C.LENGTH_UNSET : (assetFileDescriptorLength - dataSpec.position);
+ }
+ } catch (IOException e) {
+ throw new RawResourceDataSourceException(e);
+ }
+
+ opened = true;
+ transferStarted(dataSpec);
+
+ return bytesRemaining;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws RawResourceDataSourceException {
+ if (readLength == 0) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ int bytesRead;
+ try {
+ int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
+ : (int) Math.min(bytesRemaining, readLength);
+ bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead);
+ } catch (IOException e) {
+ throw new RawResourceDataSourceException(e);
+ }
+
+ if (bytesRead == -1) {
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ // End of stream reached having not read sufficient data.
+ throw new RawResourceDataSourceException(new EOFException());
+ }
+ return C.RESULT_END_OF_INPUT;
+ }
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ bytesTransferred(bytesRead);
+ return bytesRead;
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return uri;
+ }
+
+ @SuppressWarnings("Finally")
+ @Override
+ public void close() throws RawResourceDataSourceException {
+ uri = null;
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (IOException e) {
+ throw new RawResourceDataSourceException(e);
+ } finally {
+ inputStream = null;
+ try {
+ if (assetFileDescriptor != null) {
+ assetFileDescriptor.close();
+ }
+ } catch (IOException e) {
+ throw new RawResourceDataSourceException(e);
+ } finally {
+ assetFileDescriptor = null;
+ if (opened) {
+ opened = false;
+ transferEnded();
+ }
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ResolvingDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/ResolvingDataSource.java
new file mode 100644
index 0000000000..80046e1757
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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.
+ *
+ * <p>Implementations do not need to overwrite this method unless they want to change the
+ * reported URI.
+ *
+ * <p>This method is <em>not</em> 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<String, List<String>> getResponseHeaders() {
+ return upstreamDataSource.getResponseHeaders();
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (upstreamOpened) {
+ upstreamOpened = false;
+ upstreamDataSource.close();
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/StatsDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/StatsDataSource.java
new file mode 100644
index 0000000000..e2a179cc9d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<String, List<String>> 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<String, List<String>> 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<String, List<String>> getResponseHeaders() {
+ return dataSource.getResponseHeaders();
+ }
+
+ @Override
+ public void close() throws IOException {
+ dataSource.close();
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TeeDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TeeDataSource.java
new file mode 100644
index 0000000000..c6063b916f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TeeDataSource.java
@@ -0,0 +1,105 @@
+/*
+ * 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.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.
+ */
+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.
+ */
+ public TeeDataSource(DataSource upstream, DataSink dataSink) {
+ this.upstream = Assertions.checkNotNull(upstream);
+ this.dataSink = Assertions.checkNotNull(dataSink);
+ }
+
+ @Override
+ public void addTransferListener(TransferListener transferListener) {
+ upstream.addTransferListener(transferListener);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ 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 bytesRemaining;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int max) throws IOException {
+ if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ 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<String, List<String>> getResponseHeaders() {
+ return upstream.getResponseHeaders();
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ upstream.close();
+ } finally {
+ if (dataSinkNeedsClosing) {
+ dataSinkNeedsClosing = false;
+ dataSink.close();
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TransferListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TransferListener.java
new file mode 100644
index 0000000000..f6574120ff
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/TransferListener.java
@@ -0,0 +1,77 @@
+/*
+ * 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.upstream;
+
+/**
+ * A listener of data transfer events.
+ *
+ * <p>A transfer usually progresses through multiple steps:
+ *
+ * <ol>
+ * <li>Initializing the underlying resource (e.g. opening a HTTP connection). {@link
+ * #onTransferInitializing(DataSource, DataSpec, boolean)} is called before the initialization
+ * starts.
+ * <li>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.
+ * <li>Transferring data. {@link #onBytesTransferred(DataSource, DataSpec, boolean, int)} is
+ * called frequently during the transfer to indicate progress.
+ * <li>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)}.
+ * </ol>
+ */
+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(DataSource source, DataSpec dataSpec, boolean isNetwork);
+
+ /**
+ * Called incrementally during a transfer.
+ *
+ * @param source The source performing the transfer.
+ * @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(
+ 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(DataSource source, DataSpec dataSpec, boolean isNetwork);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/UdpDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/UdpDataSource.java
new file mode 100644
index 0000000000..8e9b44563c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/UdpDataSource.java
@@ -0,0 +1,176 @@
+/*
+ * 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.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;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.SocketException;
+
+/** A UDP {@link DataSource}. */
+public final class UdpDataSource extends BaseDataSource {
+
+ /**
+ * Thrown when an error is encountered when trying to read from a {@link UdpDataSource}.
+ */
+ public static final class UdpDataSourceException extends IOException {
+
+ public UdpDataSourceException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+ /**
+ * The default maximum datagram packet size, in bytes.
+ */
+ public static final int DEFAULT_MAX_PACKET_SIZE = 2000;
+
+ /** The default socket timeout, in milliseconds. */
+ public static final int DEFAULT_SOCKET_TIMEOUT_MILLIS = 8 * 1000;
+
+ private final int socketTimeoutMillis;
+ private final byte[] packetBuffer;
+ private final DatagramPacket packet;
+
+ @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;
+
+ public UdpDataSource() {
+ this(DEFAULT_MAX_PACKET_SIZE);
+ }
+
+ /**
+ * Constructs a new instance.
+ *
+ * @param maxPacketSize The maximum datagram packet size, in bytes.
+ */
+ public UdpDataSource(int maxPacketSize) {
+ this(maxPacketSize, DEFAULT_SOCKET_TIMEOUT_MILLIS);
+ }
+
+ /**
+ * 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(int maxPacketSize, int socketTimeoutMillis) {
+ super(/* isNetwork= */ true);
+ this.socketTimeoutMillis = socketTimeoutMillis;
+ packetBuffer = new byte[maxPacketSize];
+ packet = new DatagramPacket(packetBuffer, 0, maxPacketSize);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws UdpDataSourceException {
+ uri = dataSpec.uri;
+ String host = uri.getHost();
+ int port = uri.getPort();
+ transferInitializing(dataSpec);
+ try {
+ address = InetAddress.getByName(host);
+ socketAddress = new InetSocketAddress(address, port);
+ if (address.isMulticastAddress()) {
+ multicastSocket = new MulticastSocket(socketAddress);
+ multicastSocket.joinGroup(address);
+ socket = multicastSocket;
+ } else {
+ socket = new DatagramSocket(socketAddress);
+ }
+ } catch (IOException e) {
+ throw new UdpDataSourceException(e);
+ }
+
+ try {
+ socket.setSoTimeout(socketTimeoutMillis);
+ } catch (SocketException e) {
+ throw new UdpDataSourceException(e);
+ }
+
+ opened = true;
+ transferStarted(dataSpec);
+ return C.LENGTH_UNSET;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws UdpDataSourceException {
+ if (readLength == 0) {
+ return 0;
+ }
+
+ if (packetRemaining == 0) {
+ // We've read all of the data from the current packet. Get another.
+ try {
+ socket.receive(packet);
+ } catch (IOException e) {
+ throw new UdpDataSourceException(e);
+ }
+ packetRemaining = packet.getLength();
+ bytesTransferred(packetRemaining);
+ }
+
+ int packetOffset = packet.getLength() - packetRemaining;
+ int bytesToRead = Math.min(packetRemaining, readLength);
+ System.arraycopy(packetBuffer, packetOffset, buffer, offset, bytesToRead);
+ packetRemaining -= bytesToRead;
+ return bytesToRead;
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return uri;
+ }
+
+ @Override
+ public void close() {
+ uri = null;
+ if (multicastSocket != null) {
+ try {
+ multicastSocket.leaveGroup(address);
+ } catch (IOException e) {
+ // Do nothing.
+ }
+ multicastSocket = null;
+ }
+ if (socket != null) {
+ socket.close();
+ socket = null;
+ }
+ address = null;
+ socketAddress = null;
+ packetRemaining = 0;
+ if (opened) {
+ opened = false;
+ transferEnded();
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/Cache.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/Cache.java
new file mode 100644
index 0000000000..cb90d95bb4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/Cache.java
@@ -0,0 +1,286 @@
+/*
+ * 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.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;
+import java.util.Set;
+
+/**
+ * An interface for cache.
+ */
+public interface Cache {
+
+ /**
+ * Listener of {@link Cache} events.
+ */
+ interface Listener {
+
+ /**
+ * Called when a {@link CacheSpan} is added to the cache.
+ *
+ * @param cache The source of the event.
+ * @param span The added {@link CacheSpan}.
+ */
+ void onSpanAdded(Cache cache, CacheSpan span);
+
+ /**
+ * Called when a {@link CacheSpan} is removed from the cache.
+ *
+ * @param cache The source of the event.
+ * @param span The removed {@link CacheSpan}.
+ */
+ void onSpanRemoved(Cache cache, CacheSpan span);
+
+ /**
+ * 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#lastTouchTimestamp} may have changed.
+ *
+ * <p>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.
+ */
+ class CacheException extends IOException {
+
+ public CacheException(String message) {
+ super(message);
+ }
+
+ 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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ * @return The current spans for the key.
+ */
+ NavigableSet<CacheSpan> addListener(String key, Listener listener);
+
+ /**
+ * Unregisters a listener.
+ *
+ * @param key The key to stop listening to.
+ * @param listener The listener to remove.
+ */
+ void removeListener(String key, Listener listener);
+
+ /**
+ * 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.
+ */
+ NavigableSet<CacheSpan> getCachedSpans(String key);
+
+ /**
+ * Returns all keys in the cache.
+ *
+ * @return All the keys in the cache.
+ */
+ Set<String> getKeys();
+
+ /**
+ * Returns the total disk space in bytes used by the cache.
+ *
+ * @return The total disk space in bytes.
+ */
+ long getCacheSpace();
+
+ /**
+ * A caller should invoke this method when they require data from a given position for a given
+ * key.
+ *
+ * <p>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.
+ *
+ * <p>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, long)}. When the caller has finished writing, it must release
+ * the lock by calling {@link #releaseHoleSpan}.
+ *
+ * <p>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 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}.
+ *
+ * <p>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)}.
+ *
+ * <p>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 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.
+ */
+ @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)}.
+ *
+ * <p>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.
+ */
+ @WorkerThread
+ void commitFile(File file, long length) throws CacheException;
+
+ /**
+ * Releases a {@link CacheSpan} obtained from {@link #startReadWrite(String, long)} which
+ * corresponded to a hole in the cache.
+ *
+ * @param holeSpan The {@link CacheSpan} being released.
+ */
+ void releaseHoleSpan(CacheSpan holeSpan);
+
+ /**
+ * Removes a cached {@link CacheSpan} from the cache, deleting the underlying file.
+ *
+ * <p>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;
+ */
+ boolean isCached(String key, long position, long length);
+
+ /**
+ * Returns the length of the cached data block starting from the {@code position} to the block end
+ * up to {@code length} bytes. If the {@code position} isn't cached then -(the length of the gap
+ * to the next cached data up to {@code length} bytes) is returned.
+ *
+ * @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.
+ */
+ long getCachedLength(String key, long position, long length);
+
+ /**
+ * 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.
+ *
+ * <p>This method may be slow and shouldn't normally be called on the main thread.
+ *
+ * @param key The cache key for the data.
+ * @param mutations Contains mutations to be applied to the metadata.
+ * @throws CacheException If an error is encountered.
+ */
+ @WorkerThread
+ void applyContentMetadataMutations(String key, ContentMetadataMutations mutations)
+ throws CacheException;
+
+ /**
+ * Returns a {@link ContentMetadata} for the given key.
+ *
+ * @param key The cache key for the data.
+ * @return A {@link ContentMetadata} for the given key.
+ */
+ ContentMetadata getContentMetadata(String key);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java
new file mode 100644
index 0000000000..e372a02851
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java
@@ -0,0 +1,210 @@
+/*
+ * 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.upstream.cache;
+
+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.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;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Writes data into a cache.
+ *
+ * <p>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 {@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 fragmentSize;
+ private final int bufferSize;
+
+ private DataSpec dataSpec;
+ private long dataSpecFragmentSize;
+ private File file;
+ private OutputStream outputStream;
+ private long outputStreamBytesWritten;
+ private long dataSpecBytesWritten;
+ private ReusableBufferedOutputStream bufferedOutputStream;
+
+ /**
+ * Thrown when IOException is encountered when writing data into sink.
+ */
+ public static class CacheDataSinkException extends CacheException {
+
+ public CacheDataSinkException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+ /**
+ * Constructs an instance using {@link #DEFAULT_BUFFER_SIZE}.
+ *
+ * @param cache The cache into which data should be written.
+ * @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 fragmentSize) {
+ this(cache, fragmentSize, DEFAULT_BUFFER_SIZE);
+ }
+
+ /**
+ * @param cache The cache into which data should be written.
+ * @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.
+ */
+ 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.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_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();
+ } catch (IOException e) {
+ throw new CacheDataSinkException(e);
+ }
+ }
+
+ @Override
+ public void write(byte[] buffer, int offset, int length) throws CacheDataSinkException {
+ if (dataSpec == null) {
+ return;
+ }
+ try {
+ int bytesWritten = 0;
+ while (bytesWritten < length) {
+ if (outputStreamBytesWritten == dataSpecFragmentSize) {
+ closeCurrentOutputStream();
+ openNextOutputStream();
+ }
+ int bytesToWrite =
+ (int) Math.min(length - bytesWritten, dataSpecFragmentSize - outputStreamBytesWritten);
+ outputStream.write(buffer, offset + bytesWritten, bytesToWrite);
+ bytesWritten += bytesToWrite;
+ outputStreamBytesWritten += bytesToWrite;
+ dataSpecBytesWritten += bytesToWrite;
+ }
+ } catch (IOException e) {
+ throw new CacheDataSinkException(e);
+ }
+ }
+
+ @Override
+ public void close() throws CacheDataSinkException {
+ if (dataSpec == null) {
+ return;
+ }
+ try {
+ closeCurrentOutputStream();
+ } catch (IOException e) {
+ throw new CacheDataSinkException(e);
+ }
+ }
+
+ private void openNextOutputStream() throws IOException {
+ 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,
+ bufferSize);
+ } else {
+ bufferedOutputStream.reset(underlyingFileOutputStream);
+ }
+ outputStream = bufferedOutputStream;
+ } else {
+ outputStream = underlyingFileOutputStream;
+ }
+ outputStreamBytesWritten = 0;
+ }
+
+ private void closeCurrentOutputStream() throws IOException {
+ if (outputStream == null) {
+ return;
+ }
+
+ boolean success = false;
+ try {
+ outputStream.flush();
+ success = true;
+ } finally {
+ Util.closeQuietly(outputStream);
+ outputStream = null;
+ File fileToCommit = file;
+ file = null;
+ if (success) {
+ cache.commitFile(fileToCommit, outputStreamBytesWritten);
+ } else {
+ fileToCommit.delete();
+ }
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java
new file mode 100644
index 0000000000..51ba6f4294
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java
@@ -0,0 +1,45 @@
+/*
+ * 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.upstream.cache;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSink;
+
+/**
+ * A {@link DataSink.Factory} that produces {@link CacheDataSink}.
+ */
+public final class CacheDataSinkFactory implements DataSink.Factory {
+
+ private final Cache cache;
+ private final long fragmentSize;
+ private final int bufferSize;
+
+ /** @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 fragmentSize, int bufferSize) {
+ this.cache = cache;
+ this.fragmentSize = fragmentSize;
+ this.bufferSize = bufferSize;
+ }
+
+ @Override
+ public DataSink createDataSink() {
+ return new CacheDataSink(cache, fragmentSize, bufferSize);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
new file mode 100644
index 0000000000..19fb8191e4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
@@ -0,0 +1,580 @@
+/*
+ * 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.upstream.cache;
+
+import android.net.Uri;
+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
+ * when possible. When data is not cached it is requested from an upstream {@link DataSource} and
+ * written into the cache.
+ */
+public final class CacheDataSource implements DataSource {
+
+ /**
+ * 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
+ })
+ public @interface Flags {}
+ /**
+ * 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;
+
+ /**
+ * 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; // 2
+
+ /**
+ * 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; // 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.
+ */
+ public interface EventListener {
+
+ /**
+ * Called when bytes have been read from the cache.
+ *
+ * @param cacheSizeBytes Current cache size in bytes.
+ * @param cachedBytesRead Total bytes read from the cache since this method was last called.
+ */
+ 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;
+ @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;
+
+ @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<String, String> httpRequestHeaders = Collections.emptyMap();
+ @DataSpec.Flags private int flags;
+ @Nullable private String key;
+ private long readPosition;
+ private long bytesRemaining;
+ @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.
+ *
+ * @param cache The cache.
+ * @param upstream A {@link DataSource} for reading data not in the cache.
+ */
+ 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.
+ *
+ * @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}, {@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) {
+ this(
+ cache,
+ upstream,
+ new FileDataSource(),
+ new CacheDataSink(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE),
+ flags,
+ /* eventListener= */ 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.
+ */
+ 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 =
+ (flags & FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS) != 0;
+ this.upstreamDataSource = upstream;
+ if (cacheWriteDataSink != null) {
+ this.cacheWriteDataSource = new TeeDataSource(upstream, cacheWriteDataSink);
+ } else {
+ this.cacheWriteDataSource = null;
+ }
+ 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;
+ readPosition = dataSpec.position;
+
+ 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 = ContentMetadata.getContentLength(cache.getContentMetadata(key));
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= dataSpec.position;
+ if (bytesRemaining <= 0) {
+ throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
+ }
+ }
+ }
+ openNextSource(false);
+ return bytesRemaining;
+ } catch (Throwable e) {
+ handleBeforeThrow(e);
+ throw e;
+ }
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ if (readLength == 0) {
+ return 0;
+ }
+ if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ try {
+ if (readPosition >= checkCachePosition) {
+ openNextSource(true);
+ }
+ int bytesRead = currentDataSource.read(buffer, offset, readLength);
+ if (bytesRead != C.RESULT_END_OF_INPUT) {
+ if (isReadingFromCache()) {
+ totalCachedBytesRead += bytesRead;
+ }
+ readPosition += bytesRead;
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ } else if (currentDataSpecLengthUnset) {
+ setNoBytesRemainingAndMaybeStoreLength();
+ } else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {
+ closeCurrentSource();
+ 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 actualUri;
+ }
+
+ @Override
+ public Map<String, List<String>> 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 (Throwable e) {
+ handleBeforeThrow(e);
+ throw e;
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * <p>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 void openNextSource(boolean checkCache) throws IOException {
+ CacheSpan nextSpan;
+ if (currentRequestIgnoresCache) {
+ nextSpan = null;
+ } else if (blockOnCache) {
+ try {
+ nextSpan = cache.startReadWrite(key, readPosition);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new InterruptedIOException();
+ }
+ } else {
+ nextSpan = cache.startReadWriteNonBlocking(key, readPosition);
+ }
+
+ 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.
+ 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(nextSpan.file);
+ long filePosition = readPosition - nextSpan.position;
+ long length = nextSpan.length - filePosition;
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ length = Math.min(length, bytesRemaining);
+ }
+ // 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 (nextSpan.isOpenEnded()) {
+ length = bytesRemaining;
+ } else {
+ length = nextSpan.length;
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ length = Math.min(length, bytesRemaining);
+ }
+ }
+ nextDataSpec =
+ new DataSpec(
+ uri,
+ httpMethod,
+ httpBody,
+ readPosition,
+ readPosition,
+ length,
+ key,
+ flags,
+ httpRequestHeaders);
+ if (cacheWriteDataSource != null) {
+ nextDataSource = cacheWriteDataSource;
+ } else {
+ nextDataSource = upstreamDataSource;
+ cache.releaseHoleSpan(nextSpan);
+ nextSpan = null;
+ }
+ }
+
+ 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;
+ }
+ // 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 (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);
+ }
+ }
+
+ 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();
+ } finally {
+ currentDataSource = null;
+ currentDataSpecLengthUnset = false;
+ if (currentHoleSpan != null) {
+ cache.releaseHoleSpan(currentHoleSpan);
+ currentHoleSpan = null;
+ }
+ }
+ }
+
+ 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);
+ totalCachedBytesRead = 0;
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java
new file mode 100644
index 0000000000..21aef3f93a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java
@@ -0,0 +1,112 @@
+/*
+ * 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.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.FileDataSource;
+
+/** 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;
+ @CacheDataSource.Flags private final int flags;
+ @Nullable private final DataSink.Factory cacheWriteDataSinkFactory;
+ @Nullable private final CacheDataSource.EventListener eventListener;
+ @Nullable private final CacheKeyFactory cacheKeyFactory;
+
+ /**
+ * 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) {
+ this(cache, upstreamFactory, /* flags= */ 0);
+ }
+
+ /** @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,
+ * CacheDataSource.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(),
+ cacheReadDataSourceFactory.createDataSource(),
+ cacheWriteDataSinkFactory == null ? null : cacheWriteDataSinkFactory.createDataSink(),
+ flags,
+ eventListener,
+ cacheKeyFactory);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java
new file mode 100644
index 0000000000..017e84c8c8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java
@@ -0,0 +1,47 @@
+/*
+ * 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.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.
+ */
+ void onCacheInitialized();
+
+ /**
+ * Called when a writer starts writing to the cache.
+ *
+ * @param cache The source of the event.
+ * @param key The key being written.
+ * @param position The starting position 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 length);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java
new file mode 100644
index 0000000000..2618a3ef6a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java
new file mode 100644
index 0000000000..cd69336ff4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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<String, CacheFileMetadata> getAll() throws DatabaseIOException {
+ try (Cursor cursor = getCursor()) {
+ Map<String, CacheFileMetadata> 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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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<String> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheKeyFactory.java
new file mode 100644
index 0000000000..1c30a4b03e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheSpan.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheSpan.java
new file mode 100644
index 0000000000..f57544f12b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheSpan.java
@@ -0,0 +1,106 @@
+/*
+ * 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.upstream.cache;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.io.File;
+
+/**
+ * Defines a span of data that may or may not be cached (as indicated by {@link #isCached}).
+ */
+public class CacheSpan implements Comparable<CacheSpan> {
+
+ /**
+ * The cache key that uniquely identifies the original stream.
+ */
+ public final String key;
+ /**
+ * The position of the {@link CacheSpan} in the original stream.
+ */
+ public final long position;
+ /**
+ * The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an open-ended hole.
+ */
+ public final long length;
+ /**
+ * Whether the {@link CacheSpan} is cached.
+ */
+ public final boolean isCached;
+ /** 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 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.
+ * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an
+ * open-ended hole.
+ */
+ public CacheSpan(String key, long position, long length) {
+ this(key, position, length, C.TIME_UNSET, null);
+ }
+
+ /**
+ * Creates a CacheSpan.
+ *
+ * @param key The cache key that uniquely identifies the original stream.
+ * @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.
+ */
+ 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.lastTouchTimestamp = lastTouchTimestamp;
+ }
+
+ /**
+ * Returns whether this is an open-ended {@link CacheSpan}.
+ */
+ public boolean isOpenEnded() {
+ return length == C.LENGTH_UNSET;
+ }
+
+ /**
+ * Returns whether this is a hole {@link CacheSpan}.
+ */
+ public boolean isHoleSpan() {
+ return !isCached;
+ }
+
+ @Override
+ public int compareTo(@NonNull CacheSpan another) {
+ if (!key.equals(another.key)) {
+ return key.compareTo(another.key);
+ }
+ long startOffsetDiff = position - another.position;
+ return startOffsetDiff == 0 ? 0 : ((startOffsetDiff < 0) ? -1 : 1);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
new file mode 100644
index 0000000000..01fef2b605
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
@@ -0,0 +1,434 @@
+/*
+ * 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.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 {
+
+ /** Receives progress updates during cache operations. */
+ public interface ProgressListener {
+
+ /**
+ * Called when progress is made during a cache operation.
+ *
+ * @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.
+ */
+ 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}.
+ *
+ * @param uri Uri of a content which the requested key is for.
+ */
+ public static String generateKey(Uri uri) {
+ return uri.toString();
+ }
+
+ /**
+ * 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 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 Pair<Long, Long> 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}, skipping already cached data. Caching stops early
+ * if the end of the input is reached.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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 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 directly or via {@code isCanceled}.
+ */
+ @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);
+
+ String key = buildCacheKey(dataSpec, cacheKeyFactory);
+ long bytesLeft;
+ ProgressNotifier progressNotifier = null;
+ if (progressListener != null) {
+ progressNotifier = new ProgressNotifier(progressListener);
+ Pair<Long, Long> lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory);
+ progressNotifier.init(lengthAndBytesAlreadyCached.first, lengthAndBytesAlreadyCached.second);
+ bytesLeft = lengthAndBytesAlreadyCached.first;
+ } else {
+ bytesLeft = getRequestLength(dataSpec, cache, key);
+ }
+
+ 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) {
+ // Skip already cached data.
+ } else {
+ // There is a hole in the cache which is at least "-blockLength" long.
+ blockLength = -blockLength;
+ 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();
+ }
+ break;
+ }
+ }
+ position += blockLength;
+ if (!lengthUnset) {
+ bytesLeft -= blockLength;
+ }
+ }
+ }
+
+ 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. {@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.
+ */
+ 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 {
+ 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);
+ }
+ }
+ 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 {
+ Util.closeQuietly(dataSource);
+ }
+ }
+ }
+
+ /**
+ * Removes all of the data specified by the {@code dataSpec}.
+ *
+ * <p>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}.
+ *
+ * <p>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<CacheSpan> cachedSpans = cache.getCachedSpans(key);
+ for (CacheSpan cachedSpan : cachedSpans) {
+ try {
+ cache.removeSpan(cachedSpan);
+ } catch (Cache.CacheException e) {
+ // 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContent.java
new file mode 100644
index 0000000000..660a2a3cb3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContent.java
@@ -0,0 +1,208 @@
+/*
+ * 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.upstream.cache;
+
+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 java.io.File;
+import java.util.TreeSet;
+
+/** Defines the cached content for a single stream. */
+/* package */ final class CachedContent {
+
+ 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. */
+ public final String key;
+ /** The cached spans of this content. */
+ private final TreeSet<SimpleCacheSpan> cachedSpans;
+ /** 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.
+ */
+ 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.metadata = metadata;
+ this.cachedSpans = new TreeSet<>();
+ }
+
+ /** Returns the metadata. */
+ public DefaultContentMetadata getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * Applies {@code mutations} to the metadata.
+ *
+ * @return Whether {@code mutations} changed any metadata.
+ */
+ public boolean applyMetadataMutations(ContentMetadataMutations mutations) {
+ DefaultContentMetadata oldMetadata = metadata;
+ metadata = metadata.copyWithMutationsApplied(mutations);
+ return !metadata.equals(oldMetadata);
+ }
+
+ /** Returns whether the content is locked. */
+ public boolean isLocked() {
+ return locked;
+ }
+
+ /** 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. */
+ public void addSpan(SimpleCacheSpan span) {
+ cachedSpans.add(span);
+ }
+
+ /** Returns a set of all {@link SimpleCacheSpan}s. */
+ public TreeSet<SimpleCacheSpan> getSpans() {
+ return cachedSpans;
+ }
+
+ /**
+ * Returns the span containing the position. If there isn't one, it returns a hole span
+ * which defines the maximum extents of the hole in the cache.
+ */
+ public SimpleCacheSpan getSpan(long position) {
+ SimpleCacheSpan lookupSpan = SimpleCacheSpan.createLookup(key, position);
+ SimpleCacheSpan floorSpan = cachedSpans.floor(lookupSpan);
+ if (floorSpan != null && floorSpan.position + floorSpan.length > position) {
+ return floorSpan;
+ }
+ SimpleCacheSpan ceilSpan = cachedSpans.ceiling(lookupSpan);
+ return ceilSpan == null ? SimpleCacheSpan.createOpenHole(key, position)
+ : SimpleCacheSpan.createClosedHole(key, position, ceilSpan.position - position);
+ }
+
+ /**
+ * Returns the length of the cached data block starting from the {@code position} to the block end
+ * up to {@code length} bytes. If the {@code position} isn't cached then -(the length of the gap
+ * to the next cached data up to {@code length} bytes) is returned.
+ *
+ * @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.
+ */
+ 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.
+ return -Math.min(span.isOpenEnded() ? Long.MAX_VALUE : span.length, length);
+ }
+ long queryEndPosition = position + length;
+ long currentEndPosition = span.position + span.length;
+ if (currentEndPosition < queryEndPosition) {
+ for (SimpleCacheSpan next : cachedSpans.tailSet(span, false)) {
+ if (next.position > currentEndPosition) {
+ // There's a hole in the cache within the queried region.
+ break;
+ }
+ // We expect currentEndPosition to always equal (next.position + next.length), but
+ // perform a max check anyway to guard against the existence of overlapping spans.
+ currentEndPosition = Math.max(currentEndPosition, next.position + next.length);
+ if (currentEndPosition >= queryEndPosition) {
+ // We've found spans covering the queried region.
+ break;
+ }
+ }
+ }
+ return Math.min(currentEndPosition - position, length);
+ }
+
+ /**
+ * Sets the given span's last touch timestamp. The passed span becomes invalid after this call.
+ *
+ * @param cacheSpan Span to be copied and updated.
+ * @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 setLastTouchTimestamp(
+ SimpleCacheSpan cacheSpan, long lastTouchTimestamp, boolean updateFile) {
+ Assertions.checkState(cachedSpans.remove(cacheSpan));
+ 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);
+ }
+ }
+ SimpleCacheSpan newCacheSpan =
+ cacheSpan.copyWithFileAndLastTouchTimestamp(file, lastTouchTimestamp);
+ cachedSpans.add(newCacheSpan);
+ return newCacheSpan;
+ }
+
+ /** Returns whether there are any spans cached. */
+ public boolean isEmpty() {
+ return cachedSpans.isEmpty();
+ }
+
+ /** Removes the given span from cache. */
+ public boolean removeSpan(CacheSpan span) {
+ if (cachedSpans.remove(span)) {
+ span.file.delete();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id;
+ result = 31 * result + key.hashCode();
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
new file mode 100644
index 0000000000..ac31e492a2
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
@@ -0,0 +1,956 @@
+/*
+ * 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.upstream.cache;
+
+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 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.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.Map;
+import java.util.Random;
+import java.util.Set;
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+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;
+
+/** Maintains the index of cached content. */
+/* package */ class CachedContentIndex {
+
+ /* package */ static final String FILE_NAME_ATOMIC = "cached_content_index.exi";
+
+ private static final int INCREMENTAL_METADATA_READ_LENGTH = 10 * 1024 * 1024;
+
+ private final HashMap<String, CachedContent> keyToContent;
+ /**
+ * 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:
+ *
+ * <p>[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
+ *
+ * <p>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.
+ *
+ * <p>When the index is next stored (id -> null) entries are removed, making the ids eligible for
+ * reuse.
+ */
+ 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);
+ }
+
+ /**
+ * Deletes index data for the specified cache.
+ *
+ * <p>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 {
+ 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<>();
+ 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;
+ }
+ }
+
+ /**
+ * Loads the index data for the given cache UID.
+ *
+ * <p>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);
+ }
+ 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.
+ *
+ * <p>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();
+ }
+
+ /**
+ * Adds the given key to the index if it isn't there already.
+ *
+ * @param key The cache key that uniquely identifies the original stream.
+ * @return A new or existing CachedContent instance with the given key.
+ */
+ public CachedContent getOrAdd(String key) {
+ CachedContent cachedContent = keyToContent.get(key);
+ return cachedContent == null ? addNew(key) : cachedContent;
+ }
+
+ /** Returns a CachedContent instance with the given key or null if there isn't one. */
+ public CachedContent get(String key) {
+ return keyToContent.get(key);
+ }
+
+ /**
+ * Returns a Collection of all CachedContent instances in the index. The collection is backed by
+ * the {@code keyToContent} map, so changes to the map are reflected in the collection, and
+ * vice-versa. If the map is modified while an iteration over the collection is in progress
+ * (except through the iterator's own remove operation), the results of the iteration are
+ * undefined.
+ */
+ public Collection<CachedContent> getAll() {
+ return keyToContent.values();
+ }
+
+ /** Returns an existing or new id assigned to the given key. */
+ public int assignIdForKey(String key) {
+ return getOrAdd(key).id;
+ }
+
+ /** Returns the key which has the given id assigned. */
+ public String getKeyForId(int id) {
+ return idToKey.get(id);
+ }
+
+ /** 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 and not locked {@link CachedContent} instances from index. */
+ public void removeEmpty() {
+ String[] keys = new String[keyToContent.size()];
+ keyToContent.keySet().toArray(keys);
+ for (String key : keys) {
+ maybeRemove(key);
+ }
+ }
+
+ /**
+ * Returns a set of all content keys. The set is backed by the {@code keyToContent} map, so
+ * changes to the map are reflected in the set, and vice-versa. If the map is modified while an
+ * iteration over the set is in progress (except through the iterator's own remove operation), the
+ * results of the iteration are undefined.
+ */
+ public Set<String> getKeys() {
+ return keyToContent.keySet();
+ }
+
+ /**
+ * 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 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);
+ return cachedContent != null ? cachedContent.getMetadata() : DefaultContentMetadata.EMPTY;
+ }
+
+ private CachedContent addNew(String key) {
+ int id = getNewId(idToKey);
+ 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
+ /* package */ static int getNewId(SparseArray<String> 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.
+ // TODO optimization: defragmentation or binary search?
+ for (id = 0; id < size; id++) {
+ if (id != idToKey.keyAt(id)) {
+ break;
+ }
+ }
+ }
+ 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<String, byte[]> 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<Map.Entry<String, byte[]>> entrySet = metadata.entrySet();
+ output.writeInt(entrySet.size());
+ for (Map.Entry<String, byte[]> 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.
+ *
+ * <p>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<String, CachedContent> 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<String, CachedContent> 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<String, CachedContent> 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<String, CachedContent> content, SparseArray<@NullableType String> idToKey) {
+ Assertions.checkState(!changed);
+ if (!readFile(content, idToKey)) {
+ content.clear();
+ idToKey.clear();
+ atomicFile.delete();
+ }
+ }
+
+ @Override
+ public void storeFully(HashMap<String, CachedContent> content) throws IOException {
+ writeFile(content);
+ changed = false;
+ }
+
+ @Override
+ public void storeIncremental(HashMap<String, CachedContent> 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<String, CachedContent> 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<String, CachedContent> 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<CachedContent> 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<String, CachedContent> 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<String, CachedContent> 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<String, CachedContent> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java
new file mode 100644
index 0000000000..9b08301ab8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java
@@ -0,0 +1,204 @@
+/*
+ * 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.upstream.cache;
+
+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;
+import java.util.TreeSet;
+
+/**
+ * Utility class for efficiently tracking regions of data that are stored in a {@link Cache}
+ * for a given cache key.
+ */
+public final class CachedRegionTracker implements Cache.Listener {
+
+ private static final String TAG = "CachedRegionTracker";
+
+ public static final int NOT_CACHED = -1;
+ public static final int CACHED_TO_END = -2;
+
+ private final Cache cache;
+ private final String cacheKey;
+ private final ChunkIndex chunkIndex;
+
+ private final TreeSet<Region> regions;
+ private final Region lookupRegion;
+
+ public CachedRegionTracker(Cache cache, String cacheKey, ChunkIndex chunkIndex) {
+ this.cache = cache;
+ this.cacheKey = cacheKey;
+ this.chunkIndex = chunkIndex;
+ this.regions = new TreeSet<>();
+ this.lookupRegion = new Region(0, 0);
+
+ synchronized (this) {
+ NavigableSet<CacheSpan> cacheSpans = cache.addListener(cacheKey, this);
+ // 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<CacheSpan> spanIterator = cacheSpans.descendingIterator();
+ while (spanIterator.hasNext()) {
+ CacheSpan span = spanIterator.next();
+ mergeSpan(span);
+ }
+ }
+ }
+
+ public void release() {
+ cache.removeListener(cacheKey, this);
+ }
+
+ /**
+ * When provided with a byte offset, this method locates the cached region within which the
+ * offset falls, and returns the approximate end position in milliseconds of that region. If the
+ * byte offset does not fall within a cached region then {@link #NOT_CACHED} is returned.
+ * If the cached region extends to the end of the stream, {@link #CACHED_TO_END} is returned.
+ *
+ * @param byteOffset The byte offset in the underlying stream.
+ * @return The end position of the corresponding cache region, {@link #NOT_CACHED}, or
+ * {@link #CACHED_TO_END}.
+ */
+ public synchronized int getRegionEndTimeMs(long byteOffset) {
+ lookupRegion.startOffset = byteOffset;
+ Region floorRegion = regions.floor(lookupRegion);
+ if (floorRegion == null || byteOffset > floorRegion.endOffset
+ || floorRegion.endOffsetIndex == -1) {
+ return NOT_CACHED;
+ }
+ int index = floorRegion.endOffsetIndex;
+ if (index == chunkIndex.length - 1
+ && floorRegion.endOffset == (chunkIndex.offsets[index] + chunkIndex.sizes[index])) {
+ return CACHED_TO_END;
+ }
+ long segmentFractionUs = (chunkIndex.durationsUs[index]
+ * (floorRegion.endOffset - chunkIndex.offsets[index])) / chunkIndex.sizes[index];
+ return (int) ((chunkIndex.timesUs[index] + segmentFractionUs) / 1000);
+ }
+
+ @Override
+ public synchronized void onSpanAdded(Cache cache, CacheSpan span) {
+ mergeSpan(span);
+ }
+
+ @Override
+ public synchronized void onSpanRemoved(Cache cache, CacheSpan span) {
+ Region removedRegion = new Region(span.position, span.position + span.length);
+
+ // Look up a region this span falls into.
+ Region floorRegion = regions.floor(removedRegion);
+ if (floorRegion == null) {
+ Log.e(TAG, "Removed a span we were not aware of");
+ return;
+ }
+
+ // Remove it.
+ regions.remove(floorRegion);
+
+ // Add new floor and ceiling regions, if necessary.
+ if (floorRegion.startOffset < removedRegion.startOffset) {
+ Region newFloorRegion = new Region(floorRegion.startOffset, removedRegion.startOffset);
+
+ int index = Arrays.binarySearch(chunkIndex.offsets, newFloorRegion.endOffset);
+ newFloorRegion.endOffsetIndex = index < 0 ? -index - 2 : index;
+ regions.add(newFloorRegion);
+ }
+
+ if (floorRegion.endOffset > removedRegion.endOffset) {
+ Region newCeilingRegion = new Region(removedRegion.endOffset + 1, floorRegion.endOffset);
+ newCeilingRegion.endOffsetIndex = floorRegion.endOffsetIndex;
+ regions.add(newCeilingRegion);
+ }
+ }
+
+ @Override
+ public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) {
+ // Do nothing.
+ }
+
+ private void mergeSpan(CacheSpan span) {
+ Region newRegion = new Region(span.position, span.position + span.length);
+ Region floorRegion = regions.floor(newRegion);
+ Region ceilingRegion = regions.ceiling(newRegion);
+ boolean floorConnects = regionsConnect(floorRegion, newRegion);
+ boolean ceilingConnects = regionsConnect(newRegion, ceilingRegion);
+
+ if (ceilingConnects) {
+ if (floorConnects) {
+ // Extend floorRegion to cover both newRegion and ceilingRegion.
+ floorRegion.endOffset = ceilingRegion.endOffset;
+ floorRegion.endOffsetIndex = ceilingRegion.endOffsetIndex;
+ } else {
+ // Extend newRegion to cover ceilingRegion. Add it.
+ newRegion.endOffset = ceilingRegion.endOffset;
+ newRegion.endOffsetIndex = ceilingRegion.endOffsetIndex;
+ regions.add(newRegion);
+ }
+ regions.remove(ceilingRegion);
+ } else if (floorConnects) {
+ // Extend floorRegion to the right to cover newRegion.
+ floorRegion.endOffset = newRegion.endOffset;
+ int index = floorRegion.endOffsetIndex;
+ while (index < chunkIndex.length - 1
+ && (chunkIndex.offsets[index + 1] <= floorRegion.endOffset)) {
+ index++;
+ }
+ floorRegion.endOffsetIndex = index;
+ } else {
+ // This is a new region.
+ int index = Arrays.binarySearch(chunkIndex.offsets, newRegion.endOffset);
+ newRegion.endOffsetIndex = index < 0 ? -index - 2 : index;
+ regions.add(newRegion);
+ }
+ }
+
+ private boolean regionsConnect(Region lower, Region upper) {
+ return lower != null && upper != null && lower.endOffset == upper.startOffset;
+ }
+
+ private static class Region implements Comparable<Region> {
+
+ /**
+ * The first byte of the region (inclusive).
+ */
+ public long startOffset;
+ /**
+ * End offset of the region (exclusive).
+ */
+ public long endOffset;
+ /**
+ * The index in chunkIndex that contains the end offset. May be -1 if the end offset comes
+ * before the start of the first media chunk (i.e. if the end offset is within the stream
+ * header).
+ */
+ public int endOffsetIndex;
+
+ public Region(long position, long endOffset) {
+ this.startOffset = position;
+ this.endOffset = endOffset;
+ }
+
+ @Override
+ public int compareTo(@NonNull Region another) {
+ return Util.compareLong(startOffset, another.startOffset);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadata.java
new file mode 100644
index 0000000000..aa34823043
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/ContentMetadataMutations.java
new file mode 100644
index 0000000000..c7a8d9f711
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<String, Object> editedValues;
+ private final List<String> 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<String> getRemovedValues() {
+ return Collections.unmodifiableList(new ArrayList<>(removedValues));
+ }
+
+ /** Returns a map of metadata name, value pairs to be set. Values are copied. */
+ public Map<String, Object> getEditedValues() {
+ HashMap<String, Object> hashMap = new HashMap<>(editedValues);
+ for (Entry<String, Object> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java
new file mode 100644
index 0000000000..2602f834e7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<String, byte[]> metadata;
+
+ public DefaultContentMetadata() {
+ this(Collections.emptyMap());
+ }
+
+ /** @param metadata The metadata entries in their raw byte array form. */
+ public DefaultContentMetadata(Map<String, byte[]> 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<String, byte[]> 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<Entry<String, byte[]>> 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<String, byte[]> entry : metadata.entrySet()) {
+ result += entry.getKey().hashCode() ^ Arrays.hashCode(entry.getValue());
+ }
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ private static boolean isMetadataEqual(Map<String, byte[]> first, Map<String, byte[]> second) {
+ if (first.size() != second.size()) {
+ return false;
+ }
+ for (Entry<String, byte[]> 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<String, byte[]> applyMutations(
+ Map<String, byte[]> otherMetadata, ContentMetadataMutations mutations) {
+ HashMap<String, byte[]> metadata = new HashMap<>(otherMetadata);
+ removeValues(metadata, mutations.getRemovedValues());
+ addValues(metadata, mutations.getEditedValues());
+ return metadata;
+ }
+
+ private static void removeValues(HashMap<String, byte[]> metadata, List<String> names) {
+ for (int i = 0; i < names.size(); i++) {
+ metadata.remove(names.get(i));
+ }
+ }
+
+ private static void addValues(HashMap<String, byte[]> metadata, Map<String, Object> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java
new file mode 100644
index 0000000000..56eff06b25
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java
@@ -0,0 +1,89 @@
+/*
+ * 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.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.TreeSet;
+
+/** Evicts least recently used cache files first. */
+public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor {
+
+ private final long maxBytes;
+ private final TreeSet<CacheSpan> leastRecentlyUsed;
+
+ private long currentSize;
+
+ public LeastRecentlyUsedCacheEvictor(long maxBytes) {
+ this.maxBytes = maxBytes;
+ this.leastRecentlyUsed = new TreeSet<>(LeastRecentlyUsedCacheEvictor::compare);
+ }
+
+ @Override
+ public boolean requiresCacheSpanTouches() {
+ return true;
+ }
+
+ @Override
+ public void onCacheInitialized() {
+ // Do nothing.
+ }
+
+ @Override
+ public void onStartFile(Cache cache, String key, long position, long length) {
+ if (length != C.LENGTH_UNSET) {
+ evictCache(cache, length);
+ }
+ }
+
+ @Override
+ public void onSpanAdded(Cache cache, CacheSpan span) {
+ leastRecentlyUsed.add(span);
+ currentSize += span.length;
+ evictCache(cache, 0);
+ }
+
+ @Override
+ public void onSpanRemoved(Cache cache, CacheSpan span) {
+ leastRecentlyUsed.remove(span);
+ currentSize -= span.length;
+ }
+
+ @Override
+ public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) {
+ onSpanRemoved(cache, oldSpan);
+ onSpanAdded(cache, newSpan);
+ }
+
+ private void evictCache(Cache cache, long requiredSpace) {
+ while (currentSize + requiredSpace > maxBytes && !leastRecentlyUsed.isEmpty()) {
+ try {
+ cache.removeSpan(leastRecentlyUsed.first());
+ } catch (CacheException e) {
+ // do nothing.
+ }
+ }
+ }
+
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java
new file mode 100644
index 0000000000..75c1ad0a09
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.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.upstream.cache;
+
+
+/**
+ * Evictor that doesn't ever evict cache files.
+ *
+ * Warning: Using this evictor might have unforeseeable consequences if cache
+ * size is not managed elsewhere.
+ */
+public final class NoOpCacheEvictor implements CacheEvictor {
+
+ @Override
+ public boolean requiresCacheSpanTouches() {
+ return false;
+ }
+
+ @Override
+ public void onCacheInitialized() {
+ // Do nothing.
+ }
+
+ @Override
+ public void onStartFile(Cache cache, String key, long position, long maxLength) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSpanAdded(Cache cache, CacheSpan span) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSpanRemoved(Cache cache, CacheSpan span) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) {
+ // Do nothing.
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
new file mode 100644
index 0000000000..9e36c48d88
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
@@ -0,0 +1,812 @@
+/*
+ * 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.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.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.
+ *
+ * <p>Only one instance of SimpleCache is allowed for a given directory at a given time.
+ *
+ * <p>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<File> lockedCacheDirs = new HashSet<>();
+
+ private final File cacheDir;
+ private final CacheEvictor evictor;
+ private final CachedContentIndex contentIndex;
+ @Nullable private final CacheFileMetadataIndex fileIndex;
+ private final HashMap<String, ArrayList<Listener>> listeners;
+ private final Random random;
+ private final boolean touchCacheSpans;
+
+ private long uid;
+ private long totalSpace;
+ private boolean released;
+ private @MonotonicNonNull CacheException initializationException;
+
+ /**
+ * Returns whether {@code cacheFolder} is locked by a {@link SimpleCache} instance. To unlock the
+ * folder the {@link SimpleCache} instance should be released.
+ */
+ public static synchronized boolean isCacheFolderLocked(File cacheFolder) {
+ return lockedCacheDirs.contains(cacheFolder.getAbsoluteFile());
+ }
+
+ /**
+ * Deletes all content belonging to a cache instance.
+ *
+ * <p>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);
+ }
+
+ /**
+ * 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}.
+ * @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.
+ */
+ @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.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()") {
+ @Override
+ public void run() {
+ synchronized (SimpleCache.this) {
+ conditionVariable.open();
+ initialize();
+ SimpleCache.this.evictor.onCacheInitialized();
+ }
+ }
+ }.start();
+ 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<CacheSpan> addListener(String key, Listener listener) {
+ Assertions.checkState(!released);
+ ArrayList<Listener> listenersForKey = listeners.get(key);
+ if (listenersForKey == null) {
+ listenersForKey = new ArrayList<>();
+ listeners.put(key, listenersForKey);
+ }
+ listenersForKey.add(listener);
+ return getCachedSpans(key);
+ }
+
+ @Override
+ public synchronized void removeListener(String key, Listener listener) {
+ if (released) {
+ return;
+ }
+ ArrayList<Listener> listenersForKey = listeners.get(key);
+ if (listenersForKey != null) {
+ listenersForKey.remove(listener);
+ if (listenersForKey.isEmpty()) {
+ listeners.remove(key);
+ }
+ }
+ }
+
+ @NonNull
+ @Override
+ public synchronized NavigableSet<CacheSpan> getCachedSpans(String key) {
+ Assertions.checkState(!released);
+ CachedContent cachedContent = contentIndex.get(key);
+ return cachedContent == null || cachedContent.isEmpty()
+ ? new TreeSet<>()
+ : new TreeSet<CacheSpan>(cachedContent.getSpans());
+ }
+
+ @Override
+ public synchronized Set<String> getKeys() {
+ Assertions.checkState(!released);
+ return new HashSet<>(contentIndex.getKeys());
+ }
+
+ @Override
+ public synchronized long getCacheSpace() {
+ Assertions.checkState(!released);
+ return totalSpace;
+ }
+
+ @Override
+ public synchronized CacheSpan startReadWrite(String key, long position)
+ throws InterruptedException, CacheException {
+ Assertions.checkState(!released);
+ checkInitialization();
+
+ while (true) {
+ CacheSpan span = startReadWriteNonBlocking(key, position);
+ if (span != null) {
+ return span;
+ } else {
+ // 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
+ @Nullable
+ public synchronized CacheSpan startReadWriteNonBlocking(String key, long position)
+ throws CacheException {
+ Assertions.checkState(!released);
+ checkInitialization();
+
+ SimpleCacheSpan span = getSpan(key, position);
+
+ if (span.isCached) {
+ // Read case.
+ return touchSpan(key, span);
+ }
+
+ CachedContent cachedContent = contentIndex.getOrAdd(key);
+ if (!cachedContent.isLocked()) {
+ // Write case.
+ cachedContent.setLocked(true);
+ return span;
+ }
+
+ // Lock not available.
+ return null;
+ }
+
+ @Override
+ 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.
+ cacheDir.mkdirs();
+ removeStaleSpans();
+ }
+ 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, long length) throws CacheException {
+ Assertions.checkState(!released);
+ if (!file.exists()) {
+ return;
+ }
+ 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 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);
+ try {
+ contentIndex.store();
+ } catch (IOException e) {
+ throw new CacheException(e);
+ }
+ notifyAll();
+ }
+
+ @Override
+ public synchronized void releaseHoleSpan(CacheSpan holeSpan) {
+ 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<String, CacheFileMetadata> 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);
+ }
+ }
+
+ /**
+ * 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<String, CacheFileMetadata> 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.
+ *
+ * <p>If the lookup position is contained by an existing entry in the cache, then the returned
+ * 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) {
+ 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.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;
+ }
+ }
+
+ /**
+ * Adds a cached span to the in-memory representation.
+ *
+ * @param span The span to be added.
+ */
+ private void addSpan(SimpleCacheSpan span) {
+ contentIndex.getOrAdd(span.key).addSpan(span);
+ totalSpace += span.length;
+ notifySpanAdded(span);
+ }
+
+ 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);
+ }
+
+ /**
+ * Scans all of the cached spans in the in-memory representation, removing any for which the
+ * underlying file lengths no longer match.
+ */
+ private void removeStaleSpans() {
+ ArrayList<CacheSpan> spansToBeRemoved = new ArrayList<>();
+ for (CachedContent cachedContent : contentIndex.getAll()) {
+ for (CacheSpan span : cachedContent.getSpans()) {
+ if (span.file.length() != span.length) {
+ spansToBeRemoved.add(span);
+ }
+ }
+ }
+ for (int i = 0; i < spansToBeRemoved.size(); i++) {
+ removeSpanInternal(spansToBeRemoved.get(i));
+ }
+ }
+
+ private void notifySpanRemoved(CacheSpan span) {
+ ArrayList<Listener> keyListeners = listeners.get(span.key);
+ if (keyListeners != null) {
+ for (int i = keyListeners.size() - 1; i >= 0; i--) {
+ keyListeners.get(i).onSpanRemoved(this, span);
+ }
+ }
+ evictor.onSpanRemoved(this, span);
+ }
+
+ private void notifySpanAdded(SimpleCacheSpan span) {
+ ArrayList<Listener> keyListeners = listeners.get(span.key);
+ if (keyListeners != null) {
+ for (int i = keyListeners.size() - 1; i >= 0; i--) {
+ keyListeners.get(i).onSpanAdded(this, span);
+ }
+ }
+ evictor.onSpanAdded(this, span);
+ }
+
+ private void notifySpanTouched(SimpleCacheSpan oldSpan, CacheSpan newSpan) {
+ ArrayList<Listener> keyListeners = listeners.get(oldSpan.key);
+ if (keyListeners != null) {
+ for (int i = keyListeners.size() - 1; i >= 0; i--) {
+ keyListeners.get(i).onSpanTouched(this, oldSpan, newSpan);
+ }
+ }
+ evictor.onSpanTouched(this, oldSpan, newSpan);
+ }
+
+ /**
+ * 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;
+ }
+
+ @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;
+ }
+
+ private static long parseUid(String fileName) {
+ return Long.parseLong(fileName.substring(0, fileName.indexOf('.')), /* radix= */ 16);
+ }
+
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java
new file mode 100644
index 0000000000..6e7bec301f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java
@@ -0,0 +1,217 @@
+/*
+ * 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.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;
+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 {
+
+ /* 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(
+ "^(.+)\\.(\\d+)\\.(\\d+)\\.v2\\.exo$", Pattern.DOTALL);
+ private static final Pattern CACHE_FILE_PATTERN_V3 = Pattern.compile(
+ "^(\\d+)\\.(\\d+)\\.(\\d+)\\.v3\\.exo$", Pattern.DOTALL);
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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.
+ * @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, 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)) {
+ @Nullable File upgradedFile = upgradeFile(file, index);
+ if (upgradedFile == null) {
+ return null;
+ }
+ file = upgradedFile;
+ name = file.getName();
+ }
+
+ Matcher matcher = CACHE_FILE_PATTERN_V3.matcher(name);
+ if (!matcher.matches()) {
+ return null;
+ }
+
+ int id = Integer.parseInt(matcher.group(1));
+ String key = index.getKeyForId(id);
+ 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();
+ Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(filename);
+ if (matcher.matches()) {
+ key = Util.unescapeFileName(matcher.group(1));
+ if (key == null) {
+ return null;
+ }
+ } else {
+ matcher = CACHE_FILE_PATTERN_V1.matcher(filename);
+ if (!matcher.matches()) {
+ return null;
+ }
+ key = matcher.group(1); // Keys were not escaped in version 1.
+ }
+
+ 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;
+ }
+
+ /**
+ * @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 with a new file and last touch timestamp.
+ *
+ * @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 copyWithFileAndLastTouchTimestamp(File file, long lastTouchTimestamp) {
+ Assertions.checkState(isCached);
+ return new SimpleCacheSpan(key, position, length, lastTouchTimestamp, file);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java
new file mode 100644
index 0000000000..4c6be98157
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java
@@ -0,0 +1,99 @@
+/*
+ * 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.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;
+import javax.crypto.Cipher;
+
+/**
+ * A wrapping {@link DataSink} that encrypts the data being consumed.
+ */
+public final class AesCipherDataSink implements DataSink {
+
+ private final DataSink wrappedDataSink;
+ private final byte[] secretKey;
+ @Nullable private final byte[] scratch;
+
+ @Nullable private AesFlushingCipher cipher;
+
+ /**
+ * Create an instance whose {@code write} methods have the side effect of overwriting the input
+ * {@code data}. Use this constructor for maximum efficiency in the case that there is no
+ * requirement for the input data arrays to remain unchanged.
+ *
+ * @param secretKey The key data.
+ * @param wrappedDataSink The wrapped {@link DataSink}.
+ */
+ public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink) {
+ this(secretKey, wrappedDataSink, null);
+ }
+
+ /**
+ * Create an instance whose {@code write} methods are free of side effects. Use this constructor
+ * when the input data arrays are required to remain unchanged.
+ *
+ * @param secretKey The key data.
+ * @param wrappedDataSink The wrapped {@link DataSink}.
+ * @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. If {@code null} then encryption
+ * will overwrite the input {@code data}.
+ */
+ public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, @Nullable byte[] scratch) {
+ this.wrappedDataSink = wrappedDataSink;
+ this.secretKey = secretKey;
+ this.scratch = scratch;
+ }
+
+ @Override
+ public void open(DataSpec dataSpec) throws IOException {
+ wrappedDataSink.open(dataSpec);
+ long nonce = CryptoUtil.getFNV64Hash(dataSpec.key);
+ cipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, secretKey, nonce,
+ dataSpec.absoluteStreamPosition);
+ }
+
+ @Override
+ public void write(byte[] data, int offset, int length) throws IOException {
+ if (scratch == null) {
+ // In-place mode. Writes over the input data.
+ 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);
+ castNonNull(cipher)
+ .update(data, offset + bytesProcessed, bytesToProcess, scratch, /* outOffset= */ 0);
+ wrappedDataSink.write(scratch, /* offset= */ 0, bytesToProcess);
+ bytesProcessed += bytesToProcess;
+ }
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ cipher = null;
+ wrappedDataSink.close();
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java
new file mode 100644
index 0000000000..0b0687b57e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java
@@ -0,0 +1,89 @@
+/*
+ * 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.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;
+
+/**
+ * A {@link DataSource} that decrypts the data read from an upstream source.
+ */
+public final class AesCipherDataSource implements DataSource {
+
+ private final DataSource upstream;
+ private final byte[] secretKey;
+
+ @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);
+ long nonce = CryptoUtil.getFNV64Hash(dataSpec.key);
+ cipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, secretKey, nonce,
+ dataSpec.absoluteStreamPosition);
+ return dataLength;
+ }
+
+ @Override
+ public int read(byte[] data, int offset, int readLength) throws IOException {
+ if (readLength == 0) {
+ return 0;
+ }
+ int read = upstream.read(data, offset, readLength);
+ if (read == C.RESULT_END_OF_INPUT) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ castNonNull(cipher).updateInPlace(data, offset, read);
+ return read;
+ }
+
+ @Override
+ @Nullable
+ public Uri getUri() {
+ return upstream.getUri();
+ }
+
+ @Override
+ public Map<String, List<String>> getResponseHeaders() {
+ return upstream.getResponseHeaders();
+ }
+
+ @Override
+ public void close() throws IOException {
+ cipher = null;
+ upstream.close();
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java
new file mode 100644
index 0000000000..985a6dcf24
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java
@@ -0,0 +1,123 @@
+/*
+ * 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.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;
+import java.security.NoSuchAlgorithmException;
+import javax.crypto.Cipher;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * A flushing variant of a AES/CTR/NoPadding {@link Cipher}.
+ *
+ * Unlike a regular {@link Cipher}, the update methods of this class are guaranteed to process all
+ * of the bytes input (and hence output the same number of bytes).
+ */
+public final class AesFlushingCipher {
+
+ private final Cipher cipher;
+ private final int blockSize;
+ private final byte[] zerosBlock;
+ private final byte[] flushedBlock;
+
+ private int pendingXorBytes;
+
+ public AesFlushingCipher(int mode, byte[] secretKey, long nonce, long offset) {
+ try {
+ cipher = Cipher.getInstance("AES/CTR/NoPadding");
+ blockSize = cipher.getBlockSize();
+ zerosBlock = new byte[blockSize];
+ flushedBlock = new byte[blockSize];
+ long counter = offset / blockSize;
+ int startPadding = (int) (offset % blockSize);
+ 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);
+ }
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
+ | InvalidAlgorithmParameterException e) {
+ // Should never happen.
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void updateInPlace(byte[] data, int offset, int length) {
+ update(data, offset, length, data, offset);
+ }
+
+ public void update(byte[] in, int inOffset, int length, byte[] out, int outOffset) {
+ // If we previously flushed the cipher by inputting zeros up to a block boundary, then we need
+ // to manually transform the data that actually ended the block. See the comment below for more
+ // details.
+ while (pendingXorBytes > 0) {
+ out[outOffset] = (byte) (in[inOffset] ^ flushedBlock[blockSize - pendingXorBytes]);
+ outOffset++;
+ inOffset++;
+ pendingXorBytes--;
+ length--;
+ if (length == 0) {
+ return;
+ }
+ }
+
+ // Do the bulk of the update.
+ int written = nonFlushingUpdate(in, inOffset, length, out, outOffset);
+ if (length == written) {
+ return;
+ }
+
+ // We need to finish the block to flush out the remaining bytes. We do so by inputting zeros,
+ // so that the corresponding bytes output by the cipher are those that would have been XORed
+ // against the real end-of-block data to transform it. We store these bytes so that we can
+ // perform the transformation manually in the case of a subsequent call to this method with
+ // the real data.
+ int bytesToFlush = length - written;
+ Assertions.checkState(bytesToFlush < blockSize);
+ outOffset += written;
+ pendingXorBytes = blockSize - bytesToFlush;
+ written = nonFlushingUpdate(zerosBlock, 0, pendingXorBytes, flushedBlock, 0);
+ Assertions.checkState(written == blockSize);
+ // The first part of xorBytes contains the flushed data, which we copy out. The remainder
+ // contains the bytes that will be needed for manual transformation in a subsequent call.
+ for (int i = 0; i < bytesToFlush; i++) {
+ out[outOffset++] = flushedBlock[i];
+ }
+ }
+
+ private int nonFlushingUpdate(byte[] in, int inOffset, int length, byte[] out, int outOffset) {
+ try {
+ return cipher.update(in, inOffset, length, out, outOffset);
+ } catch (ShortBufferException e) {
+ // Should never happen.
+ throw new RuntimeException(e);
+ }
+ }
+
+ private byte[] getInitializationVector(long nonce, long counter) {
+ return ByteBuffer.allocate(16).putLong(nonce).putLong(counter).array();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java
new file mode 100644
index 0000000000..a4904b9285
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java
@@ -0,0 +1,46 @@
+/*
+ * 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.upstream.crypto;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Utility functions for the crypto package.
+ */
+/* package */ final class CryptoUtil {
+
+ private CryptoUtil() {}
+
+ /**
+ * 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()}.
+ */
+ public static long getFNV64Hash(@Nullable String input) {
+ if (input == null) {
+ return 0;
+ }
+
+ long hash = 0;
+ for (int i = 0; i < input.length(); i++) {
+ hash ^= input.charAt(i);
+ // This is equivalent to hash *= 0x100000001b3 (the FNV magic prime number).
+ hash += (hash << 1) + (hash << 4) + (hash << 5) + (hash << 7) + (hash << 8) + (hash << 40);
+ }
+ return hash;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Assertions.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Assertions.java
new file mode 100644
index 0000000000..361b895695
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Assertions.java
@@ -0,0 +1,217 @@
+/*
+ * 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.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.
+ */
+public final class Assertions {
+
+ private Assertions() {}
+
+ /**
+ * Throws {@link IllegalArgumentException} if {@code expression} evaluates to false.
+ *
+ * @param expression The expression to evaluate.
+ * @throws IllegalArgumentException If {@code expression} is false.
+ */
+ public static void checkArgument(boolean expression) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Throws {@link IllegalArgumentException} if {@code expression} evaluates to false.
+ *
+ * @param expression The expression to evaluate.
+ * @param errorMessage The exception message if an exception is thrown. The message is converted
+ * to a {@link String} using {@link String#valueOf(Object)}.
+ * @throws IllegalArgumentException If {@code expression} is false.
+ */
+ public static void checkArgument(boolean expression, Object errorMessage) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {
+ throw new IllegalArgumentException(String.valueOf(errorMessage));
+ }
+ }
+
+ /**
+ * Throws {@link IndexOutOfBoundsException} if {@code index} falls outside the specified bounds.
+ *
+ * @param index The index to test.
+ * @param start The start of the allowed range (inclusive).
+ * @param limit The end of the allowed range (exclusive).
+ * @return The {@code index} that was validated.
+ * @throws IndexOutOfBoundsException If {@code index} falls outside the specified bounds.
+ */
+ public static int checkIndex(int index, int start, int limit) {
+ if (index < start || index >= limit) {
+ throw new IndexOutOfBoundsException();
+ }
+ return index;
+ }
+
+ /**
+ * Throws {@link IllegalStateException} if {@code expression} evaluates to false.
+ *
+ * @param expression The expression to evaluate.
+ * @throws IllegalStateException If {@code expression} is false.
+ */
+ public static void checkState(boolean expression) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Throws {@link IllegalStateException} if {@code expression} evaluates to false.
+ *
+ * @param expression The expression to evaluate.
+ * @param errorMessage The exception message if an exception is thrown. The message is converted
+ * to a {@link String} using {@link String#valueOf(Object)}.
+ * @throws IllegalStateException If {@code expression} is false.
+ */
+ public static void checkState(boolean expression, Object errorMessage) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {
+ throw new IllegalStateException(String.valueOf(errorMessage));
+ }
+ }
+
+ /**
+ * Throws {@link IllegalStateException} if {@code reference} is null.
+ *
+ * @param <T> 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> 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 <T> 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> 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.
+ *
+ * @param <T> The type of the reference.
+ * @param reference The reference.
+ * @return The non-null reference that was validated.
+ * @throws NullPointerException If {@code reference} is null.
+ */
+ @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"})
+ @EnsuresNonNull({"#1"})
+ public static <T> T checkNotNull(@Nullable T reference) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) {
+ throw new NullPointerException();
+ }
+ return reference;
+ }
+
+ /**
+ * Throws {@link NullPointerException} if {@code reference} is null.
+ *
+ * @param <T> 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 NullPointerException If {@code reference} is null.
+ */
+ @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"})
+ @EnsuresNonNull({"#1"})
+ public static <T> T checkNotNull(@Nullable T reference, Object errorMessage) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) {
+ throw new NullPointerException(String.valueOf(errorMessage));
+ }
+ return reference;
+ }
+
+ /**
+ * Throws {@link IllegalArgumentException} if {@code string} is null or zero length.
+ *
+ * @param string The string to check.
+ * @return The non-null, non-empty string that was validated.
+ * @throws IllegalArgumentException If {@code string} is null or 0-length.
+ */
+ @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();
+ }
+ return string;
+ }
+
+ /**
+ * Throws {@link IllegalArgumentException} if {@code string} is null or zero length.
+ *
+ * @param string The string to check.
+ * @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, non-empty string that was validated.
+ * @throws IllegalArgumentException If {@code string} is null or 0-length.
+ */
+ @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));
+ }
+ return string;
+ }
+
+ /**
+ * Throws {@link IllegalStateException} if the calling thread is not the application's main
+ * thread.
+ *
+ * @throws IllegalStateException If the calling thread is not the application's main thread.
+ */
+ public static void checkMainThread() {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && Looper.myLooper() != Looper.getMainLooper()) {
+ throw new IllegalStateException("Not in applications main thread");
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/AtomicFile.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/AtomicFile.java
new file mode 100644
index 0000000000..d868a7d22a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/AtomicFile.java
@@ -0,0 +1,201 @@
+/*
+ * 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 java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A helper class for performing atomic operations on a file by creating a backup file until a write
+ * has successfully completed.
+ *
+ * <p>Atomic file guarantees file integrity by ensuring that a file has been completely written and
+ * 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).
+ *
+ * <p>Atomic file does not confer any file locking semantics. Do not use this class when the file
+ * may be accessed or modified concurrently by multiple threads or processes. The caller is
+ * responsible for ensuring appropriate mutual exclusion invariants whenever it accesses the file.
+ */
+public final class AtomicFile {
+
+ private static final String TAG = "AtomicFile";
+
+ private final File baseName;
+ private final File backupName;
+
+ /**
+ * Create a new AtomicFile for a file located at the given File path. The secondary backup file
+ * will be the same file path with ".bak" appended.
+ */
+ public AtomicFile(File baseName) {
+ this.baseName = baseName;
+ 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();
+ backupName.delete();
+ }
+
+ /**
+ * Start a new write operation on the file. This returns an {@link OutputStream} to which you can
+ * write the new file data. If the whole data is written successfully you <em>must</em> call
+ * {@link #endWrite(OutputStream)}. On failure you should call {@link OutputStream#close()}
+ * only to free up resources used by it.
+ *
+ * <p>Example usage:
+ *
+ * <pre>
+ * DataOutputStream dataOutput = null;
+ * try {
+ * OutputStream outputStream = atomicFile.startWrite();
+ * dataOutput = new DataOutputStream(outputStream); // Wrapper stream
+ * dataOutput.write(data1);
+ * dataOutput.write(data2);
+ * atomicFile.endWrite(dataOutput); // Pass wrapper stream
+ * } finally{
+ * if (dataOutput != null) {
+ * dataOutput.close();
+ * }
+ * }
+ * </pre>
+ *
+ * <p>Note that if another thread is currently performing a write, this will simply replace
+ * whatever that thread is writing with the new file being written by this thread, and when the
+ * other thread finishes the write the new write operation will no longer be safe (or will be
+ * lost). You must do your own threading protection for access to AtomicFile.
+ */
+ public OutputStream startWrite() throws IOException {
+ // Rename the current file so it may be used as a backup during the next read
+ if (baseName.exists()) {
+ if (!backupName.exists()) {
+ if (!baseName.renameTo(backupName)) {
+ Log.w(TAG, "Couldn't rename file " + baseName + " to backup file " + backupName);
+ }
+ } else {
+ baseName.delete();
+ }
+ }
+ OutputStream str;
+ try {
+ str = new AtomicFileOutputStream(baseName);
+ } catch (FileNotFoundException e) {
+ File parent = baseName.getParentFile();
+ 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, e2);
+ }
+ }
+ return str;
+ }
+
+ /**
+ * Call when you have successfully finished writing to the stream returned by {@link
+ * #startWrite()}. This will close, sync, and commit the new data. The next attempt to read the
+ * atomic file will return the new file stream.
+ *
+ * @param str Outer-most wrapper OutputStream used to write to the stream returned by {@link
+ * #startWrite()}.
+ * @see #startWrite()
+ */
+ public void endWrite(OutputStream str) throws IOException {
+ str.close();
+ // If close() throws exception, the next line is skipped.
+ backupName.delete();
+ }
+
+ /**
+ * Open the atomic file for reading. If there previously was an incomplete write, this will roll
+ * back to the last good data before opening for read.
+ *
+ * <p>Note that if another thread is currently performing a write, this will incorrectly consider
+ * it to be in the state of a bad write and roll back, causing the new data currently being
+ * written to be dropped. You must do your own threading protection for access to AtomicFile.
+ */
+ public InputStream openRead() throws FileNotFoundException {
+ restoreBackup();
+ return new FileInputStream(baseName);
+ }
+
+ private void restoreBackup() {
+ if (backupName.exists()) {
+ baseName.delete();
+ backupName.renameTo(baseName);
+ }
+ }
+
+ private static final class AtomicFileOutputStream extends OutputStream {
+
+ private final FileOutputStream fileOutputStream;
+ private boolean closed = false;
+
+ public AtomicFileOutputStream(File file) throws FileNotFoundException {
+ fileOutputStream = new FileOutputStream(file);
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ flush();
+ try {
+ fileOutputStream.getFD().sync();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to sync file descriptor:", e);
+ }
+ fileOutputStream.close();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ fileOutputStream.flush();
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ fileOutputStream.write(b);
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ fileOutputStream.write(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ fileOutputStream.write(b, off, len);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Clock.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Clock.java
new file mode 100644
index 0000000000..4247e1db7b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Clock.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 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 androidx.annotation.Nullable;
+
+/**
+ * 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 {
+
+ /**
+ * 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java
new file mode 100644
index 0000000000..9c821c47c8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/CodecSpecificDataUtil.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 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;
+
+/**
+ * Provides static utility methods for manipulating various types of codec specific data.
+ */
+public final class CodecSpecificDataUtil {
+
+ private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
+
+ private static final int AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY = 0xF;
+
+ private static final int[] AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE = new int[] {
+ 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350
+ };
+
+ private static final int AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID = -1;
+ /**
+ * In the channel configurations below, <A> indicates a single channel element; (A, B) indicates a
+ * channel pair element; and [A] indicates a low-frequency effects element.
+ * The speaker mapping short forms used are:
+ * - FC: front center
+ * - BC: back center
+ * - FL/FR: front left/right
+ * - FCL/FCR: front center left/right
+ * - FTL/FTR: front top left/right
+ * - SL/SR: back surround left/right
+ * - BL/BR: back left/right
+ * - LFE: low frequency effects
+ */
+ private static final int[] AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE =
+ new int[] {
+ 0,
+ 1, /* mono: <FC> */
+ 2, /* stereo: (FL, FR) */
+ 3, /* 3.0: <FC>, (FL, FR) */
+ 4, /* 4.0: <FC>, (FL, FR), <BC> */
+ 5, /* 5.0 back: <FC>, (FL, FR), (SL, SR) */
+ 6, /* 5.1 back: <FC>, (FL, FR), (SL, SR), <BC>, [LFE] */
+ 8, /* 7.1 wide back: <FC>, (FCL, FCR), (FL, FR), (SL, SR), [LFE] */
+ AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID,
+ AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID,
+ AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID,
+ 7, /* 6.1: <FC>, (FL, FR), (SL, SR), <RC>, [LFE] */
+ 8, /* 7.1: <FC>, (FL, FR), (SL, SR), (BL, BR), [LFE] */
+ AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID,
+ 8, /* 7.1 top: <FC>, (FL, FR), (SL, SR), [LFE], (FTL, FTR) */
+ AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID
+ };
+
+ // Advanced Audio Coding Low-Complexity profile.
+ private static final int AUDIO_OBJECT_TYPE_AAC_LC = 2;
+ // Spectral Band Replication.
+ private static final int AUDIO_OBJECT_TYPE_SBR = 5;
+ // Error Resilient Bit-Sliced Arithmetic Coding.
+ private static final int AUDIO_OBJECT_TYPE_ER_BSAC = 22;
+ // Parametric Stereo.
+ private static final int AUDIO_OBJECT_TYPE_PS = 29;
+ // Escape code for extended audio object types.
+ private static final int AUDIO_OBJECT_TYPE_ESCAPE = 31;
+
+ private CodecSpecificDataUtil() {}
+
+ /**
+ * Parses an AAC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
+ *
+ * @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<Integer, Integer> 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<Integer, Integer> parseAacAudioSpecificConfig(
+ ParsableBitArray bitArray, boolean forceReadToEnd) throws ParserException {
+ int audioObjectType = getAacAudioObjectType(bitArray);
+ int sampleRate = getAacSamplingFrequency(bitArray);
+ int channelConfiguration = bitArray.readBits(4);
+ if (audioObjectType == AUDIO_OBJECT_TYPE_SBR || audioObjectType == AUDIO_OBJECT_TYPE_PS) {
+ // For an AAC bitstream using spectral band replication (SBR) or parametric stereo (PS) with
+ // explicit signaling, we return the extension sampling frequency as the sample rate of the
+ // content; this is identical to the sample rate of the decoded output but may differ from
+ // the sample rate set above.
+ // Use the extensionSamplingFrequencyIndex.
+ sampleRate = getAacSamplingFrequency(bitArray);
+ audioObjectType = getAacAudioObjectType(bitArray);
+ if (audioObjectType == AUDIO_OBJECT_TYPE_ER_BSAC) {
+ // Use the extensionChannelConfiguration.
+ 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);
+ }
+
+ /**
+ * 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 channelCount The channel count.
+ * @return The AudioSpecificConfig.
+ */
+ 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]) {
+ sampleRateIndex = i;
+ }
+ }
+ int channelConfig = C.INDEX_UNSET;
+ for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE.length; ++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 + ", " + channelCount);
+ }
+ return buildAacAudioSpecificConfig(AUDIO_OBJECT_TYPE_AAC_LC, sampleRateIndex, channelConfig);
+ }
+
+ /**
+ * Builds a simple AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
+ *
+ * @param audioObjectType The audio object type.
+ * @param sampleRateIndex The sample rate index.
+ * @param channelConfig The channel configuration.
+ * @return The AudioSpecificConfig.
+ */
+ public static byte[] buildAacAudioSpecificConfig(int audioObjectType, int sampleRateIndex,
+ int channelConfig) {
+ byte[] specificConfig = new byte[2];
+ specificConfig[0] = (byte) (((audioObjectType << 3) & 0xF8) | ((sampleRateIndex >> 1) & 0x07));
+ specificConfig[1] = (byte) (((sampleRateIndex << 7) & 0x80) | ((channelConfig << 3) & 0x78));
+ return specificConfig;
+ }
+
+ /**
+ * Parses an ALAC AudioSpecificConfig (i.e. an <a
+ * href="https://github.com/macosforge/alac/blob/master/ALACMagicCookieDescription.txt">ALACSpecificConfig</a>).
+ *
+ * @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<Integer, Integer> 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.
+ *
+ * @param data An array containing the data that should follow the NAL start code.
+ * @param offset The start offset into {@code data}.
+ * @param length The number of bytes to copy from {@code data}
+ * @return The constructed NAL unit.
+ */
+ public static byte[] buildNalUnit(byte[] data, int offset, int length) {
+ byte[] nalUnit = new byte[length + NAL_START_CODE.length];
+ System.arraycopy(NAL_START_CODE, 0, nalUnit, 0, NAL_START_CODE.length);
+ System.arraycopy(data, offset, nalUnit, NAL_START_CODE.length, length);
+ return nalUnit;
+ }
+
+ /**
+ * Splits an array of NAL units.
+ *
+ * <p>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.
+ *
+ * @param data An array of data.
+ * @return The individual NAL units, or null if the input did not consist of NAL start code
+ * delimited units.
+ */
+ public static @Nullable byte[][] splitNalUnits(byte[] data) {
+ if (!isNalStartCode(data, 0)) {
+ // data does not consist of NAL start code delimited units.
+ return null;
+ }
+ List<Integer> starts = new ArrayList<>();
+ int nalUnitIndex = 0;
+ do {
+ starts.add(nalUnitIndex);
+ nalUnitIndex = findNalStartCode(data, nalUnitIndex + NAL_START_CODE.length);
+ } while (nalUnitIndex != C.INDEX_UNSET);
+ byte[][] split = new byte[starts.size()][];
+ for (int i = 0; i < starts.size(); i++) {
+ int startIndex = starts.get(i);
+ int endIndex = i < starts.size() - 1 ? starts.get(i + 1) : data.length;
+ byte[] nal = new byte[endIndex - startIndex];
+ System.arraycopy(data, startIndex, nal, 0, nal.length);
+ split[i] = nal;
+ }
+ return split;
+ }
+
+ /**
+ * Finds the next occurrence of the NAL start code from a given index.
+ *
+ * @param data The data in which to search.
+ * @param index The first index to test.
+ * @return The index of the first byte of the found start code, or {@link C#INDEX_UNSET}.
+ */
+ private static int findNalStartCode(byte[] data, int index) {
+ int endIndex = data.length - NAL_START_CODE.length;
+ for (int i = index; i <= endIndex; i++) {
+ if (isNalStartCode(data, i)) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ /**
+ * Tests whether there exists a NAL start code at a given index.
+ *
+ * @param data The data.
+ * @param index The index to test.
+ * @return Whether there exists a start code that begins at {@code index}.
+ */
+ private static boolean isNalStartCode(byte[] data, int index) {
+ if (data.length - index <= NAL_START_CODE.length) {
+ return false;
+ }
+ for (int j = 0; j < NAL_START_CODE.length; j++) {
+ if (data[index + j] != NAL_START_CODE[j]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns the AAC audio object type as specified in 14496-3 (2005) Table 1.14.
+ *
+ * @param bitArray The bit array containing the audio specific configuration.
+ * @return The audio object type.
+ */
+ private static int getAacAudioObjectType(ParsableBitArray bitArray) {
+ int audioObjectType = bitArray.readBits(5);
+ if (audioObjectType == AUDIO_OBJECT_TYPE_ESCAPE) {
+ audioObjectType = 32 + bitArray.readBits(6);
+ }
+ return audioObjectType;
+ }
+
+ /**
+ * Returns the AAC sampling frequency (or extension sampling frequency) as specified in 14496-3
+ * (2005) Table 1.13.
+ *
+ * @param bitArray The bit array containing the audio specific configuration.
+ * @return The sampling frequency.
+ */
+ private static int getAacSamplingFrequency(ParsableBitArray bitArray) {
+ int samplingFrequency;
+ int frequencyIndex = bitArray.readBits(4);
+ if (frequencyIndex == AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY) {
+ samplingFrequency = bitArray.readBits(24);
+ } else {
+ Assertions.checkArgument(frequencyIndex < 13);
+ samplingFrequency = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex];
+ }
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ColorParser.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ColorParser.java
new file mode 100644
index 0000000000..31b81fe16f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ColorParser.java
@@ -0,0 +1,277 @@
+/*
+ * 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.text.TextUtils;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parser for color expressions found in styling formats, e.g. TTML and CSS.
+ *
+ * @see <a href="https://w3c.github.io/webvtt/#styling">WebVTT CSS Styling</a>
+ * @see <a href="https://www.w3.org/TR/ttml2/">Timed Text Markup Language 2 (TTML2) - 10.3.5</a>
+ */
+public final class ColorParser {
+
+ private static final String RGB = "rgb";
+ private static final String RGBA = "rgba";
+
+ private static final Pattern RGB_PATTERN = Pattern.compile(
+ "^rgb\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)$");
+
+ private static final Pattern RGBA_PATTERN_INT_ALPHA = Pattern.compile(
+ "^rgba\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)$");
+
+ private static final Pattern RGBA_PATTERN_FLOAT_ALPHA = Pattern.compile(
+ "^rgba\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3}),(\\d*\\.?\\d*?)\\)$");
+
+ private static final Map<String, Integer> COLOR_MAP;
+
+ /**
+ * Parses a TTML color expression.
+ *
+ * @param colorExpression The color expression.
+ * @return The parsed ARGB color.
+ */
+ public static int parseTtmlColor(String colorExpression) {
+ return parseColorInternal(colorExpression, false);
+ }
+
+ /**
+ * Parses a CSS color expression.
+ *
+ * @param colorExpression The color expression.
+ * @return The parsed ARGB color.
+ */
+ public static int parseCssColor(String colorExpression) {
+ return parseColorInternal(colorExpression, true);
+ }
+
+ private static int parseColorInternal(String colorExpression, boolean alphaHasFloatFormat) {
+ Assertions.checkArgument(!TextUtils.isEmpty(colorExpression));
+ colorExpression = colorExpression.replace(" ", "");
+ if (colorExpression.charAt(0) == '#') {
+ // Parse using Long to avoid failure when colorExpression is greater than #7FFFFFFF.
+ int color = (int) Long.parseLong(colorExpression.substring(1), 16);
+ if (colorExpression.length() == 7) {
+ // Set the alpha value
+ color |= 0xFF000000;
+ } else if (colorExpression.length() == 9) {
+ // We have #RRGGBBAA, but we need #AARRGGBB
+ color = ((color & 0xFF) << 24) | (color >>> 8);
+ } else {
+ throw new IllegalArgumentException();
+ }
+ return color;
+ } else if (colorExpression.startsWith(RGBA)) {
+ Matcher matcher = (alphaHasFloatFormat ? RGBA_PATTERN_FLOAT_ALPHA : RGBA_PATTERN_INT_ALPHA)
+ .matcher(colorExpression);
+ if (matcher.matches()) {
+ return argb(
+ alphaHasFloatFormat ? (int) (255 * Float.parseFloat(matcher.group(4)))
+ : Integer.parseInt(matcher.group(4), 10),
+ Integer.parseInt(matcher.group(1), 10),
+ Integer.parseInt(matcher.group(2), 10),
+ Integer.parseInt(matcher.group(3), 10)
+ );
+ }
+ } else if (colorExpression.startsWith(RGB)) {
+ Matcher matcher = RGB_PATTERN.matcher(colorExpression);
+ if (matcher.matches()) {
+ return rgb(
+ Integer.parseInt(matcher.group(1), 10),
+ Integer.parseInt(matcher.group(2), 10),
+ Integer.parseInt(matcher.group(3), 10)
+ );
+ }
+ } else {
+ // we use our own color map
+ Integer color = COLOR_MAP.get(Util.toLowerInvariant(colorExpression));
+ if (color != null) {
+ return color;
+ }
+ }
+ throw new IllegalArgumentException();
+ }
+
+ private static int argb(int alpha, int red, int green, int blue) {
+ return (alpha << 24) | (red << 16) | (green << 8) | blue;
+ }
+
+ private static int rgb(int red, int green, int blue) {
+ return argb(0xFF, red, green, blue);
+ }
+
+ static {
+ COLOR_MAP = new HashMap<>();
+ COLOR_MAP.put("aliceblue", 0xFFF0F8FF);
+ COLOR_MAP.put("antiquewhite", 0xFFFAEBD7);
+ COLOR_MAP.put("aqua", 0xFF00FFFF);
+ COLOR_MAP.put("aquamarine", 0xFF7FFFD4);
+ COLOR_MAP.put("azure", 0xFFF0FFFF);
+ COLOR_MAP.put("beige", 0xFFF5F5DC);
+ COLOR_MAP.put("bisque", 0xFFFFE4C4);
+ COLOR_MAP.put("black", 0xFF000000);
+ COLOR_MAP.put("blanchedalmond", 0xFFFFEBCD);
+ COLOR_MAP.put("blue", 0xFF0000FF);
+ COLOR_MAP.put("blueviolet", 0xFF8A2BE2);
+ COLOR_MAP.put("brown", 0xFFA52A2A);
+ COLOR_MAP.put("burlywood", 0xFFDEB887);
+ COLOR_MAP.put("cadetblue", 0xFF5F9EA0);
+ COLOR_MAP.put("chartreuse", 0xFF7FFF00);
+ COLOR_MAP.put("chocolate", 0xFFD2691E);
+ COLOR_MAP.put("coral", 0xFFFF7F50);
+ COLOR_MAP.put("cornflowerblue", 0xFF6495ED);
+ COLOR_MAP.put("cornsilk", 0xFFFFF8DC);
+ COLOR_MAP.put("crimson", 0xFFDC143C);
+ COLOR_MAP.put("cyan", 0xFF00FFFF);
+ COLOR_MAP.put("darkblue", 0xFF00008B);
+ COLOR_MAP.put("darkcyan", 0xFF008B8B);
+ COLOR_MAP.put("darkgoldenrod", 0xFFB8860B);
+ COLOR_MAP.put("darkgray", 0xFFA9A9A9);
+ COLOR_MAP.put("darkgreen", 0xFF006400);
+ COLOR_MAP.put("darkgrey", 0xFFA9A9A9);
+ COLOR_MAP.put("darkkhaki", 0xFFBDB76B);
+ COLOR_MAP.put("darkmagenta", 0xFF8B008B);
+ COLOR_MAP.put("darkolivegreen", 0xFF556B2F);
+ COLOR_MAP.put("darkorange", 0xFFFF8C00);
+ COLOR_MAP.put("darkorchid", 0xFF9932CC);
+ COLOR_MAP.put("darkred", 0xFF8B0000);
+ COLOR_MAP.put("darksalmon", 0xFFE9967A);
+ COLOR_MAP.put("darkseagreen", 0xFF8FBC8F);
+ COLOR_MAP.put("darkslateblue", 0xFF483D8B);
+ COLOR_MAP.put("darkslategray", 0xFF2F4F4F);
+ COLOR_MAP.put("darkslategrey", 0xFF2F4F4F);
+ COLOR_MAP.put("darkturquoise", 0xFF00CED1);
+ COLOR_MAP.put("darkviolet", 0xFF9400D3);
+ COLOR_MAP.put("deeppink", 0xFFFF1493);
+ COLOR_MAP.put("deepskyblue", 0xFF00BFFF);
+ COLOR_MAP.put("dimgray", 0xFF696969);
+ COLOR_MAP.put("dimgrey", 0xFF696969);
+ COLOR_MAP.put("dodgerblue", 0xFF1E90FF);
+ COLOR_MAP.put("firebrick", 0xFFB22222);
+ COLOR_MAP.put("floralwhite", 0xFFFFFAF0);
+ COLOR_MAP.put("forestgreen", 0xFF228B22);
+ COLOR_MAP.put("fuchsia", 0xFFFF00FF);
+ COLOR_MAP.put("gainsboro", 0xFFDCDCDC);
+ COLOR_MAP.put("ghostwhite", 0xFFF8F8FF);
+ COLOR_MAP.put("gold", 0xFFFFD700);
+ COLOR_MAP.put("goldenrod", 0xFFDAA520);
+ COLOR_MAP.put("gray", 0xFF808080);
+ COLOR_MAP.put("green", 0xFF008000);
+ COLOR_MAP.put("greenyellow", 0xFFADFF2F);
+ COLOR_MAP.put("grey", 0xFF808080);
+ COLOR_MAP.put("honeydew", 0xFFF0FFF0);
+ COLOR_MAP.put("hotpink", 0xFFFF69B4);
+ COLOR_MAP.put("indianred", 0xFFCD5C5C);
+ COLOR_MAP.put("indigo", 0xFF4B0082);
+ COLOR_MAP.put("ivory", 0xFFFFFFF0);
+ COLOR_MAP.put("khaki", 0xFFF0E68C);
+ COLOR_MAP.put("lavender", 0xFFE6E6FA);
+ COLOR_MAP.put("lavenderblush", 0xFFFFF0F5);
+ COLOR_MAP.put("lawngreen", 0xFF7CFC00);
+ COLOR_MAP.put("lemonchiffon", 0xFFFFFACD);
+ COLOR_MAP.put("lightblue", 0xFFADD8E6);
+ COLOR_MAP.put("lightcoral", 0xFFF08080);
+ COLOR_MAP.put("lightcyan", 0xFFE0FFFF);
+ COLOR_MAP.put("lightgoldenrodyellow", 0xFFFAFAD2);
+ COLOR_MAP.put("lightgray", 0xFFD3D3D3);
+ COLOR_MAP.put("lightgreen", 0xFF90EE90);
+ COLOR_MAP.put("lightgrey", 0xFFD3D3D3);
+ COLOR_MAP.put("lightpink", 0xFFFFB6C1);
+ COLOR_MAP.put("lightsalmon", 0xFFFFA07A);
+ COLOR_MAP.put("lightseagreen", 0xFF20B2AA);
+ COLOR_MAP.put("lightskyblue", 0xFF87CEFA);
+ COLOR_MAP.put("lightslategray", 0xFF778899);
+ COLOR_MAP.put("lightslategrey", 0xFF778899);
+ COLOR_MAP.put("lightsteelblue", 0xFFB0C4DE);
+ COLOR_MAP.put("lightyellow", 0xFFFFFFE0);
+ COLOR_MAP.put("lime", 0xFF00FF00);
+ COLOR_MAP.put("limegreen", 0xFF32CD32);
+ COLOR_MAP.put("linen", 0xFFFAF0E6);
+ COLOR_MAP.put("magenta", 0xFFFF00FF);
+ COLOR_MAP.put("maroon", 0xFF800000);
+ COLOR_MAP.put("mediumaquamarine", 0xFF66CDAA);
+ COLOR_MAP.put("mediumblue", 0xFF0000CD);
+ COLOR_MAP.put("mediumorchid", 0xFFBA55D3);
+ COLOR_MAP.put("mediumpurple", 0xFF9370DB);
+ COLOR_MAP.put("mediumseagreen", 0xFF3CB371);
+ COLOR_MAP.put("mediumslateblue", 0xFF7B68EE);
+ COLOR_MAP.put("mediumspringgreen", 0xFF00FA9A);
+ COLOR_MAP.put("mediumturquoise", 0xFF48D1CC);
+ COLOR_MAP.put("mediumvioletred", 0xFFC71585);
+ COLOR_MAP.put("midnightblue", 0xFF191970);
+ COLOR_MAP.put("mintcream", 0xFFF5FFFA);
+ COLOR_MAP.put("mistyrose", 0xFFFFE4E1);
+ COLOR_MAP.put("moccasin", 0xFFFFE4B5);
+ COLOR_MAP.put("navajowhite", 0xFFFFDEAD);
+ COLOR_MAP.put("navy", 0xFF000080);
+ COLOR_MAP.put("oldlace", 0xFFFDF5E6);
+ COLOR_MAP.put("olive", 0xFF808000);
+ COLOR_MAP.put("olivedrab", 0xFF6B8E23);
+ COLOR_MAP.put("orange", 0xFFFFA500);
+ COLOR_MAP.put("orangered", 0xFFFF4500);
+ COLOR_MAP.put("orchid", 0xFFDA70D6);
+ COLOR_MAP.put("palegoldenrod", 0xFFEEE8AA);
+ COLOR_MAP.put("palegreen", 0xFF98FB98);
+ COLOR_MAP.put("paleturquoise", 0xFFAFEEEE);
+ COLOR_MAP.put("palevioletred", 0xFFDB7093);
+ COLOR_MAP.put("papayawhip", 0xFFFFEFD5);
+ COLOR_MAP.put("peachpuff", 0xFFFFDAB9);
+ COLOR_MAP.put("peru", 0xFFCD853F);
+ COLOR_MAP.put("pink", 0xFFFFC0CB);
+ COLOR_MAP.put("plum", 0xFFDDA0DD);
+ COLOR_MAP.put("powderblue", 0xFFB0E0E6);
+ COLOR_MAP.put("purple", 0xFF800080);
+ COLOR_MAP.put("rebeccapurple", 0xFF663399);
+ COLOR_MAP.put("red", 0xFFFF0000);
+ COLOR_MAP.put("rosybrown", 0xFFBC8F8F);
+ COLOR_MAP.put("royalblue", 0xFF4169E1);
+ COLOR_MAP.put("saddlebrown", 0xFF8B4513);
+ COLOR_MAP.put("salmon", 0xFFFA8072);
+ COLOR_MAP.put("sandybrown", 0xFFF4A460);
+ COLOR_MAP.put("seagreen", 0xFF2E8B57);
+ COLOR_MAP.put("seashell", 0xFFFFF5EE);
+ COLOR_MAP.put("sienna", 0xFFA0522D);
+ COLOR_MAP.put("silver", 0xFFC0C0C0);
+ COLOR_MAP.put("skyblue", 0xFF87CEEB);
+ COLOR_MAP.put("slateblue", 0xFF6A5ACD);
+ COLOR_MAP.put("slategray", 0xFF708090);
+ COLOR_MAP.put("slategrey", 0xFF708090);
+ COLOR_MAP.put("snow", 0xFFFFFAFA);
+ COLOR_MAP.put("springgreen", 0xFF00FF7F);
+ COLOR_MAP.put("steelblue", 0xFF4682B4);
+ COLOR_MAP.put("tan", 0xFFD2B48C);
+ COLOR_MAP.put("teal", 0xFF008080);
+ COLOR_MAP.put("thistle", 0xFFD8BFD8);
+ COLOR_MAP.put("tomato", 0xFFFF6347);
+ COLOR_MAP.put("transparent", 0x00000000);
+ COLOR_MAP.put("turquoise", 0xFF40E0D0);
+ COLOR_MAP.put("violet", 0xFFEE82EE);
+ COLOR_MAP.put("wheat", 0xFFF5DEB3);
+ COLOR_MAP.put("white", 0xFFFFFFFF);
+ COLOR_MAP.put("whitesmoke", 0xFFF5F5F5);
+ COLOR_MAP.put("yellow", 0xFFFFFF00);
+ COLOR_MAP.put("yellowgreen", 0xFF9ACD32);
+ }
+
+ private ColorParser() {
+ // Prevent instantiation.
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ConditionVariable.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ConditionVariable.java
new file mode 100644
index 0000000000..3866edced1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ConditionVariable.java
@@ -0,0 +1,83 @@
+/*
+ * 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;
+
+/**
+ * An interruptible condition variable whose {@link #open()} and {@link #close()} methods return
+ * whether they resulted in a change of state.
+ */
+public final class ConditionVariable {
+
+ private boolean isOpen;
+
+ /**
+ * Opens the condition and releases all threads that are blocked.
+ *
+ * @return True if the condition variable was opened. False if it was already open.
+ */
+ public synchronized boolean open() {
+ if (isOpen) {
+ return false;
+ }
+ isOpen = true;
+ notifyAll();
+ return true;
+ }
+
+ /**
+ * Closes the condition.
+ *
+ * @return True if the condition variable was closed. False if it was already closed.
+ */
+ public synchronized boolean close() {
+ boolean wasOpen = isOpen;
+ isOpen = false;
+ return wasOpen;
+ }
+
+ /**
+ * Blocks until the condition is opened.
+ *
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ public synchronized void block() throws InterruptedException {
+ while (!isOpen) {
+ wait();
+ }
+ }
+
+ /**
+ * 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EGLSurfaceTexture.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EGLSurfaceTexture.java
new file mode 100644
index 0000000000..1f48f718b7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ErrorMessageProvider.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ErrorMessageProvider.java
new file mode 100644
index 0000000000..0eca418cd8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<T extends Throwable> {
+
+ /**
+ * 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<Integer, String> getErrorMessage(T throwable);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventDispatcher.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventDispatcher.java
new file mode 100644
index 0000000000..6e9a3798bf
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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 <T> The type of listener.
+ */
+public final class EventDispatcher<T> {
+
+ /** Functional interface to send an event. */
+ public interface Event<T> {
+
+ /**
+ * 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<HandlerAndListener<T>> 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<T> 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<T> event) {
+ for (HandlerAndListener<T> handlerAndListener : listeners) {
+ handlerAndListener.dispatch(event);
+ }
+ }
+
+ private static final class HandlerAndListener<T> {
+
+ 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<T> event) {
+ handler.post(
+ () -> {
+ if (!released) {
+ event.sendTo(listener);
+ }
+ });
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventLogger.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventLogger.java
new file mode 100644
index 0000000000..0c2a6abcf1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacConstants.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacConstants.java
new file mode 100644
index 0000000000..faa917fab8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacStreamMetadata.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacStreamMetadata.java
new file mode 100644
index 0000000000..893481d8da
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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 <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
+ * METADATA_BLOCK_STREAMINFO</a>
+ * @see <a href="https://xiph.org/flac/format.html#metadata_block_seektable">FLAC format
+ * METADATA_BLOCK_SEEKTABLE</a>
+ * @see <a href="https://xiph.org/flac/format.html#metadata_block_vorbis_comment">FLAC format
+ * METADATA_BLOCK_VORBIS_COMMENT</a>
+ * @see <a href="https://xiph.org/flac/format.html#metadata_block_picture">FLAC format
+ * METADATA_BLOCK_PICTURE</a>
+ */
+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.
+ *
+ * <p>This key is used to indicate the sample rate in the frame header for the most common values.
+ *
+ * <p>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.
+ *
+ * <p>This key is used to indicate the number of bits per sample in the frame header for the most
+ * common values.
+ *
+ * <p>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<String> vorbisComments,
+ ArrayList<PictureFrame> 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.
+ *
+ * <p>{@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<String> 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<PictureFrame> 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<String> vorbisComments, List<PictureFrame> pictureFrames) {
+ if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) {
+ return null;
+ }
+
+ ArrayList<Metadata.Entry> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/GlUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/GlUtil.java
new file mode 100644
index 0000000000..a34cee48f9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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)}.
+ *
+ * <p>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)}.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/HandlerWrapper.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/HandlerWrapper.java
new file mode 100644
index 0000000000..2e412fa10f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LibraryLoader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LibraryLoader.java
new file mode 100644
index 0000000000..31e582aac5
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LibraryLoader.java
@@ -0,0 +1,68 @@
+/*
+ * 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 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;
+
+ /**
+ * @param libraries The names of the libraries to load.
+ */
+ public LibraryLoader(String... libraries) {
+ nativeLibraries = libraries;
+ }
+
+ /**
+ * Overrides the names of the libraries to load. Must be called before any call to
+ * {@link #isAvailable()}.
+ */
+ public synchronized void setLibraries(String... libraries) {
+ Assertions.checkState(!loadAttempted, "Cannot set libraries after loading");
+ nativeLibraries = libraries;
+ }
+
+ /**
+ * Returns whether the underlying libraries are available, loading them if necessary.
+ */
+ public synchronized boolean isAvailable() {
+ if (loadAttempted) {
+ return isAvailable;
+ }
+ loadAttempted = true;
+ try {
+ for (String lib : nativeLibraries) {
+ System.loadLibrary(lib);
+ }
+ isAvailable = true;
+ } catch (UnsatisfiedLinkError exception) {
+ // 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Log.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Log.java
new file mode 100644
index 0000000000..b6e4a25935
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LongArray.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LongArray.java
new file mode 100644
index 0000000000..ef6f938ca8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LongArray.java
@@ -0,0 +1,84 @@
+/*
+ * 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 java.util.Arrays;
+
+/**
+ * An append-only, auto-growing {@code long[]}.
+ */
+public final class LongArray {
+
+ private static final int DEFAULT_INITIAL_CAPACITY = 32;
+
+ private int size;
+ private long[] values;
+
+ public LongArray() {
+ this(DEFAULT_INITIAL_CAPACITY);
+ }
+
+ /**
+ * @param initialCapacity The initial capacity of the array.
+ */
+ public LongArray(int initialCapacity) {
+ values = new long[initialCapacity];
+ }
+
+ /**
+ * Appends a value.
+ *
+ * @param value The value to append.
+ */
+ public void add(long value) {
+ if (size == values.length) {
+ values = Arrays.copyOf(values, size * 2);
+ }
+ values[size++] = value;
+ }
+
+ /**
+ * Returns the value at a specified index.
+ *
+ * @param index The index.
+ * @return The corresponding value.
+ * @throws IndexOutOfBoundsException If the index is less than zero, or greater than or equal to
+ * {@link #size()}.
+ */
+ public long get(int index) {
+ if (index < 0 || index >= size) {
+ throw new IndexOutOfBoundsException("Invalid index " + index + ", size is " + size);
+ }
+ return values[index];
+ }
+
+ /**
+ * Returns the current size of the array.
+ */
+ public int size() {
+ return size;
+ }
+
+ /**
+ * Copies the current values into a newly allocated primitive array.
+ *
+ * @return The primitive array containing the copied values.
+ */
+ public long[] toArray() {
+ return Arrays.copyOf(values, size);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MediaClock.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MediaClock.java
new file mode 100644
index 0000000000..029f3aa8f5
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MediaClock.java
@@ -0,0 +1,43 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters;
+
+/**
+ * Tracks the progression of media time.
+ */
+public interface MediaClock {
+
+ /**
+ * Returns the current media position in microseconds.
+ */
+ long getPositionUs();
+
+ /**
+ * Attempts to set the playback parameters. The media clock may override these parameters if they
+ * are not supported.
+ *
+ * @param playbackParameters The playback parameters to attempt to set.
+ */
+ void setPlaybackParameters(PlaybackParameters playbackParameters);
+
+ /**
+ * Returns the active playback parameters.
+ */
+ PlaybackParameters getPlaybackParameters();
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MimeTypes.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MimeTypes.java
new file mode 100644
index 0000000000..594a62d63a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MimeTypes.java
@@ -0,0 +1,465 @@
+/*
+ * 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.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.
+ */
+public final class MimeTypes {
+
+ public static final String BASE_TYPE_VIDEO = "video";
+ public static final String BASE_TYPE_AUDIO = "audio";
+ public static final String BASE_TYPE_TEXT = "text";
+ public static final String BASE_TYPE_APPLICATION = "application";
+
+ public static final String VIDEO_MP4 = BASE_TYPE_VIDEO + "/mp4";
+ public static final String VIDEO_WEBM = BASE_TYPE_VIDEO + "/webm";
+ public static final String VIDEO_H263 = BASE_TYPE_VIDEO + "/3gpp";
+ public static final String VIDEO_H264 = BASE_TYPE_VIDEO + "/avc";
+ 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";
+ public static final String AUDIO_AAC = BASE_TYPE_AUDIO + "/mp4a-latm";
+ public static final String AUDIO_WEBM = BASE_TYPE_AUDIO + "/webm";
+ public static final String AUDIO_MPEG = BASE_TYPE_AUDIO + "/mpeg";
+ public static final String AUDIO_MPEG_L1 = BASE_TYPE_AUDIO + "/mpeg-L1";
+ 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_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";
+ public static final String AUDIO_DTS_EXPRESS = BASE_TYPE_AUDIO + "/vnd.dts.hd;profile=lbr";
+ public static final String AUDIO_VORBIS = BASE_TYPE_AUDIO + "/vorbis";
+ 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 + "/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";
+ public static final String APPLICATION_SUBRIP = BASE_TYPE_APPLICATION + "/x-subrip";
+ public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
+ public static final String APPLICATION_TX3G = BASE_TYPE_APPLICATION + "/x-quicktime-tx3g";
+ public static final String APPLICATION_MP4VTT = BASE_TYPE_APPLICATION + "/x-mp4-vtt";
+ public static final String APPLICATION_MP4CEA608 = BASE_TYPE_APPLICATION + "/x-mp4-cea-608";
+ public static final String APPLICATION_RAWCC = BASE_TYPE_APPLICATION + "/x-rawcc";
+ public static final String APPLICATION_VOBSUB = BASE_TYPE_APPLICATION + "/vobsub";
+ public static final String APPLICATION_PGS = BASE_TYPE_APPLICATION + "/pgs";
+ public static final String APPLICATION_SCTE35 = BASE_TYPE_APPLICATION + "/x-scte35";
+ 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 static final ArrayList<CustomMimeType> customMimeTypes = new ArrayList<>();
+
+ /**
+ * 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 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 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));
+ }
+
+ /** Returns whether the given string is a video MIME type. */
+ public static boolean isVideo(@Nullable String mimeType) {
+ return BASE_TYPE_VIDEO.equals(getTopLevelType(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));
+ }
+
+ /** 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.
+ *
+ * @param codecs The codecs attribute.
+ * @return The derived video mimeType, or null if it could not be derived.
+ */
+ @Nullable
+ public static String getVideoMediaMimeType(@Nullable String codecs) {
+ if (codecs == null) {
+ return null;
+ }
+ String[] codecList = Util.splitCodecs(codecs);
+ for (String codec : codecList) {
+ @Nullable String mimeType = getMediaMimeType(codec);
+ if (mimeType != null && isVideo(mimeType)) {
+ return mimeType;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Derives a audio sample mimeType from a codecs attribute.
+ *
+ * @param codecs The codecs attribute.
+ * @return The derived audio mimeType, or null if it could not be derived.
+ */
+ @Nullable
+ public static String getAudioMediaMimeType(@Nullable String codecs) {
+ if (codecs == null) {
+ return null;
+ }
+ String[] codecList = Util.splitCodecs(codecs);
+ for (String codec : codecList) {
+ @Nullable String mimeType = getMediaMimeType(codec);
+ if (mimeType != null && isAudio(mimeType)) {
+ return mimeType;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Derives a mimeType from a codec identifier, as defined in RFC 6381.
+ *
+ * @param codec The codec identifier to derive.
+ * @return The mimeType, or null if it could not be derived.
+ */
+ @Nullable
+ public static String getMediaMimeType(@Nullable String codec) {
+ if (codec == null) {
+ return null;
+ }
+ 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("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") || codec.startsWith("vp08")) {
+ return MimeTypes.VIDEO_VP8;
+ } else if (codec.startsWith("mp4a")) {
+ @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")) {
+ return MimeTypes.AUDIO_DTS_HD;
+ } else if (codec.startsWith("opus")) {
+ 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);
+ }
+ }
+
+ /**
+ * 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 MIME type.
+ * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified MIME type.
+ */
+ public static int getTrackType(@Nullable String mimeType) {
+ if (TextUtils.isEmpty(mimeType)) {
+ return C.TRACK_TYPE_UNKNOWN;
+ } else if (isAudio(mimeType)) {
+ return C.TRACK_TYPE_AUDIO;
+ } else if (isVideo(mimeType)) {
+ return C.TRACK_TYPE_VIDEO;
+ } else if (isText(mimeType) || APPLICATION_CEA608.equals(mimeType)
+ || APPLICATION_CEA708.equals(mimeType) || APPLICATION_MP4CEA608.equals(mimeType)
+ || APPLICATION_SUBRIP.equals(mimeType) || APPLICATION_TTML.equals(mimeType)
+ || APPLICATION_TX3G.equals(mimeType) || APPLICATION_MP4VTT.equals(mimeType)
+ || APPLICATION_RAWCC.equals(mimeType) || APPLICATION_VOBSUB.equals(mimeType)
+ || APPLICATION_PGS.equals(mimeType) || APPLICATION_DVBSUBS.equals(mimeType)) {
+ return C.TRACK_TYPE_TEXT;
+ } else if (APPLICATION_ID3.equals(mimeType)
+ || APPLICATION_EMSG.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 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;
+ }
+ }
+
+ /**
+ * Equivalent to {@code getTrackType(getMediaMimeType(codec))}.
+ *
+ * @param codec The codec.
+ * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified codec.
+ */
+ public static int getTrackTypeOfCodec(String codec) {
+ return getTrackType(getMediaMimeType(codec));
+ }
+
+ /**
+ * Returns the top-level type of {@code mimeType}, or null if {@code mimeType} is null or does not
+ * contain a forward slash character ({@code '/'}).
+ */
+ @Nullable
+ private static String getTopLevelType(@Nullable String mimeType) {
+ if (mimeType == null) {
+ return null;
+ }
+ int indexOfSlash = mimeType.indexOf('/');
+ if (indexOfSlash == -1) {
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NalUnitUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NalUnitUtil.java
new file mode 100644
index 0000000000..d7409daa66
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NalUnitUtil.java
@@ -0,0 +1,519 @@
+/*
+ * 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 java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * Utility methods for handling H.264/AVC and H.265/HEVC NAL units.
+ */
+public final class NalUnitUtil {
+
+ private static final String TAG = "NalUnitUtil";
+
+ /**
+ * Holds data parsed from a sequence parameter set NAL unit.
+ */
+ 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;
+ public final float pixelWidthAspectRatio;
+ public final boolean separateColorPlaneFlag;
+ public final boolean frameMbsOnlyFlag;
+ public final int frameNumLength;
+ public final int picOrderCountType;
+ public final int picOrderCntLsbLength;
+ public final 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;
+ this.pixelWidthAspectRatio = pixelWidthAspectRatio;
+ this.separateColorPlaneFlag = separateColorPlaneFlag;
+ this.frameMbsOnlyFlag = frameMbsOnlyFlag;
+ this.frameNumLength = frameNumLength;
+ this.picOrderCountType = picOrderCountType;
+ this.picOrderCntLsbLength = picOrderCntLsbLength;
+ this.deltaPicOrderAlwaysZeroFlag = deltaPicOrderAlwaysZeroFlag;
+ }
+
+ }
+
+ /**
+ * Holds data parsed from a picture parameter set NAL unit.
+ */
+ public static final class PpsData {
+
+ public final int picParameterSetId;
+ public final int seqParameterSetId;
+ public final boolean bottomFieldPicOrderInFramePresentFlag;
+
+ public PpsData(int picParameterSetId, int seqParameterSetId,
+ boolean bottomFieldPicOrderInFramePresentFlag) {
+ this.picParameterSetId = picParameterSetId;
+ this.seqParameterSetId = seqParameterSetId;
+ this.bottomFieldPicOrderInFramePresentFlag = bottomFieldPicOrderInFramePresentFlag;
+ }
+
+ }
+
+ /** Four initial bytes that must prefix NAL units for decoding. */
+ public static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
+
+ /** Value for aspect_ratio_idc indicating an extended aspect ratio, in H.264 and H.265 SPSs. */
+ public static final int EXTENDED_SAR = 0xFF;
+ /** Aspect ratios indexed by aspect_ratio_idc, in H.264 and H.265 SPSs. */
+ public static final float[] ASPECT_RATIO_IDC_VALUES = new float[] {
+ 1f /* Unspecified. Assume square */,
+ 1f,
+ 12f / 11f,
+ 10f / 11f,
+ 16f / 11f,
+ 40f / 33f,
+ 24f / 11f,
+ 20f / 11f,
+ 32f / 11f,
+ 80f / 33f,
+ 18f / 11f,
+ 15f / 11f,
+ 64f / 33f,
+ 160f / 99f,
+ 4f / 3f,
+ 3f / 2f,
+ 2f
+ };
+
+ private static final int H264_NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information
+ private static final int H264_NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set
+ private static final int H265_NAL_UNIT_TYPE_PREFIX_SEI = 39;
+
+ private static final Object scratchEscapePositionsLock = new Object();
+
+ /**
+ * Temporary store for positions of escape codes in {@link #unescapeStream(byte[], int)}. Guarded
+ * by {@link #scratchEscapePositionsLock}.
+ */
+ private static int[] scratchEscapePositions = new int[10];
+
+ /**
+ * Unescapes {@code data} up to the specified limit, replacing occurrences of [0, 0, 3] with
+ * [0, 0]. The unescaped data is returned in-place, with the return value indicating its length.
+ * <p>
+ * Executions of this method are mutually exclusive, so it should not be called with very large
+ * buffers.
+ *
+ * @param data The data to unescape.
+ * @param limit The limit (exclusive) of the data to unescape.
+ * @return The length of the unescaped data.
+ */
+ public static int unescapeStream(byte[] data, int limit) {
+ synchronized (scratchEscapePositionsLock) {
+ int position = 0;
+ int scratchEscapeCount = 0;
+ while (position < limit) {
+ position = findNextUnescapeIndex(data, position, limit);
+ if (position < limit) {
+ if (scratchEscapePositions.length <= scratchEscapeCount) {
+ // Grow scratchEscapePositions to hold a larger number of positions.
+ scratchEscapePositions = Arrays.copyOf(scratchEscapePositions,
+ scratchEscapePositions.length * 2);
+ }
+ scratchEscapePositions[scratchEscapeCount++] = position;
+ position += 3;
+ }
+ }
+
+ int unescapedLength = limit - scratchEscapeCount;
+ int escapedPosition = 0; // The position being read from.
+ int unescapedPosition = 0; // The position being written to.
+ for (int i = 0; i < scratchEscapeCount; i++) {
+ int nextEscapePosition = scratchEscapePositions[i];
+ int copyLength = nextEscapePosition - escapedPosition;
+ System.arraycopy(data, escapedPosition, data, unescapedPosition, copyLength);
+ unescapedPosition += copyLength;
+ data[unescapedPosition++] = 0;
+ data[unescapedPosition++] = 0;
+ escapedPosition += copyLength + 3;
+ }
+
+ int remainingLength = unescapedLength - unescapedPosition;
+ System.arraycopy(data, escapedPosition, data, unescapedPosition, remainingLength);
+ return unescapedLength;
+ }
+ }
+
+ /**
+ * Discards data from the buffer up to the first SPS, where {@code data.position()} is interpreted
+ * as the length of the buffer.
+ * <p>
+ * When the method returns, {@code data.position()} will contain the new length of the buffer. If
+ * the buffer is not empty it is guaranteed to start with an SPS.
+ *
+ * @param data Buffer containing start code delimited NAL units.
+ */
+ public static void discardToSps(ByteBuffer data) {
+ int length = data.position();
+ int consecutiveZeros = 0;
+ int offset = 0;
+ while (offset + 1 < length) {
+ int value = data.get(offset) & 0xFF;
+ if (consecutiveZeros == 3) {
+ if (value == 1 && (data.get(offset + 1) & 0x1F) == H264_NAL_UNIT_TYPE_SPS) {
+ // Copy from this NAL unit onwards to the start of the buffer.
+ ByteBuffer offsetData = data.duplicate();
+ offsetData.position(offset - 3);
+ offsetData.limit(length);
+ data.position(0);
+ data.put(offsetData);
+ return;
+ }
+ } else if (value == 0) {
+ consecutiveZeros++;
+ }
+ if (value != 0) {
+ consecutiveZeros = 0;
+ }
+ offset++;
+ }
+ // Empty the buffer if the SPS NAL unit was not found.
+ data.clear();
+ }
+
+ /**
+ * Returns whether the NAL unit with the specified header contains supplemental enhancement
+ * information.
+ *
+ * @param mimeType The sample MIME type.
+ * @param nalUnitHeaderFirstByte The first byte of nal_unit().
+ * @return Whether the NAL unit with the specified header is an SEI NAL unit.
+ */
+ public static boolean isNalUnitSei(String mimeType, byte nalUnitHeaderFirstByte) {
+ return (MimeTypes.VIDEO_H264.equals(mimeType)
+ && (nalUnitHeaderFirstByte & 0x1F) == H264_NAL_UNIT_TYPE_SEI)
+ || (MimeTypes.VIDEO_H265.equals(mimeType)
+ && ((nalUnitHeaderFirstByte & 0x7E) >> 1) == H265_NAL_UNIT_TYPE_PREFIX_SEI);
+ }
+
+ /**
+ * Returns the type of the NAL unit in {@code data} that starts at {@code offset}.
+ *
+ * @param data The data to search.
+ * @param offset The start offset of a NAL unit. Must lie between {@code -3} (inclusive) and
+ * {@code data.length - 3} (exclusive).
+ * @return The type of the unit.
+ */
+ public static int getNalUnitType(byte[] data, int offset) {
+ return data[offset + 3] & 0x1F;
+ }
+
+ /**
+ * Returns the type of the H.265 NAL unit in {@code data} that starts at {@code offset}.
+ *
+ * @param data The data to search.
+ * @param offset The start offset of a NAL unit. Must lie between {@code -3} (inclusive) and
+ * {@code data.length - 3} (exclusive).
+ * @return The type of the unit.
+ */
+ public static int getH265NalUnitType(byte[] data, int offset) {
+ return (data[offset + 3] & 0x7E) >> 1;
+ }
+
+ /**
+ * Parses an SPS NAL unit using the syntax defined in ITU-T Recommendation H.264 (2013) subsection
+ * 7.3.2.1.1.
+ *
+ * @param nalData A buffer containing escaped SPS data.
+ * @param nalOffset The offset of the NAL unit header in {@code nalData}.
+ * @param nalLimit The limit of the NAL unit in {@code nalData}.
+ * @return A parsed representation of the SPS data.
+ */
+ public static SpsData parseSpsNalUnit(byte[] nalData, int nalOffset, int nalLimit) {
+ ParsableNalUnitBitArray data = new ParsableNalUnitBitArray(nalData, nalOffset, nalLimit);
+ data.skipBits(8); // nal_unit
+ int profileIdc = data.readBits(8);
+ int constraintsFlagsAndReservedZero2Bits = data.readBits(8);
+ int levelIdc = data.readBits(8);
+ int seqParameterSetId = data.readUnsignedExpGolombCodedInt();
+
+ int chromaFormatIdc = 1; // Default is 4:2:0
+ boolean separateColorPlaneFlag = false;
+ if (profileIdc == 100 || profileIdc == 110 || profileIdc == 122 || profileIdc == 244
+ || profileIdc == 44 || profileIdc == 83 || profileIdc == 86 || profileIdc == 118
+ || profileIdc == 128 || profileIdc == 138) {
+ chromaFormatIdc = data.readUnsignedExpGolombCodedInt();
+ if (chromaFormatIdc == 3) {
+ separateColorPlaneFlag = data.readBit();
+ }
+ data.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8
+ data.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8
+ data.skipBit(); // qpprime_y_zero_transform_bypass_flag
+ boolean seqScalingMatrixPresentFlag = data.readBit();
+ if (seqScalingMatrixPresentFlag) {
+ int limit = (chromaFormatIdc != 3) ? 8 : 12;
+ for (int i = 0; i < limit; i++) {
+ boolean seqScalingListPresentFlag = data.readBit();
+ if (seqScalingListPresentFlag) {
+ skipScalingList(data, i < 6 ? 16 : 64);
+ }
+ }
+ }
+ }
+
+ int frameNumLength = data.readUnsignedExpGolombCodedInt() + 4; // log2_max_frame_num_minus4 + 4
+ int picOrderCntType = data.readUnsignedExpGolombCodedInt();
+ int picOrderCntLsbLength = 0;
+ boolean deltaPicOrderAlwaysZeroFlag = false;
+ if (picOrderCntType == 0) {
+ // log2_max_pic_order_cnt_lsb_minus4 + 4
+ picOrderCntLsbLength = data.readUnsignedExpGolombCodedInt() + 4;
+ } else if (picOrderCntType == 1) {
+ deltaPicOrderAlwaysZeroFlag = data.readBit(); // delta_pic_order_always_zero_flag
+ data.readSignedExpGolombCodedInt(); // offset_for_non_ref_pic
+ data.readSignedExpGolombCodedInt(); // offset_for_top_to_bottom_field
+ long numRefFramesInPicOrderCntCycle = data.readUnsignedExpGolombCodedInt();
+ for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
+ data.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i]
+ }
+ }
+ data.readUnsignedExpGolombCodedInt(); // max_num_ref_frames
+ 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.skipBit(); // mb_adaptive_frame_field_flag
+ }
+
+ data.skipBit(); // direct_8x8_inference_flag
+ int frameWidth = picWidthInMbs * 16;
+ int frameHeight = frameHeightInMbs * 16;
+ boolean frameCroppingFlag = data.readBit();
+ if (frameCroppingFlag) {
+ int frameCropLeftOffset = data.readUnsignedExpGolombCodedInt();
+ int frameCropRightOffset = data.readUnsignedExpGolombCodedInt();
+ int frameCropTopOffset = data.readUnsignedExpGolombCodedInt();
+ int frameCropBottomOffset = data.readUnsignedExpGolombCodedInt();
+ int cropUnitX;
+ int cropUnitY;
+ if (chromaFormatIdc == 0) {
+ cropUnitX = 1;
+ cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0);
+ } else {
+ int subWidthC = (chromaFormatIdc == 3) ? 1 : 2;
+ int subHeightC = (chromaFormatIdc == 1) ? 2 : 1;
+ cropUnitX = subWidthC;
+ cropUnitY = subHeightC * (2 - (frameMbsOnlyFlag ? 1 : 0));
+ }
+ frameWidth -= (frameCropLeftOffset + frameCropRightOffset) * cropUnitX;
+ frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY;
+ }
+
+ float pixelWidthHeightRatio = 1;
+ boolean vuiParametersPresentFlag = data.readBit();
+ if (vuiParametersPresentFlag) {
+ boolean aspectRatioInfoPresentFlag = data.readBit();
+ if (aspectRatioInfoPresentFlag) {
+ int aspectRatioIdc = data.readBits(8);
+ if (aspectRatioIdc == NalUnitUtil.EXTENDED_SAR) {
+ int sarWidth = data.readBits(16);
+ int sarHeight = data.readBits(16);
+ if (sarWidth != 0 && sarHeight != 0) {
+ pixelWidthHeightRatio = (float) sarWidth / sarHeight;
+ }
+ } else if (aspectRatioIdc < NalUnitUtil.ASPECT_RATIO_IDC_VALUES.length) {
+ pixelWidthHeightRatio = NalUnitUtil.ASPECT_RATIO_IDC_VALUES[aspectRatioIdc];
+ } else {
+ Log.w(TAG, "Unexpected aspect_ratio_idc value: " + aspectRatioIdc);
+ }
+ }
+ }
+
+ return new SpsData(
+ profileIdc,
+ constraintsFlagsAndReservedZero2Bits,
+ levelIdc,
+ seqParameterSetId,
+ frameWidth,
+ frameHeight,
+ pixelWidthHeightRatio,
+ separateColorPlaneFlag,
+ frameMbsOnlyFlag,
+ frameNumLength,
+ picOrderCntType,
+ picOrderCntLsbLength,
+ deltaPicOrderAlwaysZeroFlag);
+ }
+
+ /**
+ * Parses a PPS NAL unit using the syntax defined in ITU-T Recommendation H.264 (2013) subsection
+ * 7.3.2.2.
+ *
+ * @param nalData A buffer containing escaped PPS data.
+ * @param nalOffset The offset of the NAL unit header in {@code nalData}.
+ * @param nalLimit The limit of the NAL unit in {@code nalData}.
+ * @return A parsed representation of the PPS data.
+ */
+ public static PpsData parsePpsNalUnit(byte[] nalData, int nalOffset, int nalLimit) {
+ ParsableNalUnitBitArray data = new ParsableNalUnitBitArray(nalData, nalOffset, nalLimit);
+ data.skipBits(8); // nal_unit
+ int picParameterSetId = data.readUnsignedExpGolombCodedInt();
+ int seqParameterSetId = data.readUnsignedExpGolombCodedInt();
+ data.skipBit(); // entropy_coding_mode_flag
+ boolean bottomFieldPicOrderInFramePresentFlag = data.readBit();
+ return new PpsData(picParameterSetId, seqParameterSetId, bottomFieldPicOrderInFramePresentFlag);
+ }
+
+ /**
+ * Finds the first NAL unit in {@code data}.
+ * <p>
+ * If {@code prefixFlags} is null then the first three bytes of a NAL unit must be entirely
+ * contained within the part of the array being searched in order for it to be found.
+ * <p>
+ * When {@code prefixFlags} is non-null, this method supports finding NAL units whose first four
+ * bytes span {@code data} arrays passed to successive calls. To use this feature, pass the same
+ * {@code prefixFlags} parameter to successive calls. State maintained in this parameter enables
+ * the detection of such NAL units. Note that when using this feature, the return value may be 3,
+ * 2 or 1 less than {@code startOffset}, to indicate a NAL unit starting 3, 2 or 1 bytes before
+ * the first byte in the current array.
+ *
+ * @param data The data to search.
+ * @param startOffset The offset (inclusive) in the data to start the search.
+ * @param endOffset The offset (exclusive) in the data to end the search.
+ * @param prefixFlags A boolean array whose first three elements are used to store the state
+ * required to detect NAL units where the NAL unit prefix spans array boundaries. The array
+ * must be at least 3 elements long.
+ * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found.
+ */
+ public static int findNalUnit(byte[] data, int startOffset, int endOffset,
+ boolean[] prefixFlags) {
+ int length = endOffset - startOffset;
+
+ Assertions.checkState(length >= 0);
+ if (length == 0) {
+ return endOffset;
+ }
+
+ if (prefixFlags != null) {
+ if (prefixFlags[0]) {
+ clearPrefixFlags(prefixFlags);
+ return startOffset - 3;
+ } else if (length > 1 && prefixFlags[1] && data[startOffset] == 1) {
+ clearPrefixFlags(prefixFlags);
+ return startOffset - 2;
+ } else if (length > 2 && prefixFlags[2] && data[startOffset] == 0
+ && data[startOffset + 1] == 1) {
+ clearPrefixFlags(prefixFlags);
+ return startOffset - 1;
+ }
+ }
+
+ int limit = endOffset - 1;
+ // We're looking for the NAL unit start code prefix 0x000001. The value of i tracks the index of
+ // the third byte.
+ for (int i = startOffset + 2; i < limit; i += 3) {
+ if ((data[i] & 0xFE) != 0) {
+ // There isn't a NAL prefix here, or at the next two positions. Do nothing and let the
+ // loop advance the index by three.
+ } else if (data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 1) {
+ if (prefixFlags != null) {
+ clearPrefixFlags(prefixFlags);
+ }
+ return i - 2;
+ } else {
+ // There isn't a NAL prefix here, but there might be at the next position. We should
+ // only skip forward by one. The loop will skip forward by three, so subtract two here.
+ i -= 2;
+ }
+ }
+
+ if (prefixFlags != null) {
+ // True if the last three bytes in the data seen so far are {0,0,1}.
+ prefixFlags[0] = length > 2
+ ? (data[endOffset - 3] == 0 && data[endOffset - 2] == 0 && data[endOffset - 1] == 1)
+ : length == 2 ? (prefixFlags[2] && data[endOffset - 2] == 0 && data[endOffset - 1] == 1)
+ : (prefixFlags[1] && data[endOffset - 1] == 1);
+ // True if the last two bytes in the data seen so far are {0,0}.
+ prefixFlags[1] = length > 1 ? data[endOffset - 2] == 0 && data[endOffset - 1] == 0
+ : prefixFlags[2] && data[endOffset - 1] == 0;
+ // True if the last byte in the data seen so far is {0}.
+ prefixFlags[2] = data[endOffset - 1] == 0;
+ }
+
+ return endOffset;
+ }
+
+ /**
+ * Clears prefix flags, as used by {@link #findNalUnit(byte[], int, int, boolean[])}.
+ *
+ * @param prefixFlags The flags to clear.
+ */
+ public static void clearPrefixFlags(boolean[] prefixFlags) {
+ prefixFlags[0] = false;
+ prefixFlags[1] = false;
+ prefixFlags[2] = false;
+ }
+
+ private static int findNextUnescapeIndex(byte[] bytes, int offset, int limit) {
+ for (int i = offset; i < limit - 2; i++) {
+ if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x03) {
+ return i;
+ }
+ }
+ return limit;
+ }
+
+ private static void skipScalingList(ParsableNalUnitBitArray bitArray, int size) {
+ int lastScale = 8;
+ int nextScale = 8;
+ for (int i = 0; i < size; i++) {
+ if (nextScale != 0) {
+ int deltaScale = bitArray.readSignedExpGolombCodedInt();
+ nextScale = (lastScale + deltaScale + 256) % 256;
+ }
+ lastScale = (nextScale == 0) ? lastScale : nextScale;
+ }
+ }
+
+ private NalUnitUtil() {
+ // Prevent instantiation.
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NonNullApi.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NonNullApi.java
new file mode 100644
index 0000000000..0c9b9b2182
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NotificationUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NotificationUtil.java
new file mode 100644
index 0000000000..df68c8fe59
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableBitArray.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableBitArray.java
new file mode 100644
index 0000000000..3d6a702723
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableBitArray.java
@@ -0,0 +1,323 @@
+/*
+ * 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;
+
+/**
+ * Wraps a byte array, providing methods that allow it to be read as a bitstream.
+ */
+public final class ParsableBitArray {
+
+ public byte[] data;
+
+ // The offset within the data, stored as the current byte offset, and the bit offset within that
+ // byte (from 0 to 7).
+ private int byteOffset;
+ private int bitOffset;
+ private int byteLimit;
+
+ /** 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.
+ *
+ * @param data The data to wrap.
+ */
+ public ParsableBitArray(byte[] data) {
+ this(data, data.length);
+ }
+
+ /**
+ * Creates a new instance that wraps an existing array.
+ *
+ * @param data The data to wrap.
+ * @param limit The limit in bytes.
+ */
+ public ParsableBitArray(byte[] data, int limit) {
+ this.data = data;
+ byteLimit = limit;
+ }
+
+ /**
+ * Updates the instance to wrap {@code data}, and resets the position to zero.
+ *
+ * @param data The array to wrap.
+ */
+ public void reset(byte[] data) {
+ 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.
+ *
+ * @param data The array to wrap.
+ * @param limit The limit in bytes.
+ */
+ public void reset(byte[] data, int limit) {
+ this.data = data;
+ byteOffset = 0;
+ bitOffset = 0;
+ byteLimit = limit;
+ }
+
+ /**
+ * Returns the number of bits yet to be read.
+ */
+ public int bitsLeft() {
+ return (byteLimit - byteOffset) * 8 - bitOffset;
+ }
+
+ /**
+ * Returns the current bit offset.
+ */
+ public int getPosition() {
+ return byteOffset * 8 + bitOffset;
+ }
+
+ /**
+ * Returns the current byte offset. Must only be called when the position is byte aligned.
+ *
+ * @throws IllegalStateException If the position isn't byte aligned.
+ */
+ public int getBytePosition() {
+ Assertions.checkState(bitOffset == 0);
+ return byteOffset;
+ }
+
+ /**
+ * Sets the current bit offset.
+ *
+ * @param position The position to set.
+ */
+ public void setPosition(int position) {
+ byteOffset = position / 8;
+ bitOffset = position - (byteOffset * 8);
+ assertValidOffset();
+ }
+
+ /**
+ * Skips a single bit.
+ */
+ public void skipBit() {
+ if (++bitOffset == 8) {
+ bitOffset = 0;
+ byteOffset++;
+ }
+ assertValidOffset();
+ }
+
+ /**
+ * Skips bits and moves current reading position forward.
+ *
+ * @param numBits The number of bits to skip.
+ */
+ public void skipBits(int numBits) {
+ int numBytes = numBits / 8;
+ byteOffset += numBytes;
+ bitOffset += numBits - (numBytes * 8);
+ if (bitOffset > 7) {
+ byteOffset++;
+ bitOffset -= 8;
+ }
+ assertValidOffset();
+ }
+
+ /**
+ * Reads a single bit.
+ *
+ * @return Whether the bit is set.
+ */
+ public boolean readBit() {
+ 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 {@code numBits} bits hold the read data.
+ */
+ public int readBits(int numBits) {
+ if (numBits == 0) {
+ return 0;
+ }
+ int returnValue = 0;
+ 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++;
+ }
+ 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.
+ */
+ public void byteAlign() {
+ if (bitOffset == 0) {
+ return;
+ }
+ bitOffset = 0;
+ byteOffset++;
+ assertValidOffset();
+ }
+
+ /**
+ * Reads the next {@code length} bytes into {@code buffer}. Must only be called when the position
+ * is byte aligned.
+ *
+ * @see System#arraycopy(Object, int, Object, int, int)
+ * @param buffer The array into which the read data should be written.
+ * @param offset The offset in {@code buffer} at which the read data should be written.
+ * @param length The number of bytes to read.
+ * @throws IllegalStateException If the position isn't byte aligned.
+ */
+ public void readBytes(byte[] buffer, int offset, int length) {
+ Assertions.checkState(bitOffset == 0);
+ System.arraycopy(data, byteOffset, buffer, offset, length);
+ byteOffset += length;
+ assertValidOffset();
+ }
+
+ /**
+ * Skips the next {@code length} bytes. Must only be called when the position is byte aligned.
+ *
+ * @param length The number of bytes to read.
+ * @throws IllegalStateException If the position isn't byte aligned.
+ */
+ public void skipBytes(int length) {
+ Assertions.checkState(bitOffset == 0);
+ byteOffset += length;
+ 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
+ && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0)));
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableByteArray.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableByteArray.java
new file mode 100644
index 0000000000..9ad9dd1aa7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableByteArray.java
@@ -0,0 +1,586 @@
+/*
+ * 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 java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+
+/**
+ * Wraps a byte array, providing a set of methods for parsing data from it. Numerical values are
+ * parsed with the assumption that their constituent bytes are in big endian order.
+ */
+public final class ParsableByteArray {
+
+ public byte[] data;
+
+ private int position;
+ private int limit;
+
+ /** 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.
+ *
+ * @param limit The limit to set.
+ */
+ public ParsableByteArray(int limit) {
+ this.data = new byte[limit];
+ this.limit = limit;
+ }
+
+ /**
+ * Creates a new instance wrapping {@code data}, and sets the limit to {@code data.length}.
+ *
+ * @param data The array to wrap.
+ */
+ public ParsableByteArray(byte[] data) {
+ this.data = data;
+ limit = data.length;
+ }
+
+ /**
+ * Creates a new instance that wraps an existing array.
+ *
+ * @param data The data to wrap.
+ * @param limit The limit to set.
+ */
+ public ParsableByteArray(byte[] data, int limit) {
+ this.data = data;
+ 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.
+ *
+ * @param limit The limit to set.
+ */
+ public void reset(int limit) {
+ 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.
+ *
+ * @param data The array to wrap.
+ * @param limit The limit to set.
+ */
+ public void reset(byte[] data, int limit) {
+ this.data = data;
+ this.limit = limit;
+ position = 0;
+ }
+
+ /**
+ * Returns the number of bytes yet to be read.
+ */
+ public int bytesLeft() {
+ return limit - position;
+ }
+
+ /**
+ * Returns the limit.
+ */
+ public int limit() {
+ return limit;
+ }
+
+ /**
+ * Sets the limit.
+ *
+ * @param limit The limit to set.
+ */
+ public void setLimit(int limit) {
+ Assertions.checkArgument(limit >= 0 && limit <= data.length);
+ this.limit = limit;
+ }
+
+ /**
+ * Returns the current offset in the array, in bytes.
+ */
+ public int getPosition() {
+ return position;
+ }
+
+ /**
+ * Returns the capacity of the array, which may be larger than the limit.
+ */
+ public int capacity() {
+ return data.length;
+ }
+
+ /**
+ * Sets the reading offset in the array.
+ *
+ * @param position Byte offset in the array from which to read.
+ * @throws IllegalArgumentException Thrown if the new position is neither in nor at the end of the
+ * array.
+ */
+ public void setPosition(int position) {
+ // It is fine for position to be at the end of the array.
+ Assertions.checkArgument(position >= 0 && position <= limit);
+ this.position = position;
+ }
+
+ /**
+ * Moves the reading offset by {@code bytes}.
+ *
+ * @param bytes The number of bytes to skip.
+ * @throws IllegalArgumentException Thrown if the new position is neither in nor at the end of the
+ * array.
+ */
+ public void skipBytes(int bytes) {
+ setPosition(position + bytes);
+ }
+
+ /**
+ * Reads the next {@code length} bytes into {@code bitArray}, and resets the position of
+ * {@code bitArray} to zero.
+ *
+ * @param bitArray The {@link ParsableBitArray} into which the bytes should be read.
+ * @param length The number of bytes to write.
+ */
+ public void readBytes(ParsableBitArray bitArray, int length) {
+ readBytes(bitArray.data, 0, length);
+ bitArray.setPosition(0);
+ }
+
+ /**
+ * Reads the next {@code length} bytes into {@code buffer} at {@code offset}.
+ *
+ * @see System#arraycopy(Object, int, Object, int, int)
+ * @param buffer The array into which the read data should be written.
+ * @param offset The offset in {@code buffer} at which the read data should be written.
+ * @param length The number of bytes to read.
+ */
+ public void readBytes(byte[] buffer, int offset, int length) {
+ System.arraycopy(data, position, buffer, offset, length);
+ position += length;
+ }
+
+ /**
+ * Reads the next {@code length} bytes into {@code buffer}.
+ *
+ * @see ByteBuffer#put(byte[], int, int)
+ * @param buffer The {@link ByteBuffer} into which the read data should be written.
+ * @param length The number of bytes to read.
+ */
+ public void readBytes(ByteBuffer buffer, int length) {
+ buffer.put(data, position, length);
+ position += length;
+ }
+
+ /**
+ * Peeks at the next byte as an unsigned value.
+ */
+ public int peekUnsignedByte() {
+ return (data[position] & 0xFF);
+ }
+
+ /**
+ * Peeks at the next char.
+ */
+ public char peekChar() {
+ return (char) ((data[position] & 0xFF) << 8
+ | (data[position + 1] & 0xFF));
+ }
+
+ /**
+ * Reads the next byte as an unsigned value.
+ */
+ public int readUnsignedByte() {
+ return (data[position++] & 0xFF);
+ }
+
+ /**
+ * Reads the next two bytes as an unsigned value.
+ */
+ public int readUnsignedShort() {
+ return (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF);
+ }
+
+ /**
+ * Reads the next two bytes as an unsigned value.
+ */
+ public int readLittleEndianUnsignedShort() {
+ return (data[position++] & 0xFF) | (data[position++] & 0xFF) << 8;
+ }
+
+ /**
+ * Reads the next two bytes as a signed value.
+ */
+ public short readShort() {
+ return (short) ((data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF));
+ }
+
+ /**
+ * Reads the next two bytes as a signed value.
+ */
+ public short readLittleEndianShort() {
+ return (short) ((data[position++] & 0xFF) | (data[position++] & 0xFF) << 8);
+ }
+
+ /**
+ * Reads the next three bytes as an unsigned value.
+ */
+ public int readUnsignedInt24() {
+ return (data[position++] & 0xFF) << 16
+ | (data[position++] & 0xFF) << 8
+ | (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.
+ */
+ public int readLittleEndianInt24() {
+ return (data[position++] & 0xFF)
+ | (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF) << 16;
+ }
+
+ /**
+ * Reads the next three bytes as an unsigned value in little endian order.
+ */
+ public int readLittleEndianUnsignedInt24() {
+ return (data[position++] & 0xFF)
+ | (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF) << 16;
+ }
+
+ /**
+ * Reads the next four bytes as an unsigned value.
+ */
+ public long readUnsignedInt() {
+ return (data[position++] & 0xFFL) << 24
+ | (data[position++] & 0xFFL) << 16
+ | (data[position++] & 0xFFL) << 8
+ | (data[position++] & 0xFFL);
+ }
+
+ /**
+ * Reads the next four bytes as an unsigned value in little endian order.
+ */
+ public long readLittleEndianUnsignedInt() {
+ return (data[position++] & 0xFFL)
+ | (data[position++] & 0xFFL) << 8
+ | (data[position++] & 0xFFL) << 16
+ | (data[position++] & 0xFFL) << 24;
+ }
+
+ /**
+ * Reads the next four bytes as a signed value
+ */
+ public int readInt() {
+ return (data[position++] & 0xFF) << 24
+ | (data[position++] & 0xFF) << 16
+ | (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF);
+ }
+
+ /**
+ * Reads the next four bytes as a signed value in little endian order.
+ */
+ public int readLittleEndianInt() {
+ return (data[position++] & 0xFF)
+ | (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF) << 16
+ | (data[position++] & 0xFF) << 24;
+ }
+
+ /**
+ * Reads the next eight bytes as a signed value.
+ */
+ public long readLong() {
+ return (data[position++] & 0xFFL) << 56
+ | (data[position++] & 0xFFL) << 48
+ | (data[position++] & 0xFFL) << 40
+ | (data[position++] & 0xFFL) << 32
+ | (data[position++] & 0xFFL) << 24
+ | (data[position++] & 0xFFL) << 16
+ | (data[position++] & 0xFFL) << 8
+ | (data[position++] & 0xFFL);
+ }
+
+ /**
+ * Reads the next eight bytes as a signed value in little endian order.
+ */
+ public long readLittleEndianLong() {
+ return (data[position++] & 0xFFL)
+ | (data[position++] & 0xFFL) << 8
+ | (data[position++] & 0xFFL) << 16
+ | (data[position++] & 0xFFL) << 24
+ | (data[position++] & 0xFFL) << 32
+ | (data[position++] & 0xFFL) << 40
+ | (data[position++] & 0xFFL) << 48
+ | (data[position++] & 0xFFL) << 56;
+ }
+
+ /**
+ * Reads the next four bytes, returning the integer portion of the fixed point 16.16 integer.
+ */
+ public int readUnsignedFixedPoint1616() {
+ int result = (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF);
+ position += 2; // Skip the non-integer portion.
+ return result;
+ }
+
+ /**
+ * Reads a Synchsafe integer.
+ * <p>
+ * Synchsafe integers keep the highest bit of every byte zeroed. A 32 bit synchsafe integer can
+ * store 28 bits of information.
+ *
+ * @return The parsed value.
+ */
+ public int readSynchSafeInt() {
+ int b1 = readUnsignedByte();
+ int b2 = readUnsignedByte();
+ int b3 = readUnsignedByte();
+ int b4 = readUnsignedByte();
+ return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4;
+ }
+
+ /**
+ * Reads the next four bytes as an unsigned integer into an integer, if the top bit is a zero.
+ *
+ * @throws IllegalStateException Thrown if the top bit of the input data is set.
+ */
+ public int readUnsignedIntToInt() {
+ int result = readInt();
+ if (result < 0) {
+ throw new IllegalStateException("Top bit not zero: " + result);
+ }
+ return result;
+ }
+
+ /**
+ * Reads the next four bytes as a little endian unsigned integer into an integer, if the top bit
+ * is a zero.
+ *
+ * @throws IllegalStateException Thrown if the top bit of the input data is set.
+ */
+ public int readLittleEndianUnsignedIntToInt() {
+ int result = readLittleEndianInt();
+ if (result < 0) {
+ throw new IllegalStateException("Top bit not zero: " + result);
+ }
+ return result;
+ }
+
+ /**
+ * Reads the next eight bytes as an unsigned long into a long, if the top bit is a zero.
+ *
+ * @throws IllegalStateException Thrown if the top bit of the input data is set.
+ */
+ public long readUnsignedLongToLong() {
+ long result = readLong();
+ if (result < 0) {
+ throw new IllegalStateException("Top bit not zero: " + result);
+ }
+ return result;
+ }
+
+ /**
+ * Reads the next four bytes as a 32-bit floating point value.
+ */
+ public float readFloat() {
+ return Float.intBitsToFloat(readInt());
+ }
+
+ /**
+ * Reads the next eight bytes as a 64-bit floating point value.
+ */
+ public double readDouble() {
+ return Double.longBitsToDouble(readLong());
+ }
+
+ /**
+ * Reads the next {@code length} bytes as UTF-8 characters.
+ *
+ * @param length The number of bytes to read.
+ * @return The string encoded by the bytes.
+ */
+ public String readString(int length) {
+ return readString(length, Charset.forName(C.UTF8_NAME));
+ }
+
+ /**
+ * Reads the next {@code length} bytes as characters in the specified {@link Charset}.
+ *
+ * @param length The number of bytes to read.
+ * @param charset The character set of the encoded characters.
+ * @return The string encoded by the bytes in the specified character set.
+ */
+ public String readString(int length, Charset charset) {
+ String result = new String(data, position, length, charset);
+ position += length;
+ return result;
+ }
+
+ /**
+ * Reads the next {@code length} bytes as UTF-8 characters. A terminating NUL byte is discarded,
+ * if present.
+ *
+ * @param length The number of bytes to read.
+ * @return The string, not including any terminating NUL byte.
+ */
+ public String readNullTerminatedString(int length) {
+ if (length == 0) {
+ return "";
+ }
+ int stringLength = length;
+ int lastIndex = position + length - 1;
+ if (lastIndex < limit && data[lastIndex] == 0) {
+ stringLength--;
+ }
+ String result = Util.fromUtf8Bytes(data, position, stringLength);
+ position += length;
+ return result;
+ }
+
+ /**
+ * Reads up to the next NUL byte (or the limit) as UTF-8 characters.
+ *
+ * @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;
+ }
+ int stringLimit = position;
+ while (stringLimit < limit && data[stringLimit] != 0) {
+ stringLimit++;
+ }
+ String string = Util.fromUtf8Bytes(data, position, stringLimit - position);
+ position = stringLimit;
+ if (position < limit) {
+ position++;
+ }
+ return string;
+ }
+
+ /**
+ * Reads a line of text.
+ *
+ * <p>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. 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;
+ }
+ int lineLimit = position;
+ while (lineLimit < limit && !Util.isLinebreak(data[lineLimit])) {
+ lineLimit++;
+ }
+ if (lineLimit - position >= 3 && data[position] == (byte) 0xEF
+ && data[position + 1] == (byte) 0xBB && data[position + 2] == (byte) 0xBF) {
+ // There's a UTF-8 byte order mark at the start of the line. Discard it.
+ position += 3;
+ }
+ String line = Util.fromUtf8Bytes(data, position, lineLimit - position);
+ position = lineLimit;
+ if (position == limit) {
+ return line;
+ }
+ if (data[position] == '\r') {
+ position++;
+ if (position == limit) {
+ return line;
+ }
+ }
+ if (data[position] == '\n') {
+ position++;
+ }
+ return line;
+ }
+
+ /**
+ * Reads a long value encoded by UTF-8 encoding
+ *
+ * @throws NumberFormatException if there is a problem with decoding
+ * @return Decoded long value
+ */
+ public long readUtf8EncodedLong() {
+ int length = 0;
+ long value = data[position];
+ // find the high most 0 bit
+ for (int j = 7; j >= 0; j--) {
+ if ((value & (1 << j)) == 0) {
+ if (j < 6) {
+ value &= (1 << j) - 1;
+ length = 7 - j;
+ } else if (j == 7) {
+ length = 1;
+ }
+ break;
+ }
+ }
+ if (length == 0) {
+ throw new NumberFormatException("Invalid UTF-8 sequence first byte: " + value);
+ }
+ for (int i = 1; i < length; i++) {
+ int x = data[position + i];
+ if ((x & 0xC0) != 0x80) { // if the high most 0 bit not 7th
+ throw new NumberFormatException("Invalid UTF-8 sequence continuation byte: " + value);
+ }
+ value = (value << 6) | (x & 0x3F);
+ }
+ position += length;
+ return value;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java
new file mode 100644
index 0000000000..e73404fd91
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java
@@ -0,0 +1,211 @@
+/*
+ * 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;
+
+/**
+ * Wraps a byte array, providing methods that allow it to be read as a NAL unit bitstream.
+ * <p>
+ * Whenever the byte sequence [0, 0, 3] appears in the wrapped byte array, it is treated as [0, 0]
+ * for all reading/skipping operations, which makes the bitstream appear to be unescaped.
+ */
+public final class ParsableNalUnitBitArray {
+
+ private byte[] data;
+ private int byteLimit;
+
+ // The byte offset is never equal to the offset of the 3rd byte in a subsequence [0, 0, 3].
+ private int byteOffset;
+ private int bitOffset;
+
+ /**
+ * @param data The data to wrap.
+ * @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);
+ }
+
+ /**
+ * Resets the wrapped data, limit and offset.
+ *
+ * @param data The data to wrap.
+ * @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}.
+ */
+ public void reset(byte[] data, int offset, int limit) {
+ this.data = data;
+ byteOffset = offset;
+ byteLimit = limit;
+ bitOffset = 0;
+ 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 numBits The number of bits to skip.
+ */
+ public void skipBits(int numBits) {
+ int oldByteOffset = byteOffset;
+ int numBytes = numBits / 8;
+ byteOffset += numBytes;
+ bitOffset += numBits - (numBytes * 8);
+ if (bitOffset > 7) {
+ byteOffset++;
+ bitOffset -= 8;
+ }
+ for (int i = oldByteOffset + 1; i <= byteOffset; i++) {
+ if (shouldSkipByte(i)) {
+ // Skip the byte and move forward to check three bytes ahead.
+ byteOffset++;
+ i += 2;
+ }
+ }
+ assertValidOffset();
+ }
+
+ /**
+ * Returns whether it's possible to read {@code n} bits starting from the current offset. The
+ * offset is not modified.
+ *
+ * @param numBits The number of bits.
+ * @return Whether it is possible to read {@code n} bits.
+ */
+ public boolean canReadBits(int numBits) {
+ int oldByteOffset = byteOffset;
+ int numBytes = numBits / 8;
+ int newByteOffset = byteOffset + numBytes;
+ int newBitOffset = bitOffset + numBits - (numBytes * 8);
+ if (newBitOffset > 7) {
+ newByteOffset++;
+ newBitOffset -= 8;
+ }
+ for (int i = oldByteOffset + 1; i <= newByteOffset && newByteOffset < byteLimit; i++) {
+ if (shouldSkipByte(i)) {
+ // Skip the byte and move forward to check three bytes ahead.
+ newByteOffset++;
+ i += 2;
+ }
+ }
+ return newByteOffset < byteLimit || (newByteOffset == byteLimit && newBitOffset == 0);
+ }
+
+ /**
+ * Reads a single bit.
+ *
+ * @return Whether the bit is set.
+ */
+ public boolean readBit() {
+ 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.
+ */
+ public int readBits(int numBits) {
+ int returnValue = 0;
+ bitOffset += numBits;
+ while (bitOffset > 8) {
+ bitOffset -= 8;
+ returnValue |= (data[byteOffset] & 0xFF) << bitOffset;
+ byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1;
+ }
+ returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset);
+ returnValue &= 0xFFFFFFFF >>> (32 - numBits);
+ if (bitOffset == 8) {
+ bitOffset = 0;
+ byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1;
+ }
+ assertValidOffset();
+ return returnValue;
+ }
+
+ /**
+ * Returns whether it is possible to read an Exp-Golomb-coded integer starting from the current
+ * offset. The offset is not modified.
+ *
+ * @return Whether it is possible to read an Exp-Golomb-coded integer.
+ */
+ public boolean canReadExpGolombCodedNum() {
+ int initialByteOffset = byteOffset;
+ int initialBitOffset = bitOffset;
+ int leadingZeros = 0;
+ while (byteOffset < byteLimit && !readBit()) {
+ leadingZeros++;
+ }
+ boolean hitLimit = byteOffset == byteLimit;
+ byteOffset = initialByteOffset;
+ bitOffset = initialBitOffset;
+ return !hitLimit && canReadBits(leadingZeros * 2 + 1);
+ }
+
+ /**
+ * Reads an unsigned Exp-Golomb-coded format integer.
+ *
+ * @return The value of the parsed Exp-Golomb-coded integer.
+ */
+ public int readUnsignedExpGolombCodedInt() {
+ return readExpGolombCodeNum();
+ }
+
+ /**
+ * Reads an signed Exp-Golomb-coded format integer.
+ *
+ * @return The value of the parsed Exp-Golomb-coded integer.
+ */
+ public int readSignedExpGolombCodedInt() {
+ int codeNum = readExpGolombCodeNum();
+ return ((codeNum % 2) == 0 ? -1 : 1) * ((codeNum + 1) / 2);
+ }
+
+ private int readExpGolombCodeNum() {
+ int leadingZeros = 0;
+ while (!readBit()) {
+ leadingZeros++;
+ }
+ return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0);
+ }
+
+ private boolean shouldSkipByte(int offset) {
+ return 2 <= offset && offset < byteLimit && data[offset] == (byte) 0x03
+ && data[offset - 2] == (byte) 0x00 && data[offset - 1] == (byte) 0x00;
+ }
+
+ 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Predicate.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Predicate.java
new file mode 100644
index 0000000000..d91d9f7254
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Predicate.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * Determines a true or false value for a given input.
+ *
+ * @param <T> The input type of the predicate.
+ */
+public interface Predicate<T> {
+
+ /**
+ * Evaluates an input.
+ *
+ * @param input The input to evaluate.
+ * @return The evaluated result.
+ */
+ boolean evaluate(T input);
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/PriorityTaskManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/PriorityTaskManager.java
new file mode 100644
index 0000000000..1067014b40
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/PriorityTaskManager.java
@@ -0,0 +1,119 @@
+/*
+ * 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 java.io.IOException;
+import java.util.Collections;
+import java.util.PriorityQueue;
+
+/**
+ * Allows tasks with associated priorities to control how they proceed relative to one another.
+ * <p>
+ * A task should call {@link #add(int)} to register with the manager and {@link #remove(int)} to
+ * unregister. A registered task will prevent tasks of lower priority from proceeding, and should
+ * call {@link #proceed(int)}, {@link #proceedNonBlocking(int)} or {@link #proceedOrThrow(int)} each
+ * time it wishes to check whether it is itself allowed to proceed.
+ */
+public final class PriorityTaskManager {
+
+ /**
+ * Thrown when task attempts to proceed when another registered task has a higher priority.
+ */
+ public static class PriorityTooLowException extends IOException {
+
+ public PriorityTooLowException(int priority, int highestPriority) {
+ super("Priority too low [priority=" + priority + ", highest=" + highestPriority + "]");
+ }
+
+ }
+
+ private final Object lock = new Object();
+
+ // Guarded by lock.
+ private final PriorityQueue<Integer> queue;
+ private int highestPriority;
+
+ public PriorityTaskManager() {
+ queue = new PriorityQueue<>(10, Collections.reverseOrder());
+ highestPriority = Integer.MIN_VALUE;
+ }
+
+ /**
+ * Register a new task. The task must call {@link #remove(int)} when done.
+ *
+ * @param priority The priority of the task. Larger values indicate higher priorities.
+ */
+ public void add(int priority) {
+ synchronized (lock) {
+ queue.add(priority);
+ highestPriority = Math.max(highestPriority, priority);
+ }
+ }
+
+ /**
+ * Blocks until the task is allowed to proceed.
+ *
+ * @param priority The priority of the task.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ public void proceed(int priority) throws InterruptedException {
+ synchronized (lock) {
+ while (highestPriority != priority) {
+ lock.wait();
+ }
+ }
+ }
+
+ /**
+ * A non-blocking variant of {@link #proceed(int)}.
+ *
+ * @param priority The priority of the task.
+ * @return Whether the task is allowed to proceed.
+ */
+ public boolean proceedNonBlocking(int priority) {
+ synchronized (lock) {
+ return highestPriority == priority;
+ }
+ }
+
+ /**
+ * A throwing variant of {@link #proceed(int)}.
+ *
+ * @param priority The priority of the task.
+ * @throws PriorityTooLowException If the task is not allowed to proceed.
+ */
+ public void proceedOrThrow(int priority) throws PriorityTooLowException {
+ synchronized (lock) {
+ if (highestPriority != priority) {
+ throw new PriorityTooLowException(priority, highestPriority);
+ }
+ }
+ }
+
+ /**
+ * Unregister a task.
+ *
+ * @param priority The priority of the task.
+ */
+ public void remove(int priority) {
+ synchronized (lock) {
+ queue.remove(priority);
+ highestPriority = queue.isEmpty() ? Integer.MIN_VALUE : Util.castNonNull(queue.peek());
+ lock.notifyAll();
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/RepeatModeUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/RepeatModeUtil.java
new file mode 100644
index 0000000000..c4964e6848
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java
new file mode 100644
index 0000000000..cd38892be0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java
@@ -0,0 +1,73 @@
+/*
+ * 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 java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * This is a subclass of {@link BufferedOutputStream} with a {@link #reset(OutputStream)} method
+ * that allows an instance to be re-used with another underlying output stream.
+ */
+public final class ReusableBufferedOutputStream extends BufferedOutputStream {
+
+ private boolean closed;
+
+ public ReusableBufferedOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ public ReusableBufferedOutputStream(OutputStream out, int size) {
+ super(out, size);
+ }
+
+ @Override
+ public void close() throws IOException {
+ closed = true;
+
+ Throwable thrown = null;
+ try {
+ flush();
+ } catch (Throwable e) {
+ thrown = e;
+ }
+ try {
+ out.close();
+ } catch (Throwable e) {
+ if (thrown == null) {
+ thrown = e;
+ }
+ }
+ if (thrown != null) {
+ Util.sneakyThrow(thrown);
+ }
+ }
+
+ /**
+ * Resets this stream and uses the given output stream for writing. This stream must be closed
+ * before resetting.
+ *
+ * @param out New output stream to be used for writing.
+ * @throws IllegalStateException If the stream isn't closed.
+ */
+ public void reset(OutputStream out) {
+ Assertions.checkState(closed);
+ this.out = out;
+ count = 0;
+ closed = false;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SlidingPercentile.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SlidingPercentile.java
new file mode 100644
index 0000000000..9048de2f34
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SlidingPercentile.java
@@ -0,0 +1,158 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * Calculate any percentile over a sliding window of weighted values. A maximum weight is
+ * configured. Once the total weight of the values reaches the maximum weight, the oldest value is
+ * reduced in weight until it reaches zero and is removed. This maintains a constant total weight,
+ * equal to the maximum allowed, at the steady state.
+ * <p>
+ * This class can be used for bandwidth estimation based on a sliding window of past transfer rate
+ * observations. This is an alternative to sliding mean and exponential averaging which suffer from
+ * susceptibility to outliers and slow adaptation to step functions.
+ *
+ * @see <a href="http://en.wikipedia.org/wiki/Moving_average">Wiki: Moving average</a>
+ * @see <a href="http://en.wikipedia.org/wiki/Selection_algorithm">Wiki: Selection algorithm</a>
+ */
+public class SlidingPercentile {
+
+ // Orderings.
+ private static final Comparator<Sample> INDEX_COMPARATOR = (a, b) -> a.index - b.index;
+ private static final Comparator<Sample> 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;
+ private static final int SORT_ORDER_BY_INDEX = 1;
+
+ private static final int MAX_RECYCLED_SAMPLES = 5;
+
+ private final int maxWeight;
+ private final ArrayList<Sample> samples;
+
+ private final Sample[] recycledSamples;
+
+ private int currentSortOrder;
+ private int nextSampleIndex;
+ private int totalWeight;
+ private int recycledSampleCount;
+
+ /**
+ * @param maxWeight The maximum weight.
+ */
+ public SlidingPercentile(int maxWeight) {
+ this.maxWeight = maxWeight;
+ recycledSamples = new Sample[MAX_RECYCLED_SAMPLES];
+ samples = new ArrayList<>();
+ 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.
+ *
+ * @param weight The weight of the new observation.
+ * @param value The value of the new observation.
+ */
+ public void addSample(int weight, float value) {
+ ensureSortedByIndex();
+
+ Sample newSample = recycledSampleCount > 0 ? recycledSamples[--recycledSampleCount]
+ : new Sample();
+ newSample.index = nextSampleIndex++;
+ newSample.weight = weight;
+ newSample.value = value;
+ samples.add(newSample);
+ totalWeight += weight;
+
+ while (totalWeight > maxWeight) {
+ int excessWeight = totalWeight - maxWeight;
+ Sample oldestSample = samples.get(0);
+ if (oldestSample.weight <= excessWeight) {
+ totalWeight -= oldestSample.weight;
+ samples.remove(0);
+ if (recycledSampleCount < MAX_RECYCLED_SAMPLES) {
+ recycledSamples[recycledSampleCount++] = oldestSample;
+ }
+ } else {
+ oldestSample.weight -= excessWeight;
+ totalWeight -= excessWeight;
+ }
+ }
+ }
+
+ /**
+ * Computes a percentile by integration.
+ *
+ * @param percentile The desired percentile, expressed as a fraction in the range (0,1].
+ * @return The requested percentile value or {@link Float#NaN} if no samples have been added.
+ */
+ public float getPercentile(float percentile) {
+ ensureSortedByValue();
+ float desiredWeight = percentile * totalWeight;
+ int accumulatedWeight = 0;
+ for (int i = 0; i < samples.size(); i++) {
+ Sample currentSample = samples.get(i);
+ accumulatedWeight += currentSample.weight;
+ if (accumulatedWeight >= desiredWeight) {
+ return currentSample.value;
+ }
+ }
+ // Clamp to maximum value or NaN if no values.
+ return samples.isEmpty() ? Float.NaN : samples.get(samples.size() - 1).value;
+ }
+
+ /**
+ * Sorts the samples by index.
+ */
+ private void ensureSortedByIndex() {
+ if (currentSortOrder != SORT_ORDER_BY_INDEX) {
+ Collections.sort(samples, INDEX_COMPARATOR);
+ currentSortOrder = SORT_ORDER_BY_INDEX;
+ }
+ }
+
+ /**
+ * Sorts the samples by value.
+ */
+ private void ensureSortedByValue() {
+ if (currentSortOrder != SORT_ORDER_BY_VALUE) {
+ Collections.sort(samples, VALUE_COMPARATOR);
+ currentSortOrder = SORT_ORDER_BY_VALUE;
+ }
+ }
+
+ private static class Sample {
+
+ public int index;
+ public int weight;
+ public float value;
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/StandaloneMediaClock.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/StandaloneMediaClock.java
new file mode 100644
index 0000000000..f72867694d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/StandaloneMediaClock.java
@@ -0,0 +1,104 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters;
+
+/**
+ * A {@link MediaClock} whose position advances with real time based on the playback parameters when
+ * started.
+ */
+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 using the given {@link Clock} implementation.
+ *
+ * @param clock A {@link Clock}.
+ */
+ public StandaloneMediaClock(Clock clock) {
+ this.clock = clock;
+ this.playbackParameters = PlaybackParameters.DEFAULT;
+ }
+
+ /**
+ * Starts the clock. Does nothing if the clock is already started.
+ */
+ public void start() {
+ if (!started) {
+ baseElapsedMs = clock.elapsedRealtime();
+ started = true;
+ }
+ }
+
+ /**
+ * Stops the clock. Does nothing if the clock is already stopped.
+ */
+ public void stop() {
+ if (started) {
+ resetPosition(getPositionUs());
+ started = false;
+ }
+ }
+
+ /**
+ * Resets the clock's position.
+ *
+ * @param positionUs The position to set in microseconds.
+ */
+ public void resetPosition(long positionUs) {
+ baseUs = positionUs;
+ if (started) {
+ baseElapsedMs = clock.elapsedRealtime();
+ }
+ }
+
+ @Override
+ public long getPositionUs() {
+ long positionUs = baseUs;
+ if (started) {
+ long elapsedSinceBaseMs = clock.elapsedRealtime() - baseElapsedMs;
+ if (playbackParameters.speed == 1f) {
+ positionUs += C.msToUs(elapsedSinceBaseMs);
+ } else {
+ positionUs += playbackParameters.getMediaTimeUsForPlayoutTimeMs(elapsedSinceBaseMs);
+ }
+ }
+ return positionUs;
+ }
+
+ @Override
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {
+ // Store the current position as the new base, in case the playback speed has changed.
+ if (started) {
+ resetPosition(getPositionUs());
+ }
+ this.playbackParameters = playbackParameters;
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return playbackParameters;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemClock.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemClock.java
new file mode 100644
index 0000000000..a2f915866d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemClock.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2014 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.Handler.Callback;
+import android.os.Looper;
+import androidx.annotation.Nullable;
+
+/**
+ * The standard implementation of {@link 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemHandlerWrapper.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemHandlerWrapper.java
new file mode 100644
index 0000000000..e69a24cc10
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimedValueQueue.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimedValueQueue.java
new file mode 100644
index 0000000000..396e50dcff
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<V> {
+ 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> V[] newArray(int length) {
+ return (V[]) new Object[length];
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimestampAdjuster.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimestampAdjuster.java
new file mode 100644
index 0000000000..e824251282
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimestampAdjuster.java
@@ -0,0 +1,186 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+
+/**
+ * Offsets timestamps according to an initial sample timestamp offset. MPEG-2 TS timestamps scaling
+ * and adjustment is supported, taking into account timestamp rollover.
+ */
+public final class TimestampAdjuster {
+
+ /**
+ * A special {@code firstSampleTimestampUs} value indicating that presentation timestamps should
+ * not be offset.
+ */
+ public static final long DO_NOT_OFFSET = Long.MAX_VALUE;
+
+ /**
+ * 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;
+
+ private long firstSampleTimestampUs;
+ private long timestampOffsetUs;
+
+ // Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp.
+ private volatile long lastSampleTimestampUs;
+
+ /**
+ * @param firstSampleTimestampUs See {@link #setFirstSampleTimestampUs(long)}.
+ */
+ public TimestampAdjuster(long firstSampleTimestampUs) {
+ lastSampleTimestampUs = C.TIME_UNSET;
+ setFirstSampleTimestampUs(firstSampleTimestampUs);
+ }
+
+ /**
+ * Sets the desired result of the first call to {@link #adjustSampleTimestamp(long)}. Can only be
+ * called before any timestamps have been adjusted.
+ *
+ * @param firstSampleTimestampUs The first adjusted sample timestamp in microseconds, or
+ * {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset.
+ */
+ public synchronized void setFirstSampleTimestampUs(long firstSampleTimestampUs) {
+ Assertions.checkState(lastSampleTimestampUs == C.TIME_UNSET);
+ this.firstSampleTimestampUs = firstSampleTimestampUs;
+ }
+
+ /** Returns the last value passed to {@link #setFirstSampleTimestampUs(long)}. */
+ public long getFirstSampleTimestampUs() {
+ return firstSampleTimestampUs;
+ }
+
+ /**
+ * 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 lastSampleTimestampUs != C.TIME_UNSET
+ ? (lastSampleTimestampUs + timestampOffsetUs)
+ : firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET;
+ }
+
+ /**
+ * Returns the offset between the input of {@link #adjustSampleTimestamp(long)} and its output.
+ * If {@link #DO_NOT_OFFSET} was provided to the constructor, 0 is returned. If the timestamp
+ * adjuster is yet not initialized, {@link C#TIME_UNSET} is returned.
+ *
+ * @return The offset between {@link #adjustSampleTimestamp(long)}'s input and output.
+ * {@link C#TIME_UNSET} if the adjuster is not yet initialized and 0 if timestamps should not
+ * be offset.
+ */
+ public long getTimestampOffsetUs() {
+ return firstSampleTimestampUs == DO_NOT_OFFSET
+ ? 0
+ : lastSampleTimestampUs == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;
+ }
+
+ /**
+ * Resets the instance to its initial state.
+ */
+ public void reset() {
+ lastSampleTimestampUs = C.TIME_UNSET;
+ }
+
+ /**
+ * Scales and offsets an MPEG-2 TS presentation timestamp considering wraparound.
+ *
+ * @param pts90Khz A 90 kHz clock MPEG-2 TS presentation timestamp.
+ * @return The adjusted timestamp in microseconds.
+ */
+ public long adjustTsTimestamp(long pts90Khz) {
+ if (pts90Khz == C.TIME_UNSET) {
+ return 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 lastSampleTimestampUs.
+ long lastPts = usToPts(lastSampleTimestampUs);
+ long closestWrapCount = (lastPts + (MAX_PTS_PLUS_ONE / 2)) / MAX_PTS_PLUS_ONE;
+ 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(pts90Khz));
+ }
+
+ /**
+ * Offsets a timestamp in microseconds.
+ *
+ * @param timeUs The timestamp to adjust in microseconds.
+ * @return The adjusted timestamp in microseconds.
+ */
+ public long adjustSampleTimestamp(long timeUs) {
+ if (timeUs == C.TIME_UNSET) {
+ return C.TIME_UNSET;
+ }
+ // Record the adjusted PTS to adjust for wraparound next time.
+ if (lastSampleTimestampUs != C.TIME_UNSET) {
+ lastSampleTimestampUs = timeUs;
+ } else {
+ if (firstSampleTimestampUs != DO_NOT_OFFSET) {
+ // Calculate the timestamp offset.
+ timestampOffsetUs = firstSampleTimestampUs - timeUs;
+ }
+ synchronized (this) {
+ lastSampleTimestampUs = timeUs;
+ // Notify threads waiting for this adjuster to be initialized.
+ notifyAll();
+ }
+ }
+ return timeUs + timestampOffsetUs;
+ }
+
+ /**
+ * Blocks the calling thread until this adjuster is initialized.
+ *
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ public synchronized void waitUntilInitialized() throws InterruptedException {
+ while (lastSampleTimestampUs == C.TIME_UNSET) {
+ wait();
+ }
+ }
+
+ /**
+ * Converts a 90 kHz clock timestamp to a timestamp in microseconds.
+ *
+ * @param pts A 90 kHz clock timestamp.
+ * @return The corresponding value in microseconds.
+ */
+ public static long ptsToUs(long pts) {
+ return (pts * C.MICROS_PER_SECOND) / 90000;
+ }
+
+ /**
+ * Converts a timestamp in microseconds to a 90 kHz clock timestamp.
+ *
+ * @param us A value in microseconds.
+ * @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TraceUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TraceUtil.java
new file mode 100644
index 0000000000..5f53c3130d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TraceUtil.java
@@ -0,0 +1,62 @@
+/*
+ * 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.annotation.TargetApi;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+
+/**
+ * Calls through to {@link android.os.Trace} methods on supported API levels.
+ */
+public final class TraceUtil {
+
+ private TraceUtil() {}
+
+ /**
+ * Writes a trace message to indicate that a given section of code has begun.
+ *
+ * @see android.os.Trace#beginSection(String)
+ * @param sectionName The name of the code section to appear in the trace. This may be at most 127
+ * Unicode code units long.
+ */
+ public static void beginSection(String sectionName) {
+ if (ExoPlayerLibraryInfo.TRACE_ENABLED && Util.SDK_INT >= 18) {
+ beginSectionV18(sectionName);
+ }
+ }
+
+ /**
+ * Writes a trace message to indicate that a given section of code has ended.
+ *
+ * @see android.os.Trace#endSection()
+ */
+ public static void endSection() {
+ if (ExoPlayerLibraryInfo.TRACE_ENABLED && Util.SDK_INT >= 18) {
+ endSectionV18();
+ }
+ }
+
+ @TargetApi(18)
+ private static void beginSectionV18(String sectionName) {
+ android.os.Trace.beginSection(sectionName);
+ }
+
+ @TargetApi(18)
+ private static void endSectionV18() {
+ android.os.Trace.endSection();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/UriUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/UriUtil.java
new file mode 100644
index 0000000000..03b5d26a51
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/UriUtil.java
@@ -0,0 +1,279 @@
+/*
+ * 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.net.Uri;
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+
+/**
+ * Utility methods for manipulating URIs.
+ */
+public final class UriUtil {
+
+ /**
+ * The length of arrays returned by {@link #getUriIndices(String)}.
+ */
+ private static final int INDEX_COUNT = 4;
+ /**
+ * An index into an array returned by {@link #getUriIndices(String)}.
+ * <p>
+ * The value at this position in the array is the index of the ':' after the scheme. Equals -1 if
+ * the URI is a relative reference (no scheme). The hier-part starts at (schemeColon + 1),
+ * including when the URI has no scheme.
+ */
+ private static final int SCHEME_COLON = 0;
+ /**
+ * An index into an array returned by {@link #getUriIndices(String)}.
+ * <p>
+ * The value at this position in the array is the index of the path part. Equals (schemeColon + 1)
+ * if no authority part, (schemeColon + 3) if the authority part consists of just "//", and
+ * (query) if no path part. The characters starting at this index can be "//" only if the
+ * authority part is non-empty (in this case the double-slash means the first segment is empty).
+ */
+ private static final int PATH = 1;
+ /**
+ * An index into an array returned by {@link #getUriIndices(String)}.
+ * <p>
+ * The value at this position in the array is the index of the query part, including the '?'
+ * before the query. Equals fragment if no query part, and (fragment - 1) if the query part is a
+ * single '?' with no data.
+ */
+ private static final int QUERY = 2;
+ /**
+ * An index into an array returned by {@link #getUriIndices(String)}.
+ * <p>
+ * The value at this position in the array is the index of the fragment part, including the '#'
+ * before the fragment. Equal to the length of the URI if no fragment part, and (length - 1) if
+ * the fragment part is a single '#' with no data.
+ */
+ private static final int FRAGMENT = 3;
+
+ private UriUtil() {}
+
+ /**
+ * Like {@link #resolve(String, String)}, but returns a {@link Uri} instead of a {@link String}.
+ *
+ * @param baseUri The base URI.
+ * @param referenceUri The reference URI to resolve.
+ */
+ 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}.
+ *
+ * <p>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(@Nullable String baseUri, @Nullable String referenceUri) {
+ StringBuilder uri = new StringBuilder();
+
+ // Map null onto empty string, to make the following logic simpler.
+ baseUri = baseUri == null ? "" : baseUri;
+ referenceUri = referenceUri == null ? "" : referenceUri;
+
+ int[] refIndices = getUriIndices(referenceUri);
+ if (refIndices[SCHEME_COLON] != -1) {
+ // The reference is absolute. The target Uri is the reference.
+ uri.append(referenceUri);
+ removeDotSegments(uri, refIndices[PATH], refIndices[QUERY]);
+ return uri.toString();
+ }
+
+ int[] baseIndices = getUriIndices(baseUri);
+ if (refIndices[FRAGMENT] == 0) {
+ // The reference is empty or contains just the fragment part, then the target Uri is the
+ // concatenation of the base Uri without its fragment, and the reference.
+ return uri.append(baseUri, 0, baseIndices[FRAGMENT]).append(referenceUri).toString();
+ }
+
+ if (refIndices[QUERY] == 0) {
+ // The reference starts with the query part. The target is the base up to (but excluding) the
+ // query, plus the reference.
+ return uri.append(baseUri, 0, baseIndices[QUERY]).append(referenceUri).toString();
+ }
+
+ if (refIndices[PATH] != 0) {
+ // The reference has authority. The target is the base scheme plus the reference.
+ int baseLimit = baseIndices[SCHEME_COLON] + 1;
+ uri.append(baseUri, 0, baseLimit).append(referenceUri);
+ return removeDotSegments(uri, baseLimit + refIndices[PATH], baseLimit + refIndices[QUERY]);
+ }
+
+ if (referenceUri.charAt(refIndices[PATH]) == '/') {
+ // The reference path is rooted. The target is the base scheme and authority (if any), plus
+ // the reference.
+ uri.append(baseUri, 0, baseIndices[PATH]).append(referenceUri);
+ return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY]);
+ }
+
+ // The target Uri is the concatenation of the base Uri up to (but excluding) the last segment,
+ // and the reference. This can be split into 2 cases:
+ if (baseIndices[SCHEME_COLON] + 2 < baseIndices[PATH]
+ && baseIndices[PATH] == baseIndices[QUERY]) {
+ // Case 1: The base hier-part is just the authority, with an empty path. An additional '/' is
+ // needed after the authority, before appending the reference.
+ uri.append(baseUri, 0, baseIndices[PATH]).append('/').append(referenceUri);
+ return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY] + 1);
+ } else {
+ // Case 2: Otherwise, find the last '/' in the base hier-part and append the reference after
+ // it. If base hier-part has no '/', it could only mean that it is completely empty or
+ // contains only one segment, in which case the whole hier-part is excluded and the reference
+ // is appended right after the base scheme colon without an added '/'.
+ int lastSlashIndex = baseUri.lastIndexOf('/', baseIndices[QUERY] - 1);
+ int baseLimit = lastSlashIndex == -1 ? baseIndices[PATH] : lastSlashIndex + 1;
+ uri.append(baseUri, 0, baseLimit).append(referenceUri);
+ return removeDotSegments(uri, baseIndices[PATH], baseLimit + refIndices[QUERY]);
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @param uri A {@link StringBuilder} containing the URI.
+ * @param offset The index of the start of the path in {@code uri}.
+ * @param limit The limit (exclusive) of the path in {@code uri}.
+ */
+ private static String removeDotSegments(StringBuilder uri, int offset, int limit) {
+ if (offset >= limit) {
+ // Nothing to do.
+ return uri.toString();
+ }
+ if (uri.charAt(offset) == '/') {
+ // If the path starts with a /, always retain it.
+ offset++;
+ }
+ // The first character of the current path segment.
+ int segmentStart = offset;
+ int i = offset;
+ while (i <= limit) {
+ int nextSegmentStart;
+ if (i == limit) {
+ nextSegmentStart = i;
+ } else if (uri.charAt(i) == '/') {
+ nextSegmentStart = i + 1;
+ } else {
+ i++;
+ continue;
+ }
+ // We've encountered the end of a segment or the end of the path. If the final segment was
+ // "." or "..", remove the appropriate segments of the path.
+ if (i == segmentStart + 1 && uri.charAt(segmentStart) == '.') {
+ // Given "abc/def/./ghi", remove "./" to get "abc/def/ghi".
+ uri.delete(segmentStart, nextSegmentStart);
+ limit -= nextSegmentStart - segmentStart;
+ i = segmentStart;
+ } else if (i == segmentStart + 2 && uri.charAt(segmentStart) == '.'
+ && uri.charAt(segmentStart + 1) == '.') {
+ // Given "abc/def/../ghi", remove "def/../" to get "abc/ghi".
+ int prevSegmentStart = uri.lastIndexOf("/", segmentStart - 2) + 1;
+ int removeFrom = prevSegmentStart > offset ? prevSegmentStart : offset;
+ uri.delete(removeFrom, nextSegmentStart);
+ limit -= nextSegmentStart - removeFrom;
+ segmentStart = prevSegmentStart;
+ i = prevSegmentStart;
+ } else {
+ i++;
+ segmentStart = i;
+ }
+ }
+ return uri.toString();
+ }
+
+ /**
+ * Calculates indices of the constituent components of a URI.
+ *
+ * @param uriString The URI as a string.
+ * @return The corresponding indices.
+ */
+ private static int[] getUriIndices(String uriString) {
+ int[] indices = new int[INDEX_COUNT];
+ if (TextUtils.isEmpty(uriString)) {
+ indices[SCHEME_COLON] = -1;
+ return indices;
+ }
+
+ // Determine outer structure from right to left.
+ // Uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
+ int length = uriString.length();
+ int fragmentIndex = uriString.indexOf('#');
+ if (fragmentIndex == -1) {
+ fragmentIndex = length;
+ }
+ int queryIndex = uriString.indexOf('?');
+ if (queryIndex == -1 || queryIndex > fragmentIndex) {
+ // '#' before '?': '?' is within the fragment.
+ queryIndex = fragmentIndex;
+ }
+ // Slashes are allowed only in hier-part so any colon after the first slash is part of the
+ // hier-part, not the scheme colon separator.
+ int schemeIndexLimit = uriString.indexOf('/');
+ if (schemeIndexLimit == -1 || schemeIndexLimit > queryIndex) {
+ schemeIndexLimit = queryIndex;
+ }
+ int schemeIndex = uriString.indexOf(':');
+ if (schemeIndex > schemeIndexLimit) {
+ // '/' before ':'
+ schemeIndex = -1;
+ }
+
+ // Determine hier-part structure: hier-part = "//" authority path / path
+ // This block can also cope with schemeIndex == -1.
+ boolean hasAuthority = schemeIndex + 2 < queryIndex
+ && uriString.charAt(schemeIndex + 1) == '/'
+ && uriString.charAt(schemeIndex + 2) == '/';
+ int pathIndex;
+ if (hasAuthority) {
+ pathIndex = uriString.indexOf('/', schemeIndex + 3); // find first '/' after "://"
+ if (pathIndex == -1 || pathIndex > queryIndex) {
+ pathIndex = queryIndex;
+ }
+ } else {
+ pathIndex = schemeIndex + 1;
+ }
+
+ indices[SCHEME_COLON] = schemeIndex;
+ indices[PATH] = pathIndex;
+ indices[QUERY] = queryIndex;
+ indices[FRAGMENT] = fragmentIndex;
+ return indices;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Util.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Util.java
new file mode 100644
index 0000000000..4d7d8014dd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Util.java
@@ -0,0 +1,2298 @@
+/*
+ * 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 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.os.Handler;
+import android.os.Looper;
+import android.os.Parcel;
+import android.security.NetworkSecurityPolicy;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+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.video.VideoRendererEventListener;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+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.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.
+ */
+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;
+
+ /**
+ * Like {@link Build#DEVICE}, but in a place where it can be conveniently overridden for local
+ * testing.
+ */
+ public static final String DEVICE = Build.DEVICE;
+
+ /**
+ * Like {@link Build#MANUFACTURER}, but in a place where it can be conveniently overridden for
+ * local testing.
+ */
+ public static final String MANUFACTURER = Build.MANUFACTURER;
+
+ /**
+ * Like {@link Build#MODEL}, but in a place where it can be conveniently overridden for local
+ * testing.
+ */
+ public static final String MODEL = Build.MODEL;
+
+ /**
+ * A concise description of the device that it can be useful to log for debugging purposes.
+ */
+ 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)))?");
+ 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<String, String> 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.
+ * @return a byte array containing all of the inputStream's bytes.
+ * @throws IOException if an error occurs reading from the stream.
+ */
+ public static byte[] toByteArray(InputStream inputStream) throws IOException {
+ byte[] buffer = new byte[1024 * 4];
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ 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.
+ *
+ * @param activity The host activity for checking and requesting the permission.
+ * @param uris {@link Uri}s that may require {@link permission#READ_EXTERNAL_STORAGE} to read.
+ * @return Whether a permission request was made.
+ */
+ @TargetApi(23)
+ public static boolean maybeRequestReadExternalStoragePermission(Activity activity, Uri... uris) {
+ if (Util.SDK_INT < 23) {
+ return false;
+ }
+ for (Uri uri : uris) {
+ if (isLocalFileUri(uri)) {
+ if (activity.checkSelfPermission(permission.READ_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ activity.requestPermissions(new String[] {permission.READ_EXTERNAL_STORAGE}, 0);
+ return true;
+ }
+ break;
+ }
+ }
+ 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.
+ *
+ * @param uri The uri to test.
+ */
+ public static boolean isLocalFileUri(Uri uri) {
+ String scheme = uri.getScheme();
+ return TextUtils.isEmpty(scheme) || "file".equals(scheme);
+ }
+
+ /**
+ * Tests two objects for {@link Object#equals(Object)} equality, handling the case where one or
+ * both may be null.
+ *
+ * @param o1 The first object.
+ * @param o2 The second object.
+ * @return {@code o1 == null ? o2 == null : o1.equals(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)}.
+ *
+ * <p>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(@NullableType Object[] items, @Nullable Object item) {
+ for (Object arrayItem : items) {
+ if (areEqual(arrayItem, item)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Removes an indexed range from a List.
+ *
+ * <p>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} &lt; 0, {@code toIndex} &gt; {@code
+ * list.size()}, or {@code fromIndex} &gt; {@code toIndex}.
+ */
+ public static <T> void removeRange(List<T> 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.
+ *
+ * <p>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> 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> 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> 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> 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> 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> 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.
+ *
+ * <p>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.
+ *
+ * @param threadName The name of the thread.
+ * @return The executor.
+ */
+ public static ExecutorService newSingleThreadExecutor(final String threadName) {
+ return Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, threadName));
+ }
+
+ /**
+ * Closes a {@link DataSource}, suppressing any {@link IOException} that may occur.
+ *
+ * @param dataSource The {@link DataSource} to close.
+ */
+ public static void closeQuietly(@Nullable DataSource dataSource) {
+ try {
+ if (dataSource != null) {
+ dataSource.close();
+ }
+ } catch (IOException e) {
+ // Ignore.
+ }
+ }
+
+ /**
+ * Closes a {@link Closeable}, suppressing any {@link IOException} that may occur. Both {@link
+ * java.io.OutputStream} and {@link InputStream} are {@code Closeable}.
+ *
+ * @param closeable The {@link Closeable} to close.
+ */
+ public static void closeQuietly(@Nullable Closeable closeable) {
+ try {
+ if (closeable != null) {
+ closeable.close();
+ }
+ } catch (IOException e) {
+ // Ignore.
+ }
+ }
+
+ /**
+ * 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 parcel The {@link Parcel} to read from.
+ * @return The read value.
+ */
+ 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}.
+ *
+ * <p>For API levels &ge; 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));
+ }
+
+ /**
+ * Returns a new byte array containing the code points of a {@link String} encoded using UTF-8.
+ *
+ * @param value The {@link String} whose bytes should be obtained.
+ * @return The code points encoding using UTF-8.
+ */
+ public static byte[] getUtf8Bytes(String value) {
+ 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);
+ }
+
+ /**
+ * Returns whether the given character is a carriage return ('\r') or a line feed ('\n').
+ *
+ * @param c The character.
+ * @return Whether the given character is a linebreak.
+ */
+ public static boolean isLinebreak(int c) {
+ return c == '\n' || c == '\r';
+ }
+
+ /**
+ * Converts text to lower case using {@link Locale#US}.
+ *
+ * @param text The text to convert.
+ * @return The lower case text, or null if {@code text} is null.
+ */
+ 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);
+ }
+
+ /**
+ * Divides a {@code numerator} by a {@code denominator}, returning the ceiled result.
+ *
+ * @param numerator The numerator to divide.
+ * @param denominator The denominator to divide by.
+ * @return The ceiled result of the division.
+ */
+ public static int ceilDivide(int numerator, int denominator) {
+ return (numerator + denominator - 1) / denominator;
+ }
+
+ /**
+ * Divides a {@code numerator} by a {@code denominator}, returning the ceiled result.
+ *
+ * @param numerator The numerator to divide.
+ * @param denominator The denominator to divide by.
+ * @return The ceiled result of the division.
+ */
+ public static long ceilDivide(long numerator, long denominator) {
+ return (numerator + denominator - 1) / denominator;
+ }
+
+ /**
+ * Constrains a value to the specified bounds.
+ *
+ * @param value The value to constrain.
+ * @param min The lower bound.
+ * @param max The upper bound.
+ * @return The constrained value {@code Math.max(min, Math.min(value, max))}.
+ */
+ public static int constrainValue(int value, int min, int max) {
+ return Math.max(min, Math.min(value, max));
+ }
+
+ /**
+ * Constrains a value to the specified bounds.
+ *
+ * @param value The value to constrain.
+ * @param min The lower bound.
+ * @param max The upper bound.
+ * @return The constrained value {@code Math.max(min, Math.min(value, max))}.
+ */
+ public static long constrainValue(long value, long min, long max) {
+ return Math.max(min, Math.min(value, max));
+ }
+
+ /**
+ * Constrains a value to the specified bounds.
+ *
+ * @param value The value to constrain.
+ * @param min The lower bound.
+ * @param max The upper bound.
+ * @return The constrained value {@code Math.max(min, Math.min(value, max))}.
+ */
+ public static float constrainValue(float value, float min, float max) {
+ 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}.
+ *
+ * <p>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.
+ *
+ * @param array The array to search.
+ * @param value The value being searched for.
+ * @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 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 array. If false then -1 will be returned.
+ * @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) {
+ int index = Arrays.binarySearch(array, value);
+ if (index < 0) {
+ index = -(index + 2);
+ } else {
+ 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 array} that is less than (or optionally
+ * equal to) a specified {@code value}.
+ * <p>
+ * 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.
+ *
+ * @param array The array to search.
+ * @param value The value being searched for.
+ * @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 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 array. If false then -1 will be returned.
+ * @return The index of the largest element in {@code array} that is less than (or optionally
+ * equal to) {@code value}.
+ */
+ public static int binarySearchFloor(long[] array, long value, boolean inclusive,
+ boolean stayInBounds) {
+ int index = Arrays.binarySearch(array, value);
+ if (index < 0) {
+ index = -(index + 2);
+ } else {
+ 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}.
+ *
+ * <p>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 <T> 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 <T extends Comparable<? super T>> int binarySearchFloor(
+ List<? extends Comparable<? super T>> 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++;
+ }
+ }
+ return stayInBounds ? Math.max(0, index) : index;
+ }
+
+ /**
+ * Returns the index of the smallest element in {@code array} that is greater than (or optionally
+ * equal to) a specified {@code value}.
+ *
+ * <p>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 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 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) {}
+ if (inclusive) {
+ index--;
+ }
+ }
+ return stayInBounds ? Math.min(array.length - 1, index) : index;
+ }
+
+ /**
+ * Returns the index of the smallest element in {@code array} that is greater than (or optionally
+ * equal to) a specified {@code value}.
+ *
+ * <p>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 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 binarySearchCeil(
+ long[] array, long value, boolean inclusive, boolean stayInBounds) {
+ int index = Arrays.binarySearch(array, value);
+ if (index < 0) {
+ index = ~index;
+ } else {
+ while (++index < array.length && array[index] == value) {}
+ if (inclusive) {
+ 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.
+ *
+ * <p>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 <T> 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 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.
+ * @return The index of the smallest element in {@code list} that is greater than (or optionally
+ * equal to) {@code value}.
+ */
+ public static <T extends Comparable<? super T>> int binarySearchCeil(
+ List<? extends Comparable<? super T>> 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) {}
+ if (inclusive) {
+ index--;
+ }
+ }
+ 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 &lt; right, or a positive value if left
+ * &gt; 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.
+ *
+ * @param value The attribute value to decode.
+ * @return The parsed duration in milliseconds.
+ */
+ public static long parseXsDuration(String value) {
+ Matcher matcher = XS_DURATION_PATTERN.matcher(value);
+ if (matcher.matches()) {
+ boolean negated = !TextUtils.isEmpty(matcher.group(1));
+ // Durations containing years and months aren't completely defined. We assume there are
+ // 30.4368 days in a month, and 365.242 days in a year.
+ String years = matcher.group(3);
+ double durationSeconds = (years != null) ? Double.parseDouble(years) * 31556908 : 0;
+ String months = matcher.group(5);
+ durationSeconds += (months != null) ? Double.parseDouble(months) * 2629739 : 0;
+ String days = matcher.group(7);
+ durationSeconds += (days != null) ? Double.parseDouble(days) * 86400 : 0;
+ String hours = matcher.group(10);
+ durationSeconds += (hours != null) ? Double.parseDouble(hours) * 3600 : 0;
+ String minutes = matcher.group(12);
+ durationSeconds += (minutes != null) ? Double.parseDouble(minutes) * 60 : 0;
+ String seconds = matcher.group(14);
+ durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0;
+ long durationMillis = (long) (durationSeconds * 1000);
+ return negated ? -durationMillis : durationMillis;
+ } else {
+ return (long) (Double.parseDouble(value) * 3600 * 1000);
+ }
+ }
+
+ /**
+ * Parses an xs:dateTime attribute value, returning the parsed timestamp in milliseconds since
+ * the epoch.
+ *
+ * @param value The attribute value to decode.
+ * @return The parsed timestamp in milliseconds since the epoch.
+ * @throws ParserException if an error occurs parsing the dateTime attribute value.
+ */
+ public static long parseXsDateTime(String value) throws ParserException {
+ Matcher matcher = XS_DATE_TIME_PATTERN.matcher(value);
+ if (!matcher.matches()) {
+ throw new ParserException("Invalid date/time format: " + value);
+ }
+
+ int timezoneShift;
+ if (matcher.group(9) == null) {
+ // No time zone specified.
+ timezoneShift = 0;
+ } else if (matcher.group(9).equalsIgnoreCase("Z")) {
+ timezoneShift = 0;
+ } else {
+ timezoneShift = ((Integer.parseInt(matcher.group(12)) * 60
+ + Integer.parseInt(matcher.group(13))));
+ if ("-".equals(matcher.group(11))) {
+ timezoneShift *= -1;
+ }
+ }
+
+ Calendar dateTime = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
+
+ dateTime.clear();
+ // Note: The month value is 0-based, hence the -1 on group(2)
+ dateTime.set(Integer.parseInt(matcher.group(1)),
+ Integer.parseInt(matcher.group(2)) - 1,
+ Integer.parseInt(matcher.group(3)),
+ Integer.parseInt(matcher.group(4)),
+ Integer.parseInt(matcher.group(5)),
+ Integer.parseInt(matcher.group(6)));
+ if (!TextUtils.isEmpty(matcher.group(8))) {
+ final BigDecimal bd = new BigDecimal("0." + matcher.group(8));
+ // we care only for milliseconds, so movePointRight(3)
+ dateTime.set(Calendar.MILLISECOND, bd.movePointRight(3).intValue());
+ }
+
+ long time = dateTime.getTimeInMillis();
+ if (timezoneShift != 0) {
+ time -= timezoneShift * 60000;
+ }
+
+ return time;
+ }
+
+ /**
+ * Scales a large timestamp.
+ * <p>
+ * Logically, scaling consists of a multiplication followed by a division. The actual operations
+ * performed are designed to minimize the probability of overflow.
+ *
+ * @param timestamp The timestamp to scale.
+ * @param multiplier The multiplier.
+ * @param divisor The divisor.
+ * @return The scaled timestamp.
+ */
+ public static long scaleLargeTimestamp(long timestamp, long multiplier, long divisor) {
+ if (divisor >= multiplier && (divisor % multiplier) == 0) {
+ long divisionFactor = divisor / multiplier;
+ return timestamp / divisionFactor;
+ } else if (divisor < multiplier && (multiplier % divisor) == 0) {
+ long multiplicationFactor = multiplier / divisor;
+ return timestamp * multiplicationFactor;
+ } else {
+ double multiplicationFactor = (double) multiplier / divisor;
+ return (long) (timestamp * multiplicationFactor);
+ }
+ }
+
+ /**
+ * Applies {@link #scaleLargeTimestamp(long, long, long)} to a list of unscaled timestamps.
+ *
+ * @param timestamps The timestamps to scale.
+ * @param multiplier The multiplier.
+ * @param divisor The divisor.
+ * @return The scaled timestamps.
+ */
+ public static long[] scaleLargeTimestamps(List<Long> timestamps, long multiplier, long divisor) {
+ long[] scaledTimestamps = new long[timestamps.size()];
+ if (divisor >= multiplier && (divisor % multiplier) == 0) {
+ long divisionFactor = divisor / multiplier;
+ for (int i = 0; i < scaledTimestamps.length; i++) {
+ scaledTimestamps[i] = timestamps.get(i) / divisionFactor;
+ }
+ } else if (divisor < multiplier && (multiplier % divisor) == 0) {
+ long multiplicationFactor = multiplier / divisor;
+ for (int i = 0; i < scaledTimestamps.length; i++) {
+ scaledTimestamps[i] = timestamps.get(i) * multiplicationFactor;
+ }
+ } else {
+ double multiplicationFactor = (double) multiplier / divisor;
+ for (int i = 0; i < scaledTimestamps.length; i++) {
+ scaledTimestamps[i] = (long) (timestamps.get(i) * multiplicationFactor);
+ }
+ }
+ return scaledTimestamps;
+ }
+
+ /**
+ * Applies {@link #scaleLargeTimestamp(long, long, long)} to an array of unscaled timestamps.
+ *
+ * @param timestamps The timestamps to scale.
+ * @param multiplier The multiplier.
+ * @param divisor The divisor.
+ */
+ public static void scaleLargeTimestampsInPlace(long[] timestamps, long multiplier, long divisor) {
+ if (divisor >= multiplier && (divisor % multiplier) == 0) {
+ long divisionFactor = divisor / multiplier;
+ for (int i = 0; i < timestamps.length; i++) {
+ timestamps[i] /= divisionFactor;
+ }
+ } else if (divisor < multiplier && (multiplier % divisor) == 0) {
+ long multiplicationFactor = multiplier / divisor;
+ for (int i = 0; i < timestamps.length; i++) {
+ timestamps[i] *= multiplicationFactor;
+ }
+ } else {
+ double multiplicationFactor = (double) multiplier / divisor;
+ for (int i = 0; i < timestamps.length; i++) {
+ timestamps[i] = (long) (timestamps[i] * multiplicationFactor);
+ }
+ }
+ }
+
+ /**
+ * 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 @PolyNull [] toArray(@PolyNull List<Integer> list) {
+ if (list == null) {
+ return null;
+ }
+ int length = list.size();
+ int[] intArray = new int[length];
+ for (int i = 0; i < length; i++) {
+ intArray[i] = list.get(i);
+ }
+ return intArray;
+ }
+
+ /**
+ * 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.
+ *
+ * @param string A string no more than four characters long.
+ */
+ public static int getIntegerCodeForString(String string) {
+ int length = string.length();
+ Assertions.checkArgument(length <= 4);
+ int result = 0;
+ for (int i = 0; i < length; i++) {
+ result <<= 8;
+ result |= string.charAt(i);
+ }
+ return result;
+ }
+
+ /**
+ * Converts an integer to a long by unsigned conversion.
+ *
+ * <p>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.
+ *
+ * @param hexString The hex string to convert to bytes.
+ * @return A byte array containing values parsed from the hex string provided.
+ */
+ public static byte[] getBytesFromHexString(String hexString) {
+ byte[] data = new byte[hexString.length() / 2];
+ for (int i = 0; i < data.length; i++) {
+ int stringOffset = i * 2;
+ data[i] = (byte) ((Character.digit(hexString.charAt(stringOffset), 16) << 4)
+ + Character.digit(hexString.charAt(stringOffset + 1), 16));
+ }
+ return data;
+ }
+
+ /**
+ * Returns a string with comma delimited simple names of each object's class.
+ *
+ * @param objects The objects whose simple class names should be comma delimited and returned.
+ * @return A string with comma delimited simple names of each object's class.
+ */
+ public static String getCommaDelimitedSimpleClassNames(Object[] objects) {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (int i = 0; i < objects.length; i++) {
+ stringBuilder.append(objects[i].getClass().getSimpleName());
+ if (i < objects.length - 1) {
+ stringBuilder.append(", ");
+ }
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Returns a user agent string based on the given application name and the library version.
+ *
+ * @param context A valid context of the calling application.
+ * @param applicationName String that will be prefix'ed to the generated user agent.
+ * @return A user agent string generated using the applicationName and the library version.
+ */
+ public static String getUserAgent(Context context, String applicationName) {
+ String versionName;
+ try {
+ String packageName = context.getPackageName();
+ PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
+ versionName = info.versionName;
+ } catch (NameNotFoundException e) {
+ versionName = "?";
+ }
+ return applicationName + "/" + versionName + " (Linux;Android " + Build.VERSION.RELEASE
+ + ") " + 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.
+ *
+ * @param bitDepth The bit depth. Supported values are 8, 16, 24 and 32.
+ * @return The corresponding encoding. One of {@link C#ENCODING_PCM_8BIT},
+ * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and
+ * {@link C#ENCODING_PCM_32BIT}. If the bit depth is unsupported then
+ * {@link C#ENCODING_INVALID} is returned.
+ */
+ @C.PcmEncoding
+ public static int getPcmEncoding(int bitDepth) {
+ switch (bitDepth) {
+ case 8:
+ return C.ENCODING_PCM_8BIT;
+ case 16:
+ return C.ENCODING_PCM_16BIT;
+ case 24:
+ return C.ENCODING_PCM_24BIT;
+ case 32:
+ return C.ENCODING_PCM_32BIT;
+ default:
+ return C.ENCODING_INVALID;
+ }
+ }
+
+ /**
+ * 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 (&gt; 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.
+ *
+ * @param pcmEncoding The encoding of the audio data.
+ * @param channelCount The channel count.
+ * @return The size of one audio frame in bytes.
+ */
+ public static int getPcmFrameSize(@C.PcmEncoding int pcmEncoding, int channelCount) {
+ switch (pcmEncoding) {
+ 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}.
+ *
+ * @param uri The {@link Uri}.
+ * @return The content type.
+ */
+ @C.ContentType
+ public static int inferContentType(Uri uri) {
+ String path = uri.getPath();
+ return path == null ? C.TYPE_OTHER : inferContentType(path);
+ }
+
+ /**
+ * Makes a best guess to infer the type from a file name.
+ *
+ * @param fileName Name of the file. It can include the path of the file.
+ * @return The content type.
+ */
+ @C.ContentType
+ public static int inferContentType(String fileName) {
+ fileName = toLowerInvariant(fileName);
+ if (fileName.endsWith(".mpd")) {
+ return C.TYPE_DASH;
+ } else if (fileName.endsWith(".m3u8")) {
+ return C.TYPE_HLS;
+ } else if (fileName.matches(".*\\.ism(l)?(/manifest(\\(.+\\))?)?")) {
+ return C.TYPE_SS;
+ } else {
+ return C.TYPE_OTHER;
+ }
+ }
+
+ /**
+ * Returns the specified millisecond time formatted as a string.
+ *
+ * @param builder The builder that {@code formatter} will write to.
+ * @param formatter The formatter.
+ * @param timeMs The time to format as a string, in milliseconds.
+ * @return The time formatted as a string.
+ */
+ public static String getStringForTime(StringBuilder builder, Formatter formatter, long timeMs) {
+ if (timeMs == C.TIME_UNSET) {
+ timeMs = 0;
+ }
+ long totalSeconds = (timeMs + 500) / 1000;
+ long seconds = totalSeconds % 60;
+ long minutes = (totalSeconds / 60) % 60;
+ long hours = totalSeconds / 3600;
+ builder.setLength(0);
+ return hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString()
+ : formatter.format("%02d:%02d", minutes, seconds).toString();
+ }
+
+ /**
+ * 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.
+ *
+ * <p>For simplicity, this only handles common characters known to be illegal on FAT32:
+ * &lt;, &gt;, :, ", /, \, |, ?, and *. % is also escaped since it is used as the escape
+ * character. Escaping is performed in a consistent way so that no collisions occur and
+ * {@link #unescapeFileName(String)} can be used to retrieve the original file name.
+ *
+ * @param fileName File name to be escaped.
+ * @return An escaped file name which will be safe for use on at least FAT32 filesystems.
+ */
+ public static String escapeFileName(String fileName) {
+ int length = fileName.length();
+ int charactersToEscapeCount = 0;
+ for (int i = 0; i < length; i++) {
+ if (shouldEscapeCharacter(fileName.charAt(i))) {
+ charactersToEscapeCount++;
+ }
+ }
+ if (charactersToEscapeCount == 0) {
+ return fileName;
+ }
+
+ int i = 0;
+ StringBuilder builder = new StringBuilder(length + charactersToEscapeCount * 2);
+ while (charactersToEscapeCount > 0) {
+ char c = fileName.charAt(i++);
+ if (shouldEscapeCharacter(c)) {
+ builder.append('%').append(Integer.toHexString(c));
+ charactersToEscapeCount--;
+ } else {
+ builder.append(c);
+ }
+ }
+ if (i < length) {
+ builder.append(fileName, i, length);
+ }
+ return builder.toString();
+ }
+
+ private static boolean shouldEscapeCharacter(char c) {
+ switch (c) {
+ case '<':
+ case '>':
+ case ':':
+ case '"':
+ case '/':
+ case '\\':
+ case '|':
+ case '?':
+ case '*':
+ case '%':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Unescapes an escaped file or directory name back to its original value.
+ *
+ * <p>See {@link #escapeFileName(String)} for more information.
+ *
+ * @param fileName File name to be unescaped.
+ * @return The original value of the file name before it was escaped, or null if the escaped
+ * fileName seems invalid.
+ */
+ public static @Nullable String unescapeFileName(String fileName) {
+ int length = fileName.length();
+ int percentCharacterCount = 0;
+ for (int i = 0; i < length; i++) {
+ if (fileName.charAt(i) == '%') {
+ percentCharacterCount++;
+ }
+ }
+ if (percentCharacterCount == 0) {
+ return fileName;
+ }
+
+ int expectedLength = length - percentCharacterCount * 2;
+ StringBuilder builder = new StringBuilder(expectedLength);
+ Matcher matcher = ESCAPED_CHARACTER_PATTERN.matcher(fileName);
+ int startOfNotEscaped = 0;
+ while (percentCharacterCount > 0 && matcher.find()) {
+ char unescapedCharacter = (char) Integer.parseInt(matcher.group(1), 16);
+ builder.append(fileName, startOfNotEscaped, matcher.start()).append(unescapedCharacter);
+ startOfNotEscaped = matcher.end();
+ percentCharacterCount--;
+ }
+ if (startOfNotEscaped < length) {
+ builder.append(fileName, startOfNotEscaped, length);
+ }
+ if (builder.length() != expectedLength) {
+ return null;
+ }
+ return builder.toString();
+ }
+
+ /**
+ * A hacky method that always throws {@code t} even if {@code t} is a checked exception,
+ * and is not declared to be thrown.
+ */
+ public static void sneakyThrow(Throwable t) {
+ sneakyThrowInternal(t);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <T extends Throwable> void sneakyThrowInternal(Throwable t) throws T {
+ throw (T) t;
+ }
+
+ /** Recursively deletes a directory and its content. */
+ public static void recursiveDelete(File fileOrDirectory) {
+ File[] directoryFiles = fileOrDirectory.listFiles();
+ if (directoryFiles != null) {
+ for (File child : directoryFiles) {
+ recursiveDelete(child);
+ }
+ }
+ fileOrDirectory.delete();
+ }
+
+ /** Creates an empty directory in the directory returned by {@link Context#getCacheDir()}. */
+ public static File createTempDirectory(Context context, String prefix) throws IOException {
+ 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-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.
+ * @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 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];
+ }
+ return initialValue;
+ }
+
+ /**
+ * Returns the result of updating a CRC-8 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.
+ * @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 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;
+ }
+
+ /**
+ * 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.
+ *
+ * <p>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.
+ *
+ * <p>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 size of the current mode, in pixels.
+ */
+ 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);
+ }
+
+ // 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 {
+ 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 display size: " + displaySize);
+ }
+ }
+
+ Point displaySize = new Point();
+ if (Util.SDK_INT >= 23) {
+ getDisplaySizeV23(display, displaySize);
+ } else if (Util.SDK_INT >= 17) {
+ getDisplaySizeV17(display, displaySize);
+ } else {
+ 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();
+ outSize.x = mode.getPhysicalWidth();
+ outSize.y = mode.getPhysicalHeight();
+ }
+
+ @TargetApi(17)
+ private static void getDisplaySizeV17(Display display, Point outSize) {
+ display.getRealSize(outSize);
+ }
+
+ private static void getDisplaySizeV16(Display display, Point outSize) {
+ display.getSize(outSize);
+ }
+
+ 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<String, String> createIsoLanguageReplacementMap() {
+ String[] iso2Languages = Locale.getISOLanguages();
+ HashMap<String, String> 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-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
+ };
+
+ /**
+ * 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/XmlPullParserUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/XmlPullParserUtil.java
new file mode 100644
index 0000000000..7b56886dba
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/XmlPullParserUtil.java
@@ -0,0 +1,131 @@
+/*
+ * 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.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * {@link XmlPullParser} utility methods.
+ */
+public final class XmlPullParserUtil {
+
+ private XmlPullParserUtil() {}
+
+ /**
+ * Returns whether the current event is an end tag with the specified name.
+ *
+ * @param xpp The {@link XmlPullParser} to query.
+ * @param name The specified name.
+ * @return Whether the current event is an end tag with the specified name.
+ * @throws XmlPullParserException If an error occurs querying the parser.
+ */
+ public static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException {
+ return isEndTag(xpp) && xpp.getName().equals(name);
+ }
+
+ /**
+ * Returns whether the current event is an end tag.
+ *
+ * @param xpp The {@link XmlPullParser} to query.
+ * @return Whether the current event is an end tag.
+ * @throws XmlPullParserException If an error occurs querying the parser.
+ */
+ public static boolean isEndTag(XmlPullParser xpp) throws XmlPullParserException {
+ return xpp.getEventType() == XmlPullParser.END_TAG;
+ }
+
+ /**
+ * Returns whether the current event is a start tag with the specified name.
+ *
+ * @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 isStartTag(XmlPullParser xpp, String name) throws XmlPullParserException {
+ return isStartTag(xpp) && xpp.getName().equals(name);
+ }
+
+ /**
+ * Returns whether the current event is a start tag.
+ *
+ * @param xpp The {@link XmlPullParser} to query.
+ * @return Whether the current event is a start tag.
+ * @throws XmlPullParserException If an error occurs querying the parser.
+ */
+ public static boolean isStartTag(XmlPullParser xpp) throws XmlPullParserException {
+ 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
+ * such attribute was found.
+ */
+ public static @Nullable String getAttributeValue(XmlPullParser xpp, String attributeName) {
+ int attributeCount = xpp.getAttributeCount();
+ for (int i = 0; i < attributeCount; 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/package-info.java
new file mode 100644
index 0000000000..49ee4a4d4d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/AvcConfig.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/AvcConfig.java
new file mode 100644
index 0000000000..2026a27ff7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/AvcConfig.java
@@ -0,0 +1,97 @@
+/*
+ * 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 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.util.CodecSpecificDataUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NalUnitUtil.SpsData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * AVC configuration data.
+ */
+public final class AvcConfig {
+
+ public final List<byte[]> initializationData;
+ public final int nalUnitLengthFieldLength;
+ public final int width;
+ public final int height;
+ public final float pixelWidthAspectRatio;
+
+ /**
+ * Parses AVC configuration data.
+ *
+ * @param data A {@link ParsableByteArray}, whose position is set to the start of the AVC
+ * configuration data to parse.
+ * @return A parsed representation of the HEVC configuration data.
+ * @throws ParserException If an error occurred parsing the data.
+ */
+ public static AvcConfig parse(ParsableByteArray data) throws ParserException {
+ try {
+ data.skipBytes(4); // Skip to the AVCDecoderConfigurationRecord (defined in 14496-15)
+ int nalUnitLengthFieldLength = (data.readUnsignedByte() & 0x3) + 1;
+ if (nalUnitLengthFieldLength == 3) {
+ throw new IllegalStateException();
+ }
+ List<byte[]> initializationData = new ArrayList<>();
+ int numSequenceParameterSets = data.readUnsignedByte() & 0x1F;
+ for (int j = 0; j < numSequenceParameterSets; j++) {
+ initializationData.add(buildNalUnitForChild(data));
+ }
+ int numPictureParameterSets = data.readUnsignedByte();
+ for (int j = 0; j < numPictureParameterSets; j++) {
+ initializationData.add(buildNalUnitForChild(data));
+ }
+
+ int width = Format.NO_VALUE;
+ int height = Format.NO_VALUE;
+ float pixelWidthAspectRatio = 1;
+ if (numSequenceParameterSets > 0) {
+ byte[] sps = initializationData.get(0);
+ SpsData spsData = NalUnitUtil.parseSpsNalUnit(initializationData.get(0),
+ nalUnitLengthFieldLength, sps.length);
+ width = spsData.width;
+ height = spsData.height;
+ pixelWidthAspectRatio = spsData.pixelWidthAspectRatio;
+ }
+ return new AvcConfig(initializationData, nalUnitLengthFieldLength, width, height,
+ pixelWidthAspectRatio);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new ParserException("Error parsing AVC config", e);
+ }
+ }
+
+ private AvcConfig(List<byte[]> initializationData, int nalUnitLengthFieldLength,
+ int width, int height, float pixelWidthAspectRatio) {
+ this.initializationData = initializationData;
+ this.nalUnitLengthFieldLength = nalUnitLengthFieldLength;
+ this.width = width;
+ this.height = height;
+ this.pixelWidthAspectRatio = pixelWidthAspectRatio;
+ }
+
+ private static byte[] buildNalUnitForChild(ParsableByteArray data) {
+ int length = data.readUnsignedShort();
+ int offset = data.getPosition();
+ data.skipBytes(length);
+ return CodecSpecificDataUtil.buildNalUnit(data.data, offset, length);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/ColorInfo.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/ColorInfo.java
new file mode 100644
index 0000000000..7eed4e3eaf
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/ColorInfo.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.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;
+
+/**
+ * Stores color info.
+ */
+public final class ColorInfo implements Parcelable {
+
+ /**
+ * The color space of the video. Valid values are {@link C#COLOR_SPACE_BT601}, {@link
+ * C#COLOR_SPACE_BT709}, {@link C#COLOR_SPACE_BT2020} or {@link Format#NO_VALUE} if unknown.
+ */
+ @C.ColorSpace
+ public final int colorSpace;
+
+ /**
+ * The color range of the video. Valid values are {@link C#COLOR_RANGE_LIMITED}, {@link
+ * C#COLOR_RANGE_FULL} or {@link Format#NO_VALUE} if unknown.
+ */
+ @C.ColorRange
+ public final int colorRange;
+
+ /**
+ * The color transfer characteristicks of the video. Valid values are {@link
+ * C#COLOR_TRANSFER_HLG}, {@link C#COLOR_TRANSFER_ST2084}, {@link C#COLOR_TRANSFER_SDR} or {@link
+ * Format#NO_VALUE} if unknown.
+ */
+ @C.ColorTransfer
+ public final int colorTransfer;
+
+ /** HdrStaticInfo as defined in CTA-861.3, or null if none specified. */
+ @Nullable public final byte[] hdrStaticInfo;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * Constructs the ColorInfo.
+ *
+ * @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, or null if none specified.
+ */
+ 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;
+ this.hdrStaticInfo = hdrStaticInfo;
+ }
+
+ @SuppressWarnings("ResourceType")
+ /* package */ ColorInfo(Parcel in) {
+ colorSpace = in.readInt();
+ colorRange = in.readInt();
+ colorTransfer = in.readInt();
+ boolean hasHdrStaticInfo = Util.readBoolean(in);
+ hdrStaticInfo = hasHdrStaticInfo ? in.createByteArray() : null;
+ }
+
+ // Parcelable implementation.
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ ColorInfo other = (ColorInfo) obj;
+ return colorSpace == other.colorSpace
+ && colorRange == other.colorRange
+ && colorTransfer == other.colorTransfer
+ && Arrays.equals(hdrStaticInfo, other.hdrStaticInfo);
+ }
+
+ @Override
+ public String toString() {
+ return "ColorInfo(" + colorSpace + ", " + colorRange + ", " + colorTransfer
+ + ", " + (hdrStaticInfo != null) + ")";
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = 17;
+ result = 31 * result + colorSpace;
+ result = 31 * result + colorRange;
+ result = 31 * result + colorTransfer;
+ result = 31 * result + Arrays.hashCode(hdrStaticInfo);
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(colorSpace);
+ dest.writeInt(colorRange);
+ dest.writeInt(colorTransfer);
+ Util.writeBoolean(dest, hdrStaticInfo != null);
+ if (hdrStaticInfo != null) {
+ dest.writeByteArray(hdrStaticInfo);
+ }
+ }
+
+ public static final Parcelable.Creator<ColorInfo> CREATOR =
+ new Parcelable.Creator<ColorInfo>() {
+ @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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/DolbyVisionConfig.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/DolbyVisionConfig.java
new file mode 100644
index 0000000000..bfc1f814d2
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/DummySurface.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/DummySurface.java
new file mode 100644
index 0000000000..abfb8b0952
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ * <p>
+ * 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/HevcConfig.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/HevcConfig.java
new file mode 100644
index 0000000000..844712146a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/HevcConfig.java
@@ -0,0 +1,91 @@
+/*
+ * 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 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;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * HEVC configuration data.
+ */
+public final class HevcConfig {
+
+ @Nullable public final List<byte[]> initializationData;
+ public final int nalUnitLengthFieldLength;
+
+ /**
+ * Parses HEVC configuration data.
+ *
+ * @param data A {@link ParsableByteArray}, whose position is set to the start of the HEVC
+ * configuration data to parse.
+ * @return A parsed representation of the HEVC configuration data.
+ * @throws ParserException If an error occurred parsing the data.
+ */
+ public static HevcConfig parse(ParsableByteArray data) throws ParserException {
+ try {
+ data.skipBytes(21); // Skip to the NAL unit length size field.
+ int lengthSizeMinusOne = data.readUnsignedByte() & 0x03;
+
+ // Calculate the combined size of all VPS/SPS/PPS bitstreams.
+ int numberOfArrays = data.readUnsignedByte();
+ int csdLength = 0;
+ int csdStartPosition = data.getPosition();
+ for (int i = 0; i < numberOfArrays; i++) {
+ data.skipBytes(1); // completeness (1), nal_unit_type (7)
+ int numberOfNalUnits = data.readUnsignedShort();
+ for (int j = 0; j < numberOfNalUnits; j++) {
+ int nalUnitLength = data.readUnsignedShort();
+ csdLength += 4 + nalUnitLength; // Start code and NAL unit.
+ data.skipBytes(nalUnitLength);
+ }
+ }
+
+ // Concatenate the codec-specific data into a single buffer.
+ data.setPosition(csdStartPosition);
+ byte[] buffer = new byte[csdLength];
+ int bufferPosition = 0;
+ for (int i = 0; i < numberOfArrays; i++) {
+ data.skipBytes(1); // completeness (1), nal_unit_type (7)
+ int numberOfNalUnits = data.readUnsignedShort();
+ for (int j = 0; j < numberOfNalUnits; j++) {
+ int nalUnitLength = data.readUnsignedShort();
+ System.arraycopy(NalUnitUtil.NAL_START_CODE, 0, buffer, bufferPosition,
+ NalUnitUtil.NAL_START_CODE.length);
+ bufferPosition += NalUnitUtil.NAL_START_CODE.length;
+ System
+ .arraycopy(data.data, data.getPosition(), buffer, bufferPosition, nalUnitLength);
+ bufferPosition += nalUnitLength;
+ data.skipBytes(nalUnitLength);
+ }
+ }
+
+ List<byte[]> initializationData = csdLength == 0 ? null : Collections.singletonList(buffer);
+ return new HevcConfig(initializationData, lengthSizeMinusOne + 1);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new ParserException("Error parsing HEVC config", e);
+ }
+ }
+
+ private HevcConfig(@Nullable List<byte[]> initializationData, int nalUnitLengthFieldLength) {
+ this.initializationData = initializationData;
+ this.nalUnitLengthFieldLength = nalUnitLengthFieldLength;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
new file mode 100644
index 0000000000..1627b70a28
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
@@ -0,0 +1,1873 @@
+/*
+ * 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.annotation.SuppressLint;
+import android.annotation.TargetApi;
+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.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;
+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.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}.
+ *
+ * <p>This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)}
+ * on the playback thread:
+ *
+ * <ul>
+ * <li>Message with type {@link C#MSG_SET_SURFACE} to set the output surface. The message payload
+ * should be the target {@link Surface}, or null.
+ * <li>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}.
+ * </ul>
+ */
+public class MediaCodecVideoRenderer extends MediaCodecRenderer {
+
+ private static final String TAG = "MediaCodecVideoRenderer";
+ private static final String KEY_CROP_LEFT = "crop-left";
+ private static final String KEY_CROP_RIGHT = "crop-right";
+ private static final String KEY_CROP_BOTTOM = "crop-bottom";
+ private static final String KEY_CROP_TOP = "crop-top";
+
+ // Long edge length in pixels for standard video formats, in decreasing in order.
+ 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 deviceNeedsNoPostProcessWorkaround;
+ private final long[] pendingOutputStreamOffsetsUs;
+ private final long[] pendingOutputStreamSwitchTimesUs;
+
+ 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;
+ private float currentPixelWidthHeightRatio;
+ private int reportedWidth;
+ private int reportedHeight;
+ private int reportedUnappliedRotationDegrees;
+ private float reportedPixelWidthHeightRatio;
+
+ private boolean tunneling;
+ private int tunnelingAudioSessionId;
+ /* package */ @Nullable OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener;
+
+ private long lastInputTimeUs;
+ private long outputStreamOffsetUs;
+ private int pendingOutputStreamOffsetCount;
+ @Nullable private VideoFrameMetadataListener frameMetadataListener;
+
+ /**
+ * @param context A context.
+ * @param mediaCodecSelector A decoder selector.
+ */
+ public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector) {
+ this(context, mediaCodecSelector, 0);
+ }
+
+ /**
+ * @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.
+ */
+ public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector,
+ long allowedJoiningTimeMs) {
+ this(
+ context,
+ mediaCodecSelector,
+ allowedJoiningTimeMs,
+ /* eventHandler= */ null,
+ /* eventListener= */ null,
+ /* maxDroppedFramesToNotify= */ -1);
+ }
+
+ /**
+ * @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 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,
+ @Nullable Handler eventHandler,
+ @Nullable VideoRendererEventListener eventListener,
+ int maxDroppedFramesToNotify) {
+ this(
+ context,
+ mediaCodecSelector,
+ allowedJoiningTimeMs,
+ /* drmSessionManager= */ null,
+ /* playClearSamplesWithoutKeys= */ 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 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 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
+ @SuppressWarnings("deprecation")
+ public MediaCodecVideoRenderer(
+ Context context,
+ MediaCodecSelector mediaCodecSelector,
+ long allowedJoiningTimeMs,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> 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<FrameworkMediaCrypto> 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;
+ this.context = context.getApplicationContext();
+ frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(this.context);
+ eventDispatcher = new EventDispatcher(eventHandler, eventListener);
+ 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;
+ currentPixelWidthHeightRatio = Format.NO_VALUE;
+ pendingPixelWidthHeightRatio = Format.NO_VALUE;
+ scalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
+ clearReportedVideoSize();
+ }
+
+ @Override
+ @Capabilities
+ protected int supportsFormat(
+ MediaCodecSelector mediaCodecSelector,
+ @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ Format format)
+ throws DecoderQueryException {
+ String mimeType = format.sampleMimeType;
+ if (!MimeTypes.isVideo(mimeType)) {
+ return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
+ }
+ @Nullable DrmInitData drmInitData = format.drmInitData;
+ // Assume encrypted content requires secure decoders.
+ boolean requiresSecureDecryption = drmInitData != null;
+ List<MediaCodecInfo> 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);
+ }
+ if (decoderInfos.isEmpty()) {
+ return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE);
+ }
+ 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<MediaCodecInfo> 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);
+ }
+
+ @Override
+ protected List<MediaCodecInfo> getDecoderInfos(
+ MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder)
+ throws DecoderQueryException {
+ return getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder, tunneling);
+ }
+
+ private static List<MediaCodecInfo> getDecoderInfos(
+ MediaCodecSelector mediaCodecSelector,
+ Format format,
+ boolean requiresSecureDecoder,
+ boolean requiresTunnelingDecoder)
+ throws DecoderQueryException {
+ @Nullable String mimeType = format.sampleMimeType;
+ if (mimeType == null) {
+ return Collections.emptyList();
+ }
+ List<MediaCodecInfo> 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<Integer, Integer> 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, 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 {
+ joiningDeadlineMs = C.TIME_UNSET;
+ }
+ }
+
+ @Override
+ public boolean 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;
+ } 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;
+ }
+ }
+
+ @Override
+ protected void onStarted() {
+ super.onStarted();
+ droppedFrames = 0;
+ droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
+ lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
+ }
+
+ @Override
+ protected void onStopped() {
+ joiningDeadlineMs = C.TIME_UNSET;
+ maybeNotifyDroppedFrames();
+ super.onStopped();
+ }
+
+ @Override
+ protected void onDisabled() {
+ lastInputTimeUs = C.TIME_UNSET;
+ outputStreamOffsetUs = C.TIME_UNSET;
+ pendingOutputStreamOffsetCount = 0;
+ currentMediaFormat = null;
+ clearReportedVideoSize();
+ clearRenderedFirstFrame();
+ frameReleaseTimeHelper.disable();
+ tunnelingOnFrameRenderedListener = null;
+ try {
+ super.onDisabled();
+ } finally {
+ eventDispatcher.disabled(decoderCounters);
+ }
+ }
+
+ @Override
+ 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) {
+ 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;
+ @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 && surface != dummySurface) {
+ // If we know the video size, report it again immediately.
+ maybeRenotifyVideoSizeChanged();
+ // We haven't rendered to the new surface yet.
+ clearRenderedFirstFrame();
+ if (state == STATE_STARTED) {
+ setJoiningDeadlineMs();
+ }
+ } else {
+ // The surface has been removed.
+ clearReportedVideoSize();
+ clearRenderedFirstFrame();
+ }
+ } 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(MediaCodecInfo codecInfo) {
+ return surface != null || shouldUseDummySurface(codecInfo);
+ }
+
+ @Override
+ 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);
+ }
+ }
+
+ @Override
+ 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 releaseCodec() {
+ try {
+ super.releaseCodec();
+ } finally {
+ buffersInCodecCount = 0;
+ }
+ }
+
+ @CallSuper
+ @Override
+ protected boolean flushOrReleaseCodec() {
+ try {
+ return super.flushOrReleaseCodec();
+ } finally {
+ buffersInCodecCount = 0;
+ }
+ }
+
+ @Override
+ 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.
+ // Hence currentUnappliedRotation should always be 0. For 90 and 270 degree rotations, we need
+ // to flip the width, height and pixel aspect ratio to reflect the rotation that was applied.
+ if (pendingRotationDegrees == 90 || pendingRotationDegrees == 270) {
+ int rotatedHeight = currentWidth;
+ currentWidth = currentHeight;
+ currentHeight = rotatedHeight;
+ currentPixelWidthHeightRatio = 1 / currentPixelWidthHeightRatio;
+ }
+ } else {
+ // On API level 20 and below the decoder does not apply the rotation.
+ currentUnappliedRotationDegrees = pendingRotationDegrees;
+ }
+ // Must be applied each time the output MediaFormat changes.
+ codec.setVideoScalingMode(scalingMode);
+ }
+
+ 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 void onProcessedOutputBuffer(long presentationTimeUs) {
+ if (!tunneling) {
+ buffersInCodecCount--;
+ }
+ 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();
+ }
+ }
+
+ /**
+ * 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.
+ * @param isLastBuffer Whether the buffer is the last buffer in the current stream.
+ */
+ protected boolean shouldDropOutputBuffer(
+ long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) {
+ return isBufferLate(earlyUs) && !isLastBuffer;
+ }
+
+ /**
+ * 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(index, false);
+ TraceUtil.endSection();
+ decoderCounters.skippedOutputBufferCount++;
+ }
+
+ /**
+ * 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(index, false);
+ TraceUtil.endSection();
+ 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();
+ }
+ }
+
+ /**
+ * 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(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)
+ protected void renderOutputBufferV21(
+ MediaCodec codec, int index, long presentationTimeUs, long releaseTimeNs) {
+ maybeNotifyVideoSizeChanged();
+ TraceUtil.beginSection("releaseOutputBuffer");
+ 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;
+ }
+
+ private void clearRenderedFirstFrame() {
+ renderedFirstFrame = false;
+ // The first frame notification is triggered by renderOutputBuffer or renderOutputBufferV21 for
+ // non-tunneled playback, onQueueInputBuffer for tunneled playback prior to API level 23, and
+ // OnFrameRenderedListenerV23.onFrameRenderedListener for tunneled playback on API level 23 and
+ // above.
+ if (Util.SDK_INT >= 23 && tunneling) {
+ MediaCodec codec = getCodec();
+ // If codec is null then the listener will be instantiated in configureCodec.
+ if (codec != null) {
+ tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec);
+ }
+ }
+ }
+
+ /* package */ 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;
+ reportedPixelWidthHeightRatio = Format.NO_VALUE;
+ reportedUnappliedRotationDegrees = Format.NO_VALUE;
+ }
+
+ private void maybeNotifyVideoSizeChanged() {
+ if ((currentWidth != Format.NO_VALUE || currentHeight != Format.NO_VALUE)
+ && (reportedWidth != currentWidth || reportedHeight != currentHeight
+ || reportedUnappliedRotationDegrees != currentUnappliedRotationDegrees
+ || reportedPixelWidthHeightRatio != currentPixelWidthHeightRatio)) {
+ eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees,
+ currentPixelWidthHeightRatio);
+ reportedWidth = currentWidth;
+ reportedHeight = currentHeight;
+ reportedUnappliedRotationDegrees = currentUnappliedRotationDegrees;
+ reportedPixelWidthHeightRatio = currentPixelWidthHeightRatio;
+ }
+ }
+
+ private void maybeRenotifyVideoSizeChanged() {
+ if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) {
+ eventDispatcher.videoSizeChanged(reportedWidth, reportedHeight,
+ reportedUnappliedRotationDegrees, reportedPixelWidthHeightRatio);
+ }
+ }
+
+ 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;
+ }
+
+ @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)
+ private static void setOutputSurfaceV23(MediaCodec codec, Surface surface) {
+ codec.setOutputSurface(surface);
+ }
+
+ @TargetApi(21)
+ private static void configureTunnelingV21(MediaFormat mediaFormat, int tunnelingAudioSessionId) {
+ mediaFormat.setFeatureEnabled(CodecCapabilities.FEATURE_TunneledPlayback, true);
+ 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<Integer, Integer> 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 {@link Format} for which the codec is being configured.
+ * @param streamFormats The possible stream formats.
+ * @return Suitable {@link CodecMaxValues}.
+ */
+ protected CodecMaxValues getCodecMaxValues(
+ MediaCodecInfo codecInfo, Format format, Format[] streamFormats) {
+ int maxWidth = format.width;
+ int maxHeight = format.height;
+ 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 (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(codecInfo, streamFormat));
+ }
+ }
+ if (haveUnknownDimensions) {
+ Log.w(TAG, "Resolutions unknown. Codec max resolution: " + maxWidth + "x" + maxHeight);
+ Point codecMaxSize = getCodecMaxSize(codecInfo, format);
+ if (codecMaxSize != null) {
+ maxWidth = Math.max(maxWidth, codecMaxSize.x);
+ maxHeight = Math.max(maxHeight, codecMaxSize.y);
+ 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.
+ *
+ * @param codecInfo Information about the {@link MediaCodec} being configured.
+ * @param format The {@link Format} for which the codec is being configured.
+ * @return The maximum video size to use, or null if the size of {@code format} should be used.
+ */
+ 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;
+ float aspectRatio = (float) formatShortEdgePx / formatLongEdgePx;
+ for (int longEdgePx : STANDARD_LONG_EDGE_VIDEO_PX) {
+ int shortEdgePx = (int) (longEdgePx * aspectRatio);
+ if (longEdgePx <= formatLongEdgePx || shortEdgePx <= formatShortEdgePx) {
+ // Don't return a size not larger than the format for which the codec is being configured.
+ return null;
+ } else if (Util.SDK_INT >= 21) {
+ Point alignedSize = codecInfo.alignVideoSizeV21(isVerticalVideo ? shortEdgePx : longEdgePx,
+ isVerticalVideo ? longEdgePx : shortEdgePx);
+ float frameRate = format.frameRate;
+ if (codecInfo.isVideoSizeAndRateSupportedV21(alignedSize.x, alignedSize.y, frameRate)) {
+ return alignedSize;
+ }
+ } else {
+ 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;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 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 buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not
+ * be determined.
+ */
+ private static int getMaxInputSize(MediaCodecInfo codecInfo, Format format) {
+ if (format.maxInputSize != Format.NO_VALUE) {
+ // 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);
+ }
+ }
+
+ /**
+ * 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 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;
+ }
+
+ // Attempt to infer a maximum input size from the format.
+ int maxPixels;
+ int minCompressionRatio;
+ switch (sampleMimeType) {
+ case MimeTypes.VIDEO_H263:
+ case MimeTypes.VIDEO_MP4V:
+ maxPixels = width * height;
+ minCompressionRatio = 2;
+ break;
+ case MimeTypes.VIDEO_H264:
+ 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.
+ maxPixels = Util.ceilDivide(width, 16) * Util.ceilDivide(height, 16) * 16 * 16;
+ minCompressionRatio = 2;
+ break;
+ case MimeTypes.VIDEO_VP8:
+ // VPX does not specify a ratio so use the values from the platform's SoftVPX.cpp.
+ maxPixels = width * height;
+ minCompressionRatio = 2;
+ break;
+ case MimeTypes.VIDEO_H265:
+ case MimeTypes.VIDEO_VP9:
+ maxPixels = width * height;
+ minCompressionRatio = 4;
+ break;
+ default:
+ // Leave the default max input size.
+ return Format.NO_VALUE;
+ }
+ // Estimate the maximum input size assuming three channel 4:2:0 subsampled input frames.
+ return (maxPixels * 3) / (2 * minCompressionRatio);
+ }
+
+ /**
+ * Returns whether the device is known to do post processing by default that isn't compatible with
+ * ExoPlayer.
+ *
+ * @return Whether the device is known to do post processing by default that isn't compatible with
+ * ExoPlayer.
+ */
+ 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]. 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);
+ }
+
+ /*
+ * TODO:
+ *
+ * 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.
+ */
+ /**
+ * Returns whether the codec is known to implement {@link MediaCodec#setOutputSurface(Surface)}
+ * incorrectly.
+ *
+ * <p>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;
+ }
+
+ protected Surface getSurface() {
+ return surface;
+ }
+
+ protected static final class CodecMaxValues {
+
+ public final int width;
+ public final int height;
+ public final int inputSize;
+
+ public CodecMaxValues(int width, int height, int inputSize) {
+ this.width = width;
+ this.height = height;
+ this.inputSize = inputSize;
+ }
+
+ }
+
+ @TargetApi(23)
+ private final class OnFrameRenderedListenerV23
+ implements MediaCodec.OnFrameRenderedListener, Handler.Callback {
+
+ 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(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;
+ }
+ if (presentationTimeUs == TUNNELING_EOS_PRESENTATION_TIME_US) {
+ onProcessedTunneledEndOfStream();
+ } else {
+ onProcessedTunneledBuffer(presentationTimeUs);
+ }
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java
new file mode 100644
index 0000000000..fbcd4d959c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<Format> formatQueue;
+ private final DecoderInputBuffer flagsOnlyBuffer;
+ private final DrmSessionManager<ExoMediaCrypto> 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<ExoMediaCrypto> decoderDrmSession;
+ @Nullable private DrmSession<ExoMediaCrypto> 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<ExoMediaCrypto> 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.
+ *
+ * <p>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<ExoMediaCrypto>) 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.
+ *
+ * <p>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<ExoMediaCrypto> 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.
+ *
+ * <p>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.
+ *
+ * <p>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<ExoMediaCrypto> session) {
+ DrmSession.replaceSession(sourceDrmSession, session);
+ sourceDrmSession = session;
+ }
+
+ private void setDecoderDrmSession(@Nullable DrmSession<ExoMediaCrypto> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderException.java
new file mode 100644
index 0000000000..dfffbe049b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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 <tt>null</tt> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java
new file mode 100644
index 0000000000..69249dd426
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java
new file mode 100644
index 0000000000..d911ac3a5a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java
new file mode 100644
index 0000000000..b09e8b759a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderOutputBufferRenderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderOutputBufferRenderer.java
new file mode 100644
index 0000000000..f4058ea40f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderRenderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoDecoderRenderer.java
new file mode 100644
index 0000000000..1e302e4aaa
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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<VideoDecoderOutputBuffer> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java
new file mode 100644
index 0000000000..46e05def5c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java
new file mode 100644
index 0000000000..c13cd4b1cb
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java
@@ -0,0 +1,361 @@
+/*
+ * 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.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.
+ */
+public final class VideoFrameReleaseTimeHelper {
+
+ private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500;
+ private static final long MAX_ALLOWED_DRIFT_NS = 20000000;
+
+ 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 DefaultDisplayListener displayListener;
+
+ private long vsyncDurationNs;
+ private long vsyncOffsetNs;
+
+ private long lastFramePresentationTimeUs;
+ private long adjustedLastFrameTimeNs;
+ private long pendingAdjustedFrameTimeNs;
+
+ private boolean haveSync;
+ private long syncUnadjustedReleaseTimeNs;
+ private long syncFramePresentationTimeNs;
+ private long frameCount;
+
+ /**
+ * Constructs an instance that smooths frame release timestamps but does not align them with
+ * the default display's vsync signal.
+ */
+ public VideoFrameReleaseTimeHelper() {
+ this(null);
+ }
+
+ /**
+ * 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(@Nullable Context context) {
+ if (context != null) {
+ context = context.getApplicationContext();
+ windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ } else {
+ 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. Must be called from the playback thread.
+ */
+ public void enable() {
+ haveSync = false;
+ if (windowManager != null) {
+ vsyncSampler.addObserver();
+ if (displayListener != null) {
+ displayListener.register();
+ }
+ updateDefaultDisplayRefreshRateParams();
+ }
+ }
+
+ /**
+ * Disables the helper. Must be called from the playback thread.
+ */
+ public void disable() {
+ if (windowManager != null) {
+ if (displayListener != null) {
+ displayListener.unregister();
+ }
+ vsyncSampler.removeObserver();
+ }
+ }
+
+ /**
+ * 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
+ * the same time base as {@link System#nanoTime()}.
+ * @return The adjusted frame release timestamp, in nanoseconds and in the same time base as
+ * {@link System#nanoTime()}.
+ */
+ public long adjustReleaseTime(long framePresentationTimeUs, long unadjustedReleaseTimeNs) {
+ long framePresentationTimeNs = framePresentationTimeUs * 1000;
+
+ // Until we know better, the adjustment will be a no-op.
+ long adjustedFrameTimeNs = framePresentationTimeNs;
+ long adjustedReleaseTimeNs = unadjustedReleaseTimeNs;
+
+ if (haveSync) {
+ // See if we've advanced to the next frame.
+ if (framePresentationTimeUs != lastFramePresentationTimeUs) {
+ frameCount++;
+ adjustedLastFrameTimeNs = pendingAdjustedFrameTimeNs;
+ }
+ if (frameCount >= MIN_FRAMES_FOR_ADJUSTMENT) {
+ // We're synced and have waited the required number of frames to apply an adjustment.
+ // Calculate the average frame time across all the frames we've seen since the last sync.
+ // This will typically give us a frame rate at a finer granularity than the frame times
+ // themselves (which often only have millisecond granularity).
+ long averageFrameDurationNs = (framePresentationTimeNs - syncFramePresentationTimeNs)
+ / frameCount;
+ // Project the adjusted frame time forward using the average.
+ long candidateAdjustedFrameTimeNs = adjustedLastFrameTimeNs + averageFrameDurationNs;
+
+ if (isDriftTooLarge(candidateAdjustedFrameTimeNs, unadjustedReleaseTimeNs)) {
+ haveSync = false;
+ } else {
+ adjustedFrameTimeNs = candidateAdjustedFrameTimeNs;
+ adjustedReleaseTimeNs = syncUnadjustedReleaseTimeNs + adjustedFrameTimeNs
+ - syncFramePresentationTimeNs;
+ }
+ } else {
+ // We're synced but haven't waited the required number of frames to apply an adjustment.
+ // Check drift anyway.
+ if (isDriftTooLarge(framePresentationTimeNs, unadjustedReleaseTimeNs)) {
+ haveSync = false;
+ }
+ }
+ }
+
+ // If we need to sync, do so now.
+ if (!haveSync) {
+ syncFramePresentationTimeNs = framePresentationTimeNs;
+ syncUnadjustedReleaseTimeNs = unadjustedReleaseTimeNs;
+ frameCount = 0;
+ haveSync = true;
+ }
+
+ lastFramePresentationTimeUs = framePresentationTimeUs;
+ pendingAdjustedFrameTimeNs = adjustedFrameTimeNs;
+
+ 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, sampledVsyncTimeNs, vsyncDurationNs);
+ // Apply an offset so that we release before the target vsync, but after the previous one.
+ return snappedTimeNs - vsyncOffsetNs;
+ }
+
+ @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) {
+ long elapsedFrameTimeNs = frameTimeNs - syncFramePresentationTimeNs;
+ long elapsedReleaseTimeNs = releaseTimeNs - syncUnadjustedReleaseTimeNs;
+ return Math.abs(elapsedReleaseTimeNs - elapsedFrameTimeNs) > MAX_ALLOWED_DRIFT_NS;
+ }
+
+ private static long closestVsync(long releaseTime, long sampledVsyncTime, long vsyncDuration) {
+ long vsyncCount = (releaseTime - sampledVsyncTime) / vsyncDuration;
+ long snappedTimeNs = sampledVsyncTime + (vsyncDuration * vsyncCount);
+ long snappedBeforeNs;
+ long snappedAfterNs;
+ if (releaseTime <= snappedTimeNs) {
+ snappedBeforeNs = snappedTimeNs - vsyncDuration;
+ snappedAfterNs = snappedTimeNs;
+ } else {
+ snappedBeforeNs = snappedTimeNs;
+ snappedAfterNs = snappedTimeNs + vsyncDuration;
+ }
+ long snappedAfterDiff = snappedAfterNs - releaseTime;
+ long snappedBeforeDiff = releaseTime - snappedBeforeNs;
+ return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs;
+ }
+
+ @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();
+ }
+ }
+
+ }
+
+ /**
+ * Samples display vsync timestamps. A single instance using a single {@link Choreographer} is
+ * shared by all {@link VideoFrameReleaseTimeHelper} instances. This is done to avoid a resource
+ * leak in the platform on API levels prior to 23. See [Internal: b/12455729].
+ */
+ private static final class VSyncSampler implements FrameCallback, Handler.Callback {
+
+ public volatile long sampledVsyncTimeNs;
+
+ private static final int CREATE_CHOREOGRAPHER = 0;
+ private static final int MSG_ADD_OBSERVER = 1;
+ private static final int MSG_REMOVE_OBSERVER = 2;
+
+ private static final VSyncSampler INSTANCE = new VSyncSampler();
+
+ private final Handler handler;
+ private final HandlerThread choreographerOwnerThread;
+ private Choreographer choreographer;
+ private int observerCount;
+
+ public static VSyncSampler getInstance() {
+ return INSTANCE;
+ }
+
+ private VSyncSampler() {
+ sampledVsyncTimeNs = C.TIME_UNSET;
+ choreographerOwnerThread = new HandlerThread("ChoreographerOwner:Handler");
+ choreographerOwnerThread.start();
+ handler = Util.createHandler(choreographerOwnerThread.getLooper(), /* callback= */ this);
+ handler.sendEmptyMessage(CREATE_CHOREOGRAPHER);
+ }
+
+ /**
+ * Notifies the sampler that a {@link VideoFrameReleaseTimeHelper} is observing
+ * {@link #sampledVsyncTimeNs}, and hence that the value should be periodically updated.
+ */
+ public void addObserver() {
+ handler.sendEmptyMessage(MSG_ADD_OBSERVER);
+ }
+
+ /**
+ * Notifies the sampler that a {@link VideoFrameReleaseTimeHelper} is no longer observing
+ * {@link #sampledVsyncTimeNs}.
+ */
+ public void removeObserver() {
+ handler.sendEmptyMessage(MSG_REMOVE_OBSERVER);
+ }
+
+ @Override
+ public void doFrame(long vsyncTimeNs) {
+ sampledVsyncTimeNs = vsyncTimeNs;
+ choreographer.postFrameCallbackDelayed(this, CHOREOGRAPHER_SAMPLE_DELAY_MILLIS);
+ }
+
+ @Override
+ public boolean handleMessage(Message message) {
+ switch (message.what) {
+ case CREATE_CHOREOGRAPHER: {
+ createChoreographerInstanceInternal();
+ return true;
+ }
+ case MSG_ADD_OBSERVER: {
+ addObserverInternal();
+ return true;
+ }
+ case MSG_REMOVE_OBSERVER: {
+ removeObserverInternal();
+ return true;
+ }
+ default: {
+ return false;
+ }
+ }
+ }
+
+ private void createChoreographerInstanceInternal() {
+ choreographer = Choreographer.getInstance();
+ }
+
+ private void addObserverInternal() {
+ observerCount++;
+ if (observerCount == 1) {
+ choreographer.postFrameCallback(this);
+ }
+ }
+
+ private void removeObserverInternal() {
+ observerCount--;
+ if (observerCount == 0) {
+ choreographer.removeFrameCallback(this);
+ sampledVsyncTimeNs = C.TIME_UNSET;
+ }
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoListener.java
new file mode 100644
index 0000000000..a469366b78
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoRendererEventListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoRendererEventListener.java
new file mode 100644
index 0000000000..6509a353b2
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/VideoRendererEventListener.java
@@ -0,0 +1,198 @@
+/*
+ * 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 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. All methods have no-op default implementations to
+ * allow selective overrides.
+ */
+public interface VideoRendererEventListener {
+
+ /**
+ * Called when the renderer is enabled.
+ *
+ * @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it
+ * remains enabled.
+ */
+ default void onVideoEnabled(DecoderCounters counters) {}
+
+ /**
+ * Called when a decoder is created.
+ *
+ * @param decoderName The decoder that was created.
+ * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization
+ * finished.
+ * @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
+ */
+ 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.
+ */
+ default void onVideoInputFormatChanged(Format format) {}
+
+ /**
+ * Called to report the number of frames dropped by the renderer. Dropped frames are reported
+ * whenever the renderer is stopped having dropped frames, and optionally, whenever the count
+ * 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.
+ */
+ default void onDroppedFrames(int count, 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, rotation or pixel aspect ratio 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 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
+ * content.
+ */
+ 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
+ * is rendered for the first time since the renderer was reset.
+ *
+ * @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(@Nullable Surface surface) {}
+
+ /**
+ * Called when the renderer is disabled.
+ *
+ * @param counters {@link DecoderCounters} that were updated by the renderer.
+ */
+ default void onVideoDisabled(DecoderCounters counters) {}
+
+ /**
+ * Dispatches events to a {@link VideoRendererEventListener}.
+ */
+ final class EventDispatcher {
+
+ @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(@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(DecoderCounters decoderCounters) {
+ if (handler != null) {
+ handler.post(() -> castNonNull(listener).onVideoEnabled(decoderCounters));
+ }
+ }
+
+ /** 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(Format format) {
+ if (handler != null) {
+ handler.post(() -> castNonNull(listener).onVideoInputFormatChanged(format));
+ }
+ }
+
+ /** 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(
+ 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(@Nullable Surface surface) {
+ if (handler != null) {
+ handler.post(() -> castNonNull(listener).onRenderedFirstFrame(surface));
+ }
+ }
+
+ /** 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/package-info.java
new file mode 100644
index 0000000000..7053c14d16
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/CameraMotionListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/CameraMotionListener.java
new file mode 100644
index 0000000000..87bd94c5bc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java
new file mode 100644
index 0000000000..378363aca0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/FrameRotationQueue.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/FrameRotationQueue.java
new file mode 100644
index 0000000000..450058fb6a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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:
+ *
+ * <ul>
+ * <li>Queues the rotation metadata extracted from camera motion track.
+ * <li>Converts the metadata to rotation matrices in OpenGl coordinate system.
+ * <li>Recenters the rotations to componsate the yaw of the initial rotation.
+ * </ul>
+ */
+public final class FrameRotationQueue {
+ private final float[] recenterMatrix;
+ private final float[] rotationMatrix;
+ private final TimedValueQueue<float[]> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/Projection.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/Projection.java
new file mode 100644
index 0000000000..e3d614cab3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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 &gt; 0.
+ * @param latitudes Number of rows that make up the sphere. Must be &gt;= 1.
+ * @param longitudes Number of columns that make up the sphere. Must be &gt;= 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java
new file mode 100644
index 0000000000..cff4b2845d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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.
+ *
+ * <p>The mesh boxes parsed are described at <a
+ * href="https://github.com/google/spatial-media/blob/master/docs/spherical-video-v2-rfc.md">
+ * Spherical Video V2 RFC</a>.
+ *
+ * <p>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<Mesh> 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<Mesh> 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<Mesh> 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<Mesh> parseRawMshpData(ParsableByteArray input) {
+ ArrayList<Mesh> 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/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/spherical/package-info.java
new file mode 100644
index 0000000000..7ab7fced0b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/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;